Dockerで3層アーキテクチャを構築しよう

このハンズオンでは、Dockerを使って3層アーキテクチャのノートアプリを構築する方法について実際にハンズオン形式で手を動かしながら体験します。

  • 3層アーキテクチャの概念と各層の役割
  • データベース層(MySQL)コンテナの構築
  • アプリケーション層(Python FastAPI)コンテナの構築
  • プレゼンテーション層(HTML/CSS/JavaScript)コンテナの構築
  • Dockerネットワークによるコンテナ間通信

1. 事前準備

Gitの基礎知識を習得していることを前提とします。自信のない方は先に以下の講座を実施してください。

この講座のハンズオンでは、以下のツールやアカウントが必要です。まだ準備できていない場合は、リンク先の手順に沿って準備をお願いします。

2. 3層アーキテクチャとは

この講座では、アプリケーションを3つの層に分割して設計する「3層アーキテクチャ」という設計パターンをベースに、コンテナによるWebアプリケーションをデプロイしていきます。

役割 具体例 よくある構成
プレゼンテーション層 フロントエンド ユーザが直接触れる部分。ユーザからの入力を受け付けてアプリケーション層に渡す HTML/CSS、React、Next.js など
アプリケーション層 バックエンド ビジネスロジックを処理する層。リクエストを受け取り、データベース層と通信して結果を返す FastAPI(Python)、Rails(Ruby)、Go など
データ層 データベース データを永続的に保存する層。アプリケーション層からのリクエストに応じてデータの読み書きを行う MySQL、PostgreSQL、MongoDB など

今回のハンズオンでは、プレゼンテーション層にHTML/CSS/JavaScript(Nginx)、アプリケーション層にPythonのFastAPI、データ層にMySQLを使用します。

3層アーキテクチャには多くの利点があります。まず、各層を別々のチームで独立して開発できるため、大規模なプロジェクトでも効率的に作業を進められます。また、1つの層を変更しても他の層に影響しにくいため、保守性が向上します。さらに、負荷が高い層だけを増強できるスケーラビリティも大きなメリットです。セキュリティの面でも、データベースを直接外部に公開せず、アプリケーション層経由でアクセスさせることで、より安全なシステムを構築できます。

3層アーキテクチャを構築するには、各層に対応した環境を用意する必要があります。これらを個別にインストールするとバージョンの競合や設定の問題が発生しがちですが、コンテナを使えば各層を独立した環境として簡単に構築できます。コンテナは「各層を独立させる」という3層アーキテクチャの考え方と非常に相性が良いため、現代のWebアプリケーション開発で広く採用されています。

今回構築する3層アーキテクチャの全体構成を以下の図に示します。

flowchart LR
    User["ユーザ<br>ブラウザ"]
    subgraph Docker
        direction LR
        subgraph note-network
            API["api-container<br>FastAPI<br>ポート 8000"]
            DB["db-container<br>MySQL<br>notedb"]
            API -->|SQL| DB
        end
        Front["front-container<br>Nginx<br>ポート 3000"]
    end
    User -->|"http://localhost:3000"| Front
    User -->|"http://localhost:8000<br>(API呼び出し)"| API

3. ハンズオンの概要

このハンズオンでは、3つのコンテナを作成し、Dockerネットワークで接続してシンプルな「ノートアプリ」を構築します。

3.1 今回の構成

今回は、プレゼンテーション層にHTML/CSS/JavaScript(Nginx)、アプリケーション層にPythonのFastAPI、データベース層にMySQLを用いた3層アーキテクチャを構成していきます。

技術 特徴
プレゼンテーション層 HTML/CSS/JavaScript(Nginx) シンプルな静的ファイル構成。Nginxで高速に配信
アプリケーション層 FastAPI Pythonの高速なWebフレームワーク。シンプルな記述でREST APIを構築できる
データベース層 MySQL 世界で最も普及しているオープンソースのリレーショナルデータベース

3.2 画面構成

ノート一覧画面のみのシンプルな構成です。画面上部にノート追加フォーム、下部にノート一覧を配置します。各ノートには「詳細」「編集」「削除」ボタンがあります。

3.3 APIの構成

アプリケーション層は「API」として機能を提供します。以下のようなAPIのエンドポイントを用意します。

エンドポイント メソッド 説明
/notes GET ノート一覧を取得
/notes/{id} GET ノート詳細を取得
/notes POST 新しいノートを作成
/notes/{id} PUT ノートを更新
/notes/{id} DELETE ノートを削除

3.4 データベース項目

データベース層で用意するデータベースのテーブル構成です。ノートを管理するシンプルな構成です。

項目 説明
id INT ノートの一意識別子(自動採番)
title VARCHAR(100) ノートのタイトル
content TEXT ノートの内容
created_at TIMESTAMP 作成日時
updated_at TIMESTAMP 更新日時

4. プロジェクトの準備

4.1 サンプルリポジトリのクローン

このハンズオンで使用するノートアプリケーションのソースコードは、GitHubのサンプルリポジトリに用意しています。コマンドプロンプト(Windows)またはターミナル(Mac)を開き、任意の場所で以下のコマンドを実行します。

git clone https://github.com/gevanni-academy/sample-note-api.git

クローンが完了したら、Visual Studio Codeを起動し、「ファイル」→「フォルダーを開く」から、クローンしたsample-note-apiフォルダを開いてください。以降の操作は、Visual Studio Codeのターミナルから行います。

4.2 フォルダ構成の確認

クローンしたリポジトリのフォルダ構成は以下のようになっています。

sample-note-api
├── api/
│   ├── Dockerfile
│   ├── main.py
│   ├── models.py
│   ├── database.py
│   ├── requirements.txt
│   └── routers/
│       └── notes.py
└── front/
    ├── Dockerfile
    ├── index.html
    ├── style.css
    └── script.js

各フォルダは3層アーキテクチャの各層に対応しています。

フォルダ 内容
api/ アプリケーション層 FastAPIのソースコード、Dockerfilerequirements.txt
front/ プレゼンテーション層 HTML/CSS/JavaScriptのフロントエンド、Nginx用のDockerfile

データベース層はMySQLの公式イメージをそのまま使うため、ソースコードのフォルダはありません。

💡 ポイント
リポジトリにはcompose.yamldb/フォルダも含まれていますが、これらは後続の講座で使用するファイルです。この講座では使用しないため、無視してください。
💡 ポイント
PythonやFastAPIの詳しい説明は省略します。コードの詳細については「PythonでREST APIを作ろう」講座に記載があります。この講座では、Dockerの操作に集中します。

5. データベース層の構築

最初に、データベース層となるMySQLコンテナを構築します。

5.1 イメージの取得

MySQLの公式イメージをDocker Hubから取得します。

docker pull mysql:latest
💡 ポイント
latestは最新バージョンを指すタグです。本番環境では、mysql:8.0のようにバージョンを明示的に指定することが推奨されます。

イメージが取得されたことを確認します。

docker image ls

以下のようにmysqlが表示されていれば成功です。

REPOSITORY   TAG       IMAGE ID       CREATED       SIZE
mysql        latest    xxxxxxxxxxxx   x days ago    xxx MB

5.2 コンテナの起動

MySQLコンテナを起動します。

docker container run -d --name db-container \
  -v notedb-data:/var/lib/mysql \
  --env MYSQL_ROOT_PASSWORD=rootpass \
  --env MYSQL_DATABASE=notedb \
  --env MYSQL_USER=noteuser \
  --env MYSQL_PASSWORD=notepass \
  mysql:latest
オプション 説明
-d - バックグラウンドでコンテナを実行する
--name db-container コンテナに名前を付ける
-v notedb-data:/var/lib/mysql - データ永続化用のボリュームをマウントする
--env MYSQL_ROOT_PASSWORD rootpass MySQLのrootユーザのパスワード(必須)
--env MYSQL_DATABASE notedb 起動時に自動作成するデータベース名
--env MYSQL_USER noteuser アプリケーション用のユーザ名
--env MYSQL_PASSWORD notepass アプリケーション用ユーザのパスワード
💡 ポイント
-v notedb-data:/var/lib/mysqlでボリュームをマウントすることで、コンテナを削除してもデータベースのデータが保持されます。コンテナを再作成しても、同じボリュームをマウントすればデータを引き継ぐことができます。
📝 MySQL環境変数について
MySQLの公式イメージは、環境変数を使って初期設定を行うことができます。MYSQL_DATABASEを指定すると、コンテナ起動時にデータベースが自動作成されます。MYSQL_USERMYSQL_PASSWORDで作成したユーザは、MYSQL_DATABASEで指定したデータベースへのアクセス権限が自動的に付与されます。

コンテナが起動していることを確認します。

docker container ls

STATUSUpになっていれば、コンテナは正常に起動しています。

5.3 テーブルの作成

MySQLコンテナに接続し、ノート用のテーブルを作成します。

docker container exec -it db-container mysql -u noteuser -pnotepass notedb

mysql> の待ち受け状態になれば、MySQLにログインできています。

MySQLのプロンプトが表示されたら、以下のSQLを実行してテーブルを作成します。

CREATE TABLE notes (
    id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(100) NOT NULL,
    content TEXT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

テーブルが作成されたことを確認します。

SHOW TABLES;

以下のようにnotesテーブルが表示されれば成功です。

+------------------+
| Tables_in_notedb |
+------------------+
| notes            |
+------------------+

MySQLからログアウトします。

exit
💡 ポイント
実はこのアプリケーションには、APIコンテナ起動時にテーブルを自動作成する仕組み(lifespan関数)が組み込まれています。ここで手動でテーブルを作成したのは、MySQLコンテナに接続してSQLを実行する操作を体験するためです。
💡 ポイント
起動したdb-containerは、この後のハンズオンでも使用するため、削除せずに残しておいてください。

6. アプリケーション層の構築

次に、アプリケーション層となるFastAPIコンテナを構築します。クローンしたリポジトリのapiフォルダにソースコードとDockerfileが用意されています。

📝 REST APIとは
REST APIは、HTTPメソッド(GET、POST、PUT、DELETEなど)を使ってデータをやり取りする仕組みです。URLでリソース(データ)を指定し、メソッドで操作を指定します。例えば「GET /notes」でノート一覧を取得、「POST /notes」で新規ノートを作成します。Webアプリケーションでフロントエンドとバックエンドが通信する標準的な方法として広く使われています。

6.1 Dockerfileの確認

api/Dockerfileを開いてみましょう。以下の内容が記述されています。

FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY main.py models.py database.py ./
COPY routers/ ./routers/

EXPOSE 8000

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

このDockerfileで行っている各命令について、1行ずつ解説します。

FROM python:3.11-slim

ここでは、ベースイメージとしてPython 3.11の軽量版(slim)を使用することを指定しています。slimは不要なパッケージを省いた軽量なイメージで、コンテナサイズを小さくできます。

WORKDIR /app

ここでは、コンテナ内の作業ディレクトリを/appに設定しています。以降の命令(COPY、RUNなど)はこのディレクトリを基準に実行されます。

COPY requirements.txt .

ここでは、ホストマシンのrequirements.txtをコンテナの作業ディレクトリ(/app)にコピーしています。末尾の.は現在の作業ディレクトリを意味します。

RUN pip install --no-cache-dir -r requirements.txt

ここでは、requirements.txtに記載されたライブラリをインストールしています。--no-cache-dirオプションを付けることで、キャッシュを保存せずイメージサイズを削減しています。

COPY main.py models.py database.py ./
COPY routers/ ./routers/

ここでは、アプリケーションのソースコードをコンテナの作業ディレクトリにコピーしています。main.pymodels.pydatabase.pyを作業ディレクトリ直下に、routers/フォルダをサブフォルダごとコピーしています。

EXPOSE 8000

ここでは、コンテナがポート8000で待ち受けることを宣言しています。これはドキュメント的な役割であり、実際のポート公開はdocker run-pオプションで行います。

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

ここでは、コンテナ起動時に実行するコマンドを指定しています。uvicornサーバを使ってmain.py内のapp(FastAPIアプリケーション)をポート8000で起動します。

6.2 イメージのビルド

Visual Studio Codeのターミナルからapiフォルダに移動し、Dockerイメージをビルドします。

cd api
docker image build -t api-image .
オプション 説明
-t api-image イメージに名前を付ける
. - 現在のディレクトリのDockerfileを使用する

イメージが作成されたことを確認します。

docker image ls

以下のようにapi-imageが表示されていれば成功です。

REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
api-image    latest    xxxxxxxxxxxx   x seconds ago    xxx MB
mysql        latest    xxxxxxxxxxxx   x days ago       xxx MB

6.3 コンテナの起動

APIコンテナを起動します。apiフォルダで以下のコマンドを実行します。

Macの場合:

docker container run -d -p 8000:8000 --name api-container \
  -v $(pwd):/app \
  -e DB_HOST=db-container \
  -e DB_USER=noteuser \
  -e DB_PASSWORD=notepass \
  -e DB_NAME=notedb \
  api-image

Windowsの場合(PowerShell):

docker container run -d -p 8000:8000 --name api-container -v ${PWD}:/app -e DB_HOST=db-container -e DB_USER=noteuser -e DB_PASSWORD=notepass -e DB_NAME=notedb api-image
オプション 説明
-d - バックグラウンドでコンテナを実行する
-p 8000:8000 ホストの8000番ポートをコンテナの8000番ポートに転送する
--name api-container コンテナに名前を付ける
-v $(pwd):/app - 現在のフォルダをコンテナの/appにバインドマウントする
-e DB_HOST db-container 接続先のデータベースコンテナ名
-e DB_USER noteuser データベース接続用のユーザ名
-e DB_PASSWORD notepass データベース接続用のパスワード
-e DB_NAME notedb 接続先のデータベース名
💡 ポイント
-v $(pwd):/appでバインドマウントすることで、現在のフォルダ(apiフォルダ)がコンテナ内の/appにマウントされます。ソースコードを変更した場合、コンテナを再起動すると変更が反映されます。

コンテナが起動していることを確認します。

docker container ls

STATUSUpになっていれば、コンテナは正常に起動しています。

6.4 APIの動作確認

APIサーバが起動したことを確認します。

curl http://localhost:8000/health

以下のレスポンスが返ってくれば、APIサーバは正常に動作しています。

{"status":"healthy"}
💡 ポイント
この時点では、APIコンテナとDBコンテナは同じネットワークに属していないため、データベースとの接続は失敗します。次のセクションでネットワークを設定し、コンテナ間通信を可能にします。

7. ネットワークの設定

APIコンテナとDBコンテナが通信できるように、Dockerネットワークを作成して両方のコンテナを接続します。

7.1 ネットワークの作成

ノートアプリ用のネットワークを作成します。

docker network create note-network

ネットワークが作成されたことを確認します。

docker network ls

以下のようにnote-networkが表示されていれば成功です。

NETWORK ID     NAME           DRIVER    SCOPE
xxxxxxxxxxxx   note-network   bridge    local
xxxxxxxxxxxx   bridge         bridge    local
xxxxxxxxxxxx   host           host      local
xxxxxxxxxxxx   none           null      local

7.2 コンテナをネットワークに接続

既に起動しているコンテナを、作成したネットワークに接続します。

まず、DBコンテナを接続します。

docker network connect note-network db-container

次に、APIコンテナを接続します。

docker network connect note-network api-container

7.3 ネットワーク接続の確認

ネットワークの詳細を確認し、両方のコンテナが接続されていることを確認します。

docker network inspect note-network

以下のような出力結果が表示されます。Containersセクションに、db-containerapi-containerの両方が表示されていれば、正しく接続されています。

[
    {
        "Name": "note-network",
        "Id": "a1b2c3d4e5f6...",
        "Created": "2026-01-15T10:00:00.000000000Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.18.0.0/16",
                    "Gateway": "172.18.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "abc123...": {
                "Name": "api-container",
                "EndpointID": "def456...",
                "MacAddress": "02:42:ac:12:00:03",
                "IPv4Address": "172.18.0.3/16",
                "IPv6Address": ""
            },
            "xyz789...": {
                "Name": "db-container",
                "EndpointID": "ghi012...",
                "MacAddress": "02:42:ac:12:00:02",
                "IPv4Address": "172.18.0.2/16",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {}
    }
]

7.4 API動作確認

ネットワーク接続が完了したら、APIからデータベースへの接続をテストします。

まず、ノートを1件作成します。

curl -X POST http://localhost:8000/notes \
  -H "Content-Type: application/json" \
  -d '{"title": "Docker Notes", "content": "Learning the basics of containers"}'

以下のようなレスポンスが返れば、ノートの作成に成功しています。

{"title":"Docker Notes","content":"Learning the basics of containers","id":1,"created_at":"2026-01-01T10:00:00","updated_at":"2026-01-01T10:00:00"}

次に、作成したノートを取得します。

curl http://localhost:8000/notes

先ほど作成したノートが取得できれば、APIコンテナからDBコンテナへの通信が正常に行われています。

[{"title":"Docker Notes","content":"Learning the basics of containers","id":1,"created_at":"2026-01-01T10:00:00","updated_at":"2026-01-01T10:00:00"}]

8. プレゼンテーション層の構築

最後に、プレゼンテーション層となるWebフロントエンドコンテナを構築します。クローンしたリポジトリのfrontフォルダにHTML、CSS、JavaScriptファイルとDockerfileが用意されています。

8.1 Dockerfileの確認

front/Dockerfileを開いてみましょう。以下の内容が記述されています。

FROM nginx:alpine

COPY . /usr/share/nginx/html/

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

各命令について解説します。

FROM nginx:alpine

ここでは、軽量なNginxイメージをベースとして使用しています。Alpineベースのイメージはサイズが小さく、起動も高速です。

COPY . /usr/share/nginx/html/

ここでは、frontフォルダ内のすべてのファイル(index.htmlstyle.cssscript.js)をNginxのデフォルトの公開ディレクトリにコピーしています。

EXPOSE 80

ここでは、コンテナがポート80で待ち受けることを宣言しています。

CMD ["nginx", "-g", "daemon off;"]

ここでは、Nginxをフォアグラウンドで実行するコマンドを指定しています。daemon off;オプションにより、コンテナが終了せずに動作し続けます。

8.2 イメージのビルド

ターミナルでfrontフォルダに移動してからDockerイメージをビルドします。

cd ../front
docker image build -t front-image .

イメージが作成されたことを確認します。

docker image ls

以下のようにfront-imageが表示されていれば成功です。

REPOSITORY    TAG       IMAGE ID       CREATED          SIZE
front-image   latest    xxxxxxxxxxxx   x seconds ago    xxx MB
api-image     latest    xxxxxxxxxxxx   x minutes ago    xxx MB
mysql         8.0       xxxxxxxxxxxx   x days ago       xxx MB

8.3 コンテナの起動

フロントコンテナを起動します。frontフォルダで以下のコマンドを実行します。

Macの場合:

docker container run -d -p 3000:80 --name front-container \
  -v $(pwd):/usr/share/nginx/html \
  front-image

Windowsの場合(PowerShell):

docker container run -d -p 3000:80 --name front-container -v ${PWD}:/usr/share/nginx/html front-image
オプション 説明
-d - バックグラウンドでコンテナを実行する
-p 3000:80 ホストの3000番ポートをコンテナの80番ポートに転送する
--name front-container コンテナに名前を付ける
-v $(pwd):/usr/share/nginx/html - 現在のフォルダをコンテナにバインドマウントする
💡 ポイント
-v $(pwd):/usr/share/nginx/htmlでバインドマウントすることで、現在のフォルダ(frontフォルダ)がコンテナ内にマウントされます。ホスト側でindex.htmlを編集すると、ブラウザを再読み込みするだけで変更が反映されます。

コンテナが起動していることを確認します。

docker container ls

STATUSUpになっていれば、コンテナは正常に起動しています。

💡 ポイント
フロントエンドコンテナはnote-networkに接続していません。これは、フロントエンドのJavaScriptがユーザのブラウザ上で動作するためです。ブラウザからAPIへのリクエストは、http://localhost:8000(ホストマシン)を経由して行われ、Dockerネットワークを通じた直接通信は行いません。そのため、フロントエンドコンテナをDockerネットワークに参加させる必要はありません。

8.4 動作確認

Webブラウザを開き、以下のURLにアクセスします。

http://localhost:3000

「ノートアプリ」というページが表示され、先ほどcurlで作成したノートが一覧表示されれば成功です。

以下の機能を試してみてください。

  1. ノート作成 - 「新規ノート」欄にタイトルと内容を入力し、「追加」ボタンをクリック
  2. 詳細表示 - 「詳細」ボタンをクリックすると、ノートの全文と作成日・更新日が表示される
  3. 編集 - 「編集」ボタンをクリックすると、タイトルと内容を編集できる
  4. 削除 - 「削除」ボタンをクリックすると、ノートが削除される

9. 不要リソースの削除

ハンズオンが終わったら、作成したリソースを削除します。

9.1 コンテナの停止

まず、起動中のすべてのコンテナを停止します。

docker container stop front-container api-container db-container

9.2 コンテナの削除

停止したコンテナを削除します。

docker container rm front-container api-container db-container

コンテナが削除されたことを確認します。

docker container ls -a

front-containerapi-containerdb-containerが一覧から消えていれば、削除は完了です。

9.3 イメージの削除

作成したイメージを削除します。

docker image rm front-image api-image mysql:latest

イメージが削除されたことを確認します。

docker image ls

front-imageapi-imagemysql:latestが一覧から消えていれば、削除は完了です。

9.4 ボリュームの削除

データベース用に作成したボリュームを削除します。

docker volume rm notedb-data

ボリュームが削除されたことを確認します。

docker volume ls

notedb-dataが一覧から消えていれば、削除は完了です。

9.5 ネットワークの削除

作成したDockerネットワークを削除します。

docker network rm note-network

ネットワークが削除されたことを確認します。

docker network ls

note-networkが一覧から消えていれば、削除は完了です。

NETWORK ID     NAME      DRIVER    SCOPE
xxxxxxxxxxxx   bridge    bridge    local
xxxxxxxxxxxx   host      host      local
xxxxxxxxxxxx   none      null      local

9.6 プロジェクトフォルダの削除(任意)

クローンしたプロジェクトフォルダが不要な場合は、Visual Studio Codeでフォルダを閉じた後、エクスプローラー(Windows)やFinder(Mac)からsample-note-apiフォルダを削除してください。

10. まとめ

このハンズオンでは、Dockerを使って3層アーキテクチャのノートアプリを構築する方法を体験しました。

  • 3層アーキテクチャは、プレゼンテーション層・アプリケーション層・データ層の3つに分離する設計パターン
  • データベース層として、MySQLコンテナを構築しノート用のテーブルを作成した
  • アプリケーション層として、Dockerfileからイメージをビルドし、FastAPIコンテナを起動した
  • Dockerネットワークを作成し、コンテナ間通信を可能にした
  • プレゼンテーション層として、Dockerfileからイメージをビルドし、Nginxコンテナを起動した
  • 同じネットワーク内のコンテナは、コンテナ名で通信できる
  • 環境変数を使うことで、設定を柔軟に変更できる