docker.svg

【Docker】Docker Compose の基本

Docker

Docker Compose とは

これまでのDockerの記事では、docker container runコマンドを用いて1つ1つコンテナを作成していました。 しかし複数コンテナを使用する場合、例えば開発メンバーが同じ環境を作ろうとすると、同じようにコマンドを実行していく必要があります。 すべての開発メンバーがDockerに関する知識があることが望ましいですが、そうでないケースも多々あるかと思います。

Docker Composeは、YAMLを使用して複数コンテナを定義することで、コンテナの起動、停止、管理を簡易化することができます。 また本番や開発などの環境の切り替えやCI/CDなどにも利用できます。

ここでは簡単な三層構造のアプリケーションを作成することで、Docker Composeの基本的な部分を説明していきます。

docker-compose.yml

始めにDBとしてMySQLのコンテナを定義していきます。 docker container runでは以下のようにしてコンテナを起動します。

MySQLコンテナの作成
$ docker volume create db-store
$ docker container run --name db -e MYSQL_ROOT_PASSWORD=password --mount type=volume,src=db-store,dst=/var/lib/mysql -d mysql

簡単に補足をしておきます。

-eオプションで環境変数MYSQL_ROOT_PASSWORDを設定しています。 これはルートユーザーのパスワードになります。 --mountオプションで予め作成したdb-storeボリュームを/var/lib/mysqlにマウントしています。 これによりデータが永続化されます。

これをDocker Composeで定義していきます。 Docker Composeによる定義は、docker-compose.ymlという名前のファイルにしていきます。 ここでは/my-appディレクトリ直下にdocker-compose.ymlを作成したとします。 作成したファイルに以下を記述します(YAML自体の書式については割愛します)。

docker-compose.yml
services:  # コンテナの定義
  db:
    image: mysql
    environment:
      - MYSQL_ROOT_PASSWORD=password
    volumes:
      - db-store:/var/lib/mysql
volumes:   # ボリュームの作成
  db-store:

servicesはサービスとしてコンテナの定義を行います。 dbはサービス名になります。 コンテナの定義内容としては、docker container runで指定した情報と同じです。

  • image: 使用するイメージを表す(MySQLを使用するためmysqlを設定)
  • environment: 環境変数を表す(ルートユーザーのパスワード設定のためにMYSQL_ROOT_PASSWORDを設定)
  • volumes: データマウントの設定を表す(db-storeボリュームを/ver/lib/mysqlにマウント)

DBを利用するためにはボリュームを作成する必要があります。 これは、servicesと同じ階層に記述したvolumesに記述することでできます。

起動/停止

ではDocker Composeを使用してコンテナを起動します。 Docker Composeでコンテナを起動するには、docker-compose.ymlのあるディレクトリでdocker compose upコマンドを実行します。 アタッチモードで起動するため、終了する場合はCtrl-Cを入力します。 デタッチモードで起動したい場合は-dオプションを指定してください。

docker compose up
$ docker compose up -d
[+] Running 11/11
  db 10 layers [⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿]      0B/0B      Pulled       11.3s 
    ea4e27ae0b4c Pull complete                             1.5s 
    837904302482 Pull complete                             0.7s 
    3c574b61b241 Pull complete                             0.7s 
    654fc4f3eb2d Pull complete                             1.6s 
    32da9c2187e3 Pull complete                             1.3s 
    dc99c3c88bd6 Pull complete                             2.0s 
    970181cc0aa6 Pull complete                             3.7s 
    d77b716c39d5 Pull complete                             2.3s 
    9e650d7f9f83 Pull complete                             4.3s 
    acc21ff36b4b Pull complete                             8.2s 
[+] Running 3/3
  Network my-app_default    Created                        0.0s 
  Volume "my-app_db-store"  Created                        0.0s 
  Container my-app-db-1     Started                        0.1s 

まず必要なイメージがPullされます。 元々Pullされている場合は省略されます。

その後、ネットワーク、ボリューム、コンテナが作成され、コンテナが起動します。 ネットワーク、ボリューム、コンテナは、それぞれmy-app_defaultmy-app_db-storemy-app-db-1という名前で作成されます。 各名前にmy-appとあるようにカレントディレクトリの名前が使用されます。 ネットワークについては省きますが、コンテナ、ボリュームはdocker-compose.ymlで定義したものが使用されています。

Docker Composeで起動したコンテナを停止させるには、docker compose downコマンドを実行します。

docker compose down
$ docker compose down 
[+] Running 2/2
  Container my-app-db-1   Removed              1.0s 
  Network my-app_default  Removed              0.0s

Removedとあるようにコンテナとネットワークは削除されます。 ボリュームは削除されないため、不要な場合はdocker volume rmで直接削除するか、--volumesオプションを指定します。 また使用したイメージもそのまま残ります。 不要な場合はdocker image rmで直接削除するか、--rmi allオプションを指定します。 --rmi allは、docker-compose.ymlで使用しているすべてのイメージを削除します。 ただし、他のコンテナで使用しているイメージは削除できません。

docker compose down ボリューム、イメージの削除
$ docker compose down --volumes --rmi all
[+] Running 4/4
  Container my-app-db-1   Removed             1.0s 
  Volume my-app_db-store  Removed             0.0s 
  Image mysql:latest      Removed             0.2s 
  Network my-app_default  Removed             0.0s 

複数コンテナの定義

上記ではDB用のコンテナのみを作成しました。 以下の説明では、AP、WEB用のコンテナを1つのdocker-compose.ymlで定義していきます。

DBコンテナ

DBコンテナは基本的に上記のものを使用するのですが、少しだけ追記します。

docker-compose.yml
services:
  db:
    image: mysql:latest
    environment:
      - MYSQL_ROOT_PASSWORD=password
      - MYSQL_USER=user           # 追記
      - MYSQL_PASSWORD=password   # 追記
      - MYSQL_DATABASE=mydb       # 追記
    volumes:
      - db-store:/var/lib/mysql
      - ./db/script:/docker-entrypoint-initdb.d  # 追記
volumes:
  db-store:

環境変数に追記したMYSQL_USERMYSQL_PASSWORDは、ユーザーを作成するためのものです。 APコンテナからDBにアクセスするための認証情報として使用します。 もう1つ環境変数に追記したMYSQL_DATABASEはデータベースを作成するためのものです。 ここではmydbというデータベースを作成しています。

バインドマウントの追記は、初期データを設定するためのものです。 /docker-entrypoint-initdb.dにSQLファイルを配備すると、コンテナ起動時にSQLを実行してくれます。 ここではホスト側の./db/script内にSQLファイルを作成しておけば、初期データが設定されることになります。

では以下のようにファイルを作成し、docker compose upでコンテナを起動します。

ディレクトリ階層
my-app/
 ┝ db/
 │  └ script/
 │      ┝ 1.message.sql
 │      └ 2.user.sql
 └ docker-compose.yml
1.message.sql
CREATE TABLE IF NOT EXISTS message (
  id INT AUTO_INCREMENT PRIMARY KEY,
  message VARCHAR(256)
);

INSERT INTO message (message) VALUES
  ('Hello.'),
  ('Good morning.'),
  ('Good evening.');
2.user.sql
CREATE TABLE IF NOT EXISTS user (
  id INT PRIMARY KEY,
  name VARCHAR(20)
);

INSERT INTO user (id, name) VALUES
  (1, 'Taro'),
  (2, 'Jiro'),
  (3, 'Saburo');

SQLが実行されていることを確認するために、コンテナ内に入ってデータの確認をします。 これまで通りdocker container execを使用しても良いのですが、Docker Composeを使用している場合はdocker compose execを使用することができます。

docker compose exec
$ docker compose exec db bash
データの確認
bash-4.4# mysql --user=user --password=password
mysql> use mydb
mysql> select * from message;
+----+---------------+
| id | message       |
+----+---------------+
|  1 | Hello.        |
|  2 | Good morning. |
|  3 | Good evening. |
+----+---------------+
3 rows in set (0.00 sec)

mysql> select * from user;
+----+--------+
| id | name   |
+----+--------+
|  1 | Taro   |
|  2 | Jiro   |
|  3 | Saburo |
+----+--------+
3 rows in set (0.01 sec)

上記の通り、ユーザーが作成されていること、データベースが作成されていること、初期データが登録されていることが確認できました。

初期データの登録について2点補足しておきます。

1つは実行順についてです。 ファイル名からなんとなく察しがついているかもしれませんが、ファイルの並び順で実行されます。 そのため実行順が重要となる場合は、例の通り先頭に数字を入れるなどして実行順を制御します。

もう1つは実行条件についてです。 データベースが存在している場合、SQLは実行されません。 つまり、初期データを登録しなおしたい場合は、ボリュームを削除する必要があります。

APコンテナ

APコンテナはNode.js(Express)で作成します。 API用に/apiディレクトリを作成し、以下のファイルを作成します。

ディレクトリ階層
my-app/
 ┝ api/
 │  ┝ Dockerfile
 │  ┝ index.js
 │  └ package.json 
 ┝ db/
 └ docker-compose.yml
package.json
{
  "name": "api",
  "version": "1.0.0",
  "description": "API Container.",
  "main": "index.js",
  "scripts": {
    "start": "nodemon index.js"  //サーバー起動用コマンド
  },
  "dependencies": {
    "express": "^4.18.2",  //Expressを利用する
    "mysql2": "^3.9.2",    //MySQL接続用
    "nodemon": "^3.1.0"    //Node.js起動用(ホットリロード)
  }
}
index.js
const express = require("express");
const mysql = require("mysql2");
const app = express();

// DB接続情報
const db = mysql.createConnection({
  host: "db",    //DBのサービス名を指定
  user: "user",
  password: "password",
  database: "mydb",
  port: 3306,
});

app.get("/api/message", (req, res) => {
  const query = "SELECT * FROM message";
  db.query(query, (err, result) => {
    if (err) {
      res.status(500).send("Error");
    } else {
      res.json(result);
    }
  });
});

app.get("/api/user", (req, res) => {
  const query = "SELECT * FROM user";
  db.query(query, (err, result) => {
    if (err) {
      res.status(500).send("Error");
    } else {
      res.json(result);
    }
  });
})

const PORT = 3000;
const HOST = '0.0.0.0';
app.listen(PORT, HOST);

Expressの説明は本件からずれてしまうため、ポイントだけ説明します。

サーバーはnpm run startで起動できるよう、package.jsonscriptsに記載しています。 実際にはnodemonを使用しており、ソースの変更が即座に判定されるようにしています(ホットリロード)。

index.jsにはAPIの処理を定義しており、データベースで作成した各テーブルのすべてのデータをJSON形式で返す/api/message/api/userを作成しています。 DBコンテナに接続するために、hostにはサービス名であるdbを指定します。 通常のコンテナでは、同一ネットワーク上であればコンテナ名で名前解決ができましたが、Docker Composeではサービス名で名前解決してくれます。

次にAPコンテナを作成するためのDockerfileを作成します。 DBコンテナとは異なり、そのままイメージを使用するのではなく、パッケージのインストールやサーバーの起動といった処理をDockerfileで定義します。

Dockerfile
FROM node:latest

# /api のファイルを /app にコピー
WORKDIR /app
COPY . .

# パッケージのインストール
RUN npm install              

# サーバーの起動
CMD ["npm", "run", "start"]  

ではDocker ComposeにAPコンテナの定義を記述していきます。

docker-compose.yml
services:
  api:
    build: ./api  # ビルドコンテキスト
    volumes:
      - ./api:/app
    ports:   # 動作確認用
      - 3000:3000
    depends_on:   # 依存関係の設定
      db:
        condition: service_healthy
  db:
    image: mysql
    environment:
      - MYSQL_ROOT_PASSWORD=password
      - MYSQL_USER=user
      - MYSQL_PASSWORD=password
      - MYSQL_DATABASE=mydb
    volumes:
      - db-store:/var/lib/mysql
      - ./db/script:/docker-entrypoint-initdb.d
    healthcheck:  # 起動チェック用の処理
      test: "mysqladmin ping -h localhost -u root -p$$MYSQL_ROOT_PASSWORD"  # 確認処理
      interval: 5s  # 実行感覚
      timeout: 5s   # タイムアウト
      retries: 10   # リトライ回数
volumes:
  db-store:

apiとしてAPコンテナを定義しています。 DBコンテナでは使用していない記述について説明をします。

  • build: Dockerfileを使用する場合にビルドコンテキストを指定する
  • ports: ホストとのポートの関連付け(動作確認用 後に削除)
  • depends_on: 依存関係を表し、コンテナの起動順を制御

depends_onは記載の通り、コンテナの起動順を制御するためのものです。 APコンテナはDBコンテナのMySQLに接続しにいくため、MySQL自体が起動していないとエラーになります。 そのためMySQLが起動していることを確認してから、APコンテナを起動するようにします。

depends_onには、依存関係にあるコンテナと起動条件であるconditionを設定します。 conditionservice_healthyは、対象コンテナが正常であることをチェックしてから起動することを表します。 ただし、正常であるかどうかをどのように判断するかは、対象コンテナにhealthcheckで定義する必要があります。

healthcheckには、testとしてチェック用のコマンドを定義します。 今回はMySQLを使用しているため、mysqladmin pingコマンドで起動を確認します。 その他の項目についてはDockerfileのコメントの通りで、詳細は割愛します。

ではコンテナを起動して動作を確認します。 起動後、ブラウザからhttp://localhost:3000/messagehttp://localhost:3000/userにアクセスしてDBに登録されているデータが表示されることを確認します。

docker compose up
$ docker compose up -d                     
[+] Building 0.1s (9/9) FINISHED                                docker:desktop-linux
 => [api internal] load build definition from Dockerfile                        0.0s
 => => transferring dockerfile: 119B                                            0.0s
 => [api internal] load .dockerignore                                           0.0s
 => => transferring context: 34B                                                0.0s
 => [api internal] load metadata for docker.io/library/node:latest              0.0s
 => [api 1/4] FROM docker.io/library/node:latest                                0.0s
 => [api internal] load build context                                           0.0s
 => => transferring context: 71.18kB                                            0.0s
 => CACHED [api 2/4] WORKDIR /app                                               0.0s
 => CACHED [api 3/4] COPY . .                                                   0.0s
 => CACHED [api 4/4] RUN npm install                                            0.0s
 => [api] exporting to image                                                    0.0s
 => => exporting layers                                                         0.0s
 => => writing image sha256:8d49ea4a5a67f999cae5fd0a44db91505ed73267dac3a19e4e  0.0s
 => => naming to docker.io/library/my-app-api                                   0.0s
[+] Running 4/4
  Network my-app_default    Created                                            0.0s 
  Volume "my-app_db-store"  Created                                            0.0s 
  Container my-app-db-1     Healthy                                            0.0s 
  Container my-app-api-1    Started                                            0.0s 
http://localhost:3000/api/message
[{"id":1,"message":"Hello."},{"id":2,"message":"Good morning."},{"id":3,"message":"Good evening."}]
http://localhost:3000/api/user
[{"id":1,"name":"Taro"},{"id":2,"name":"Jiro"},{"id":3,"name":"Saburo"}]

Dockerfileから作成されたイメージはmy-app-apiという名前で作成されます。 前述の通り、docker compose downでは通常イメージが削除されませんが、Dockerfileで作成したイメージを削除したい場合は--rmi localを指定します。

docker compose down 作成したイメージの削除
$ docker compose down --volumes --rmi local

補足

上記ではDockerfileを使用しましたが、以下のようにDocker Composeだけでも同様の構成を定義できます。

docker-compose.yml
services:
  api:
    image: node: latest
    working_dir: /app  # 作業ディレクトリの指定
    volumes:
      - ./api:/app
    ports:
      - 3000:3000
    command: sh -c "npm install && npm run start"  # 実行コマンド(パッケージインストール + サーバー起動)
    depends_on:
      db:
        condition: service_healthy
  db:
    image: mysql
    environment:
      - MYSQL_ROOT_PASSWORD=password
      - MYSQL_USER=user
      - MYSQL_PASSWORD=password
      - MYSQL_DATABASE=mydb
    volumes:
      - db-store:/var/lib/mysql
      - ./db/script:/docker-entrypoint-initdb.d
    healthcheck:
      test: "mysqladmin ping -h localhost -u root -p$$MYSQL_ROOT_PASSWORD"
      interval: 5s
      timeout: 5s
      retries: 10
volumes:
  db-store:

WEBコンテナ

最後にWEBコンテナを作成します。 APIから取得したデータを表示するページをNGINXを使って表示できるようにします。 要領はこれまでと同じです。

/webディレクトリを作成し、以下のファイルを作成します。

ディレクトリ階層
my-app/
 ┝ api/
 ┝ db/
 ┝ web/
 │  ┝ html/
 │  │   └ index.html
 │  └ nginx/conf
 └ docker-compose.yml
html/index.html
<input type="button" value="Get Message" onclick="getMessage()" />
<div id="result"></div>

<script>
getMessage = async () => {
  const response = await fetch("/api/message");
  const data = await response.text();
  document.getElementById('result').innerText = data;
}
</script>
nginx.conf
server {
  listen 80;

  location /api {
    proxy_pass http://api:3000;
  }

  location / {
    root   /usr/share/nginx/html;
    index  index.html index.htm;
  }
}

index.htmlは表示するWEBページになります。 Get Messageボタンを押すことで、APIで定義した/api/messageにアクセスして取得したデータを表示するようにしています。

nginx.confはNGINXの設定ファイルです。 注目すべきは4〜6行目の記述です。 WEBページからAPIにアクセスするということは、ホストからAPIコンテナにアクセスすることになります。 ポートを設定すればアクセスはできるのですが、次はCORSの問題があります。 CORSでは、WEBページから異なるオリジン(プロトコル、ホスト、ポートのセット)へのアクセスが制限されます。

そこでリバースプロキシを利用します。 WEBページから直接APIにアクセスするのではなく、一度WEBサーバーを経由してAPIにアクセスするようにします。 WEBサーバー、つまりWEBコンテナからAPI(APコンテナ)にアクセスするため、アクセス情報としてサービス名が利用できます。 またサーバー同士の通信となるためCORSの問題も解決できます。 4〜6行目の記述によって、WEBサーバーの/apiへアクセスした場合は、APコンテナの3000番ポートに転送されるという設定になります。

ではこれらのファイルを用いてWEBコンテナの定義をします。

docker-compose.yml
services:
  web:
    image: nginx:latest
    volumes:
      - ./web/html:/usr/share/nginx/html
      - ./web/nginx.conf:/etc/nginx/conf.d/default.conf
    ports:
      - 8080:80
    depends_on:
      api:
        condition: service_healthy
  api:
    build: ./api
    volumes:
      - ./api:/app
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: "curl -f http://localhost:3000/health"
      interval: 10s
      timeout: 10s
      retries: 3
  db:
    image: mysql
    environment:
      - MYSQL_ROOT_PASSWORD=password
      - MYSQL_USER=user
      - MYSQL_PASSWORD=password
      - MYSQL_DATABASE=mydb
    volumes:
      - db-store:/var/lib/mysql
      - ./db/script:/docker-entrypoint-initdb.d
    healthcheck:
      test: "mysqladmin ping -h localhost -u root -p$$MYSQL_ROOT_PASSWORD"
      interval: 5s
      timeout: 5s
      retries: 10
volumes:
  db-store:

volumesには先ほど作成したindex.htmlnginx.confをNGINXの該当ディレクトリ、ファイルにバインドマウントしています。 WEBコンテナは、APコンテナが起動を確認してから起動したいため、depends_onを定義しています。 APコンテナの起動の確認方法として、チェック用のAPI(/health)を一つ用意し、レスポンスが帰ってきた場合に正常として判断します。

index.js
app.get("/health", (req, res) => {
  res.status(200).send("OK");
});

ではコンテナを起動して動作確認を行います。 すべてのコンテナが起動したら、http://localhost:8080にアクセスし、index.htmlが表示されることを確認します。 その後Get Messageボタンを押し、/api/messageの内容が表示されることを確認します。

[{"id":1,"message":"Hello."},{"id":2,"message":"Good morning."},{"id":3,"message":"Good evening."}]

その他のコマンド

最後に上記で説明できなかったDocker Composeの一部のコマンドについて補足しておきます。

images / ps

imagesはDocker Composeで使用しているイメージの一覧を表示します。

docker compose images
CONTAINER           REPOSITORY          TAG                 IMAGE ID            SIZE
my-app-api-1        my-app-api          latest              b76b16435ed3        1.11GB
my-app-db-1         mysql               latest              e68e2614955c        638MB
my-app-web-1        nginx               latest              760b7cbba31e        192MB

psはDocker Composeで作成したコンテナの一覧を表示します。

docker compose ps
$ docker compose ps
NAME           IMAGE          COMMAND                   SERVICE   CREATED          STATUS                    PORTS
my-app-api-1   my-app-api     "docker-entrypoint.s…"   api       24 seconds ago   Up 18 seconds (healthy)   
my-app-db-1    mysql          "docker-entrypoint.s…"   db        24 seconds ago   Up 23 seconds (healthy)   3306/tcp, 33060/tcp
my-app-web-1   nginx:latest   "/docker-entrypoint.…"   web       24 seconds ago   Up 7 seconds              0.0.0.0:8080->80/tcp

up / down

updownは上記でも説明した通り、コンテナの作成・起動、停止・削除をするコマンドですが、 引数でサービス名を指定することで個別に実行できます。

docker compose up / down
$ docker compose up -d web
$ docker compose down web

create / rm

createはコンテナの作成を行います。upと異なり起動はされません。 rmはコンテナの削除を行います。downと異なり停止はされず、またネットワークなどの関連情報も削除されません。 それぞれサービス名を指定することで、個別に実行可能です。

docker compose create / rm
$ docker compose create

$ docker compose ps
NAME           IMAGE          COMMAND                   SERVICE   CREATED          STATUS    PORTS
my-app-api-1   my-app-api     "docker-entrypoint.s…"   api       10 seconds ago   Created   
my-app-db-1    mysql          "docker-entrypoint.s…"   db        10 seconds ago   Created   
my-app-web-1   nginx:latest   "/docker-entrypoint.…"   web       10 seconds ago   Created   

$ docker compose rm

start / stop

startはコンテナの起動を行います。 stopはコンテナの停止を行います。 それぞれサービス名を指定することで、個別に実行可能です。

docker compose start / stop
$ docker compose start
$ docker compose stop

pause / unpause

pauseはコンテナを一時停止します。 unpauseは一時停止したコンテナを再開します。 それぞれサービス名を指定することで、個別に実行可能です。

docker compose pause / unpause
$ docker compose pause
$ docker compose unpause

logs

logsはDocke Composeで起動しているコンテナのログを表示します。 サービス名を指定することで、コンテナ毎のログを出力できます。

docker compose logs
$ docker compose logs web
ocker compose logs api
api-1  | 
api-1  | > api@1.0.0 start
api-1  | > nodemon index.js
api-1  | 
api-1  | [nodemon] 3.1.0
api-1  | [nodemon] to restart at any time, enter `rs`
api-1  | [nodemon] watching path(s): *.*
api-1  | [nodemon] watching extensions: js,mjs,cjs,json
api-1  | [nodemon] starting `node index.js`

ls

Docker Composeで起動しているプロジェクトの一覧を表示します。

docker compose ls
$ docker compose ls
NAME                STATUS              CONFIG FILES
my-app              running(3)          /Users/Jhon/my-app/docker-compose.yml