Lesson 5
複数のコンテナを同時に実行する
目次
Lesson 5
Chapter 1
Docker Composeを体験する
これまでのLessonでは、docker run
コマンドを用いてコンテナを実行する方法を学習しました。
Lesson3とLesson4では、それぞれ以下の方法でコンテナを実行しています。
- Lesson3: DockerHubから取得したイメージからコンテナを実行
- Lesson4: Dockfileを用いて作成したイメージからコンテナを実行
しかし、複数のコンテナ間の通信やお互いの関係などを制御して、まとめて実行することができませんでした。
複数のコンテナをまとめて実行?
Docker Composeとは
そこで登場するのがDocker Composeになります。
Docker Composeはcompose.yml
というファイルを使って設定を行うことで、複数のコンテナをどう連携して実行するかを定義することができます。
Lesson5で学習する内容
このchapterでは、詳しい解説を後回しにして、まずはDocker Composeを体験してもらいます。設定ファイルについての詳細は次のchapterで学習します。
Docker Composeで複数のコンテナを実行する
それでは、実際にDocker Composeを用いて複数のコンテナを実行してみましょう。
例として、サイトの訪問者をカウントするサーバーを作成します。必要なファイルを用意しましたので、以下の手順を実行して下さい。
・ステップ1
まずは作業ディレクトリを作成して下さい。
ここでは、仮にC:\Users\ユーザ名\Desktop\lesson5_1
としましょう。package.json
とindex.js
ファイルを作成して、以下の内容を貼り付けて下さい。
package.json
{
"name": "lesson5_1",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"start": "node index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.18.2",
"redis": "^4.5.1"
}
}
index.js
import express from "express";
import { createClient } from "redis";
const app = express();
const client = createClient({
url: "redis://redis-server"
});
app.get("/", async (req, res) => {
await client.connect();
let counts = await client.get("counts");
if (counts === null) {
counts = 1;
};
res.send(`あなたは${counts}人目の訪問者です`);
await client.set("counts", parseInt(counts) + 1);
await client.disconnect();
});
app.listen(process.env.PORT, () => {
console.log("listening on port", process.env.PORT);
});
・ステップ2
Dockerfile
を以下の内容で作成します。
Dockerfile
FROM node:alpine
WORKDIR '/app'
COPY package.json .
RUN npm install
COPY index.js .
CMD ["npm", "start"]
・ステップ3
compose.yml
ファイルを作成して、以下の内容を貼り付けて下さい。
compose.yml
services:
redis-server:
image: 'redis'
app:
build: .
ports:
- '4000:8000'
environment:
PORT: 8000
・ステップ4
C:\Users\ユーザ名\Desktop\lesson5_1
内にpackage.json
、index.js
、Dockerfile
、compose.yml
の4つのファイルを準備しました。
作業ディレクトリ内にいることを確認して、docker compose upコマンドを実行して下さい。

compose.ymlで指定した内容でコンテナを実行するための処理が始まります。
初期実行時は少し時間がかかりますが、しばらくするとこのような出力が表示されます。


このように訪問者数を表示するページが表示されます。
更新ボタンをクリックしてみて下さい。訪問者数がカウントアップされます。

上記の例では2つのコンテナを同時に実行して、かつコンテナ同士で通信をすることができました。
docker compose upで何が行われたか
それでは、docker compose up
というコマンドで実際に何が行われたのか詳しく見ていきましょう。
Docker Composeは複数のコンテナを連携させて実行してくれると書きました。では、docker compose up
コマンドによって作成されたコンテナを見てみましょう。
未使用のDockerオブジェクトの削除
Lesson内でdocker compose up
によって何が行われたかを分かりやすくするために、docker system prune
というコマンドで、今までの作成したコンテナとイメージなどを全て削除してから、docker compose up
を実行しています。
Docker Desktopのダッシュボード画面を立ち上げて下さい。Containers
からコンテナ一覧を表示します。

lesson5_1という名前のコンテナをクリックするとその詳細がさらに表示されます。
ステータスがRunnning(起動中)になっている2つのコンテナを確認できます。それぞれ「lesson5_1-app-1」と「lesson5_1-redis-server-1」という名前がついています。
また①で示した範囲をご覧下さい。この項目ではコンテナの元になっているイメージが表示されています。
Images
からイメージの一覧を見てみると、「redis」と「lesson5_1-app」という名前でイメージが2つ作成されていることも確認できます。

docker compose upでイメージとコンテナが作成されていた
先ほどのステップ1~4において、今までのLesson4までの内容と比べて新しく行なった主なことは、compose.yml
というファイルの作成とdocker compose up
コマンドだけです。
それにも関わらず、docker compose up
コマンドの実行だけで、今までdockerコマンドで行なってきた色々なイメージとコンテナに対する基本操作が行われていることが分かります。
また、今まではコンテナをそれぞれ個別に実行して、コンテナ同士の通信を行うことができませんでしたが、今回の例ではデータベースとサーバーの2つのコンテナがデータのやり取りを行えていることにも注目して下さい。
コンテナ間の通信
コンテナ間の通信に対する特別な設定は何も行なっていないように見えますが、Docker Composeが自動で行なってくれています。docker compose up
で行う内容を指定するcompose.ymlファイルの詳細についてはchpater2で学習します。
コンテナの停止
docker compose up
で2つのコンテナを実行しましたが、PowerShellの画面がこのようになっており、コンテナが起動したままになっているので停止させましょう。

Ctrl + C
を入力(Ctrl
キーを押下しながら、c
キーを押す)して下さい。

Stopped
と表示され、コンテナが停止します。
ダッシュボード画面でもコンテナが停止されたことが確認してみましょう。

ここでは停止したコンテナが削除されずに残っていることに注意して下さい。
今度はオプションをつけて、コンテナを実行します。docker compose up -d
と入力して下さい。

同じようにコンテナが起動されますが、PowerShellの画面が入力待ちの状態になっていることが異なります。このように-d
のオプションを付けることでバックグラウンドで処理を実行してくれることになります。
今度はdocker container ls
コマンドで起動中のコンテナを見てみましょう。

確かに起動されています。
それでは、docker compose down
コマンドを実行して下さい。

今度は、Removed
と表示されています。ダッシュボード画面を確認しましょう。

コンテナが削除されていることが確認できます。
コンテナを停止する方法は2通りある
このようにdocker compose up
で起動したコンテナを停止させる方法には主に2通りあることが分かります。
最初にコンテナを停止させた方法では、docker compose up
でフォアグラウンドで実行中のものに対してCtrl + C
を入力して停止させました。
この時、コンテナは停止されるだけで削除はされませんでした。
これと同じように、docker compose up -d
でバックグラウンドで実行中のものに対して、コンテナを削除せずに停止するにはdocker compose stop
というコマンドを使用します。

docker composeコマンド
最後に、このchapterで使用したコマンドを整理しておきましょう。
コマンド | 実行する内容 |
---|---|
docker compose up | フォアグラウンドでコンテナを実行する |
docker compose up -d | バックグラウンドでコンテナを実行する |
docker compose stop | 起動中のコンテナを停止させる |
docker compose down | 起動中のコンテナを停止し、削除する |

Lesson 5
Chapter 2
compose.ymlの書き方
このchpaterでは、compose.yml
ファイルの書き方について詳しく学習していきます。
YAMLファイルとは
YAMLとはYAML Ain’t Markup Languageの頭文字をとったもので、dockerでの使用に限定されているものではなく、あらゆるプログラミング言語に対応しておりファイルの設定の記述などに使用されています。
ここでは詳説は行いませんが、ハイフン(-)やセミコロン(:)などを使って内容の構造を指定しており、決まったフォーマットに従って記述することになります。
chapter1で使用したcompose.yml
は以下のようなものでした。
compose.yml
services:
redis-server:
image: 'redis'
app:
build: .
ports:
- '4000:8000'
environment:
PORT: 8000
それでは、上記のcompose.yml
で使用した項目と、この例では使わなかった項目についても詳しく見ていきましょう。
version
初めにversion
についての解説を行います。
例として、version
は以下のように書きます。
compose.yml
version: '3'
version
では指定した値により、compose.yml
ファイルをどのような形式で読み取るのかを指定します。
つまり、compose.yml
ファイルでは他にもいくつかの項目がありますが、それをdocker compse up
コマンドで実行するときにどのように解釈して実行するかの形式をversion
の値で指定しているということです。
なぜversionを設定していないか
しかし、version
はchapter1の例では使用していません。それには理由があります。
docker compose version
を実行してみて下さい。

Docker Composeのバージョンが2になっていることが確認できます。ここでいうバージョンでは上記のversionという項目で指定した値とは違って、docker compose
コマンドで実行するプログラムであるDocker Composeのバージョンのことを指しています。
このDocker Compose V2でのyamlファイルの読み取りの形式はCompose Specというものに準拠しており、versionの項目で形式を指定するのは現在非推奨になっています。
そのため、chpater1の例ではversion
をcompose.yml
で指定していません。
compose.ymlとdocker-compose.yml
Docker Compose V1ではymlファイルの名前にdocker-compse.yml
という名前を使用していましたので、書籍やネットで得られる情報源の中にはそのように書かれているものがあると思います。Docker Compose V2ではcompose.yml
という名前に変更されましたが、後方互換をサポートするためにdocker-compose.yml
という名前でも使用できます。
services
次にservices
項目について見ていきましょう。
ymlファイルは内容の構造の指定を行うと書きましたが、chapter1の例を見てみるとインデントにより情報の構造化がされています。
具体的にはservices:
に続いて2つ分のスペースの後に記述されているredis-server:
とapp:
の2つがありますが、それがservices
の内容に該当します。
services
はその1つが、Docker Composeで実行するコンテナ1つに対応します。この例では、redis-server
とapp
の2つのコンテナを、この名前で作成するということを指定していることになります。
そして、続く項目でそれぞれ、そのコンテナの内容をさらに指定することになります。
以下で、その項目について見ていきましょう。
image
services
で指定したサービス(=コンテナ)に続く項目を見ていきましょう。
compose.yml
services:
redis-server:
image: 'redis'
app:
build: .
ports:
- '4000:8000'
environment:
PORT: 8000
chapter1の例を再掲しましたが、ここではimage: ‘redis’
となっています。これは、その名の通りコンテナを作成する元になるイメージを指定します。
ここではredis
を指定しているので、docker compose up
時に(redisイメージが存在していなければ)Docker Hubからイメージを取得してくることになります。
build
それではapp:
に続く項目を見てください。ここにはimage
項目でイメージを指定していません。しかし、代わりにbuild
という項目があります。
image
では既成のイメージを指定しましたが、build
ではこれから作成するイメージのDockerfileを指定します。build: .
というように書かれており、docker compse up
を実行するときの作業ディレクトリにあるDockerfileを指定しています。
command
chpater1のcompose.yml
ファイルでは書いていませんが、command
ではコンテナ実行時のデフォルトのコマンドを指定することができます。
DockerfileではCMD [“npm”, “install”]
としていますが、それをさらに書き換えて実行することになります。
ports
ports
ではホストOSとコンテナ内のポートの割り当てを指定します。
chapter1の例では’4000:8000’
というように書いていますが、ホスト側:コンテナ側
という順番になっています。
この場合、ホストOS側でポートの4000番にアクセスすると、コンテナ内の8000番に接続することができます。
chapter1の例ではexpressサーバを8000番で起動していたので、http://localhost:4000
アドレスにブラウザにアクセスすることで接続できたというわけになります。
environment
environment
ではコンテナ内で使用する環境変数を追加することができます。
docker compose up
のときに追加しているというだけで、DockerfileのENV
命令と行なっていることは同じです。
[キー名]: [値]
という形式で指定しており、chpater1の例ではPORT
という名前の環境変数の値を8000
に設定しています。
ここで設定した値はindex.jsのprocess.env.PORT
で使用しています。
depends_on
depends_on
ではサービス間の依存関係を指定します。
具体的な例を見てみましょう。
compose.yml
services:
db:
image: mysql
environment:
MYSQL_ROOT_PASSWORD: root
redis-server:
image: 'redis'
app:
build: .
depends_on:
- db
- redis-server
ports:
- '4000:8000'
environment:
PORT: 8000
この例では、app
コンテナはdb
コンテナとredis-server
コンテナに依存していることを設定しています。
これで、サービスの起動の順番を決めることができます。上記の例では、db
コンテナとredis-server
コンテナを起動してからapp
コンテナを起動することになります。
しかし、上記の設定では起動の順番を決めることができても、稼働の順番を確約しているものではないということに注意して下さい。
depends_on
で指定したコンテナを先に起動しても、その実行に時間がかかった場合、後から起動したコンテナの方が先に実行を終える場合があるということです。
これについてはchapter5でTODOアプリサーバーを作成するときに、もう少し詳しく解説します。

Lesson 5
Chapter 3
ネットワークの設定
さて、chapter1の例ではredisとnodeのコンテナがお互いにデータの通信を行なっていますが、よく考えてみると特に設定などを行なっていないことに気づきます。それではネットワークの設定はどのように行われているのでしょうか?
chapter1での例の出力をよく見てみると、①Nework Createdとなっています。

実は、Docker Composeはdocker compose up
時に自動的に1つのネットワークを作成しています。
以下で、どういう処理が行われているのか解説します。
docker compose upで自動的にネットワークが作成される
chapter1のcompose.yml
ファイルを再度確認しながら見てみましょう。
compose.yml
services:
redis-server:
image: 'redis'
app:
build: .
ports:
- '4000:8000'
environment:
PORT: 8000
docker compose up
の実行の裏側では以下のような処理が走ることで、ネットワークの設定が行われています。
処理1
[ディレクトリ名]_default
という名前のネットワークが作成される。(ここではlesson5_1_default
)
処理2
services
以下で指定したredis-server
という名前でコンテナが作成され、lesson5_1_default
にredis-server
という名前で接続される。
処理3
services
以下で指定したapp
という名前でコンテナが作成され、lesson5_1_default
にapp
という名前で接続される。
最終的には図で表すとこのようなネットワークが構築されます。
docker compose upで自動的に作成されたネットワーク
コンテナは隔離された実行環境である
ここで、コンテナの基本を再確認しておきたいと思います。
コンテナは隔離された実行環境でした。そのため、Docker上で構築されたコンテナ間のネットワークにはホストマシン上からはアクセスすることができません。
上記のlesson5_1_default
というネットワークもコンテナ間の通信に利用されるもので、ホストマシン上で利用できるものではないということに注意しておきましょう。
ネットワークで割り当てられた名前を利用する
これまで、chapter1で体験してもらった例のindex.js
の内容は本題とは関係が薄いこともあり解説を行なっていませんでしたが、1つ注意して見てもらいたい点があります。
node側でredisと接続を行うときにはcreateClient
にurl: “redis://[IPアドレス or ホスト名]”
という形式のオブジェクトを渡します。
index.js
const client = createClient({
url: "redis://redis-server"
});
chapter1の例では上記のように、redis-server
というホスト名を入力していますが、これは、docker compose up
時に自動的に作成されたネットワークによって使用可能になっています。
そのため、compose.yml
で指定した名前と異なるものを与えると接続エラーが起きてしまいます。
compose.ymlで指定した名前でネットワークが作成される
Docker Composeは便利で多くのことを自動で行なってくれる分、初心者にとっては何が起こっているのか分からないという点もあります。
ここでは、compose.yml
で指定したサービス名をもとにネットワークが作成され、コンテナ間でその名前を用いて接続できるようになっているということを頭に入れておきましょう。
手動でネットワークを作成する
上記ではDocker Composeにより、自動的にネットワークが作成される様子を確認しました。
次に、手動でネットワークを作成し、compose.yml
に設定を書き込んでネットワークを構築してみましょう。
まずは、Docker Composeにより自動で作成されたネットワークの詳細を見てみましょう。
docker network ls
コマンドで、dockerネットワークの一覧を表示できます。

②lesson5_1_default
という名前でネットワークが作成されています。
docker network inspect lesson5_1_default
で詳しい情報を見てみましょう。

このようになっています。特に③、④で示した範囲で設定されている項目に注目して下さい。
それでは、これと同様に機能するネットワークを手動で作成してみましょう。
ネットワークの作成にはdocker network create
コマンドを利用します。
lesson5_1_default
と同じアドレスで、名前をmy-network
にして作成してみます。
docker network create —driver=bridge —subnet=172.20.0.0/16 —gateway=172.20.0.1 my-network
を実行して下さい。

⑤同じアドレスを使用していて作成ができないというエラーが出ます。
docker network rm lesson5_1_default
で自動で作成されたネットワークを削除しましょう。

再度docker network create
でネットワークの作成に挑戦してみて下さい。

無事、作成されました。
Docker Composeで手動で作成したネットワークを利用する
それでは、このネットワークをDocker Composeで使ってみましょう。
・ステップ1
compose.yml
を以下のように書き換えます。
compose.yml
services:
redis-server:
image: 'redis'
networks:
my-network:
ipv4_address: 172.20.0.10
app:
build: .
ports:
- '4000:8000'
environment:
PORT: 8000
networks:
my-network:
ipv4_address: 172.20.0.20
networks:
my-network:
external: true
以下、変更内容の解説です。
IPアドレスの設定
compose.yml
redis-server:
image: 'redis'
networks:
my-network:
ipv4_address: 172.20.0.10
redis-server
コンテナのIPアドレスを172.20.0.10に設定しています。
compose.yml
app:
(省略)
networks:
my-network:
ipv4_address: 172.20.0.20
app
コンテナのIPアドレスを172.20.0.20に設定しています。
使用するネットワークの指定
compose.yml
networks:
my-network:
external: true
Docker Composeの外部で作成されたネットワークを使用することを示すためexternal: true
と設定しています。
・ステップ2
index.js
も以下のように書き換えてください。
index.js
import express from "express";
import { createClient } from "redis";
const app = express();
const client = createClient({
url: "redis://172.20.0.10"
});
app.get("/", async (req, res) => {
await client.connect();
let counts = await client.get("counts");
if (counts === null) {
counts = 1;
};
res.send(`あなたは${counts}人目の訪問者です`);
await client.set("counts", parseInt(counts) + 1);
await client.disconnect();
});
app.listen(process.env.PORT, () => {
console.log("listening on port", process.env.PORT);
});
以下、変更内容の解説です。
index.js
const client = createClient({
url: "redis://172.20.0.10"
});
ホスト名(redis-server
)を利用していた箇所を、上記のcompose.yml
で設定したIPアドレスに変更します。
これで設定が完了しました。
手動で作成したネットワークが利用できることを確認する
docker compose up --build -d
を実行して下さい。
--buildオプション
--buildオプションをつけなければ、docker compose up
時に既に作成されたイメージを再利用してコンテナを起動します。今回のようにファイルを書き換えた場合は、イメージを再作成しなければならないので、--buildオプションをつけています。

コンテナが起動したら、再びhttp://localhost:4000にアクセスしてみましょう。

ネットワークが正しく設定され、コンテナ間の通信が行えています。
docker compose down時の動作の違い
docker compose down
でコンテナを停止、削除しましょう。

コンテナが削除されましたが、ここでcompose.yml
でnetwork
の設定を行なっていないときとの違いを比べてみて下さい。
compose.yml
でnetwork
の設定を行なっていないときは、Docker Composeがネットワークを作成&管理していたので、docker compose down
時に⑥ネットワークも削除されていました。
Docker Composeがネットワークを自動で作成している場合
しかし、今回はネットワークをDocker Composeの外部に作成しているので、docker compose down
時に削除されません。
docker network ls
でも確認してみましょう。

⑦作成したネットワークが残っていることが確認できます。
まとめ
このchapterでは、Docker Composeが行なっているネットワークの設定の仕組みや、compose.yml
でのnetworks
項目の設定について学習しました。
Docker初学者の内に注意するポイントとしてはcompose.yml
でサービスにつけた名前がホスト名として利用できるという点です。
Docker Composeが便利な分、自動で行なってくれている内容を理解していないと、思わぬところでエラーを発生させてしまうかもしれません。
また、compose.yml
でのnetworks
の設定を行いましたが、特に必要な場面がない限りは設定を行わずDocker Composeに任せた方が便利ですので、今回学習したことを基本にして細かい設定が必要になった時にはご自身で詳細を調べてみて下さい。

Lesson 5
Chapter 4
データの永続化
このchapterではデータの永続化に関する内容を学習します。
Lesson4のchapter1で、コンテナ内のファイルを編集してもホストOS側のファイルには影響がないことを確認したことを覚えているでしょうか。
コンテナは、ホストOSや他のコンテナ間と隔離された実行環境であり、コンテナ内に保存されたデータはLesson4のchapter8で学んだように、コンテナレイヤーと呼ばれる読み書き可能なレイヤーに保存されます。そして、コンテナの削除時にはそのレイヤーと一緒にデータも削除されます。
また、データを隔離せずにコンテナとコンテナ、あるいはコンテナとホストマシンで共有を行いたい場合があります。例をあげると下記のようなケースが該当します。
- Dockerを用いた開発中に、ホストOS側で変更した内容が実行中のコンテナ内でも反映されてほしい
- APIとサーバー間の異なるコンテナ同士で同じデータを共有したい
このような場合にコンテナとホストOS、あるいはコンテナ間でデータを共有する方法をこのchapterで学習していきます。
コンテナの削除でデータも削除されることを確認する
再度、訪問者カウンターの例を用います。
chapter3でネットワークの設定を追加しましたが、ファイルはそのままで問題ありません。
作業ディレクトリに移動して、docker compose up -d
でバックグランドでコンテナを立ち上げます。

http://localhost:4000にアクセスします。
更新ボタンを何度かクリックし、カウントアップして下さい。

次にdocker compose restart
コマンドを実行します。

このコマンドはコンテナを一度停止し、再起動します。

再度http://localhost:4000にアクセスしても、カウントが保持されています。
このようにコンテナを再起動しても、データはコンテナ内に保存されていることが確認できます。
それでは、今度はコンテナを削除して再起動する例を見てみましょう。
docker compose down
でコンテナを削除します。

再び、docker compose up -d
でコンテナを立ち上げ、http://localhost:4000にアクセスします。

訪問者のカウントがリセットされていることが確認できます。
このようにコンテナを削除してしまうと、コンテナ内に保存されたデータも当然一緒に削除されてしまいます。
コンテナの削除と同時にデータも消えてしまう
コンテナの外でデータを保管する
上記の例では、コンテナの削除と同時にコンテナ内のデータも削除されてしまいました。
よって、データを永続的に保持するにはコンテナの外でデータを保管する必要があります。
Dockerではボリュームと呼ばれる、コンテナで利用されるデータをDockerによって管理する仕組みがあります。
訪問者カウンターの例を用いて、ボリュームを利用してみましょう。
Docker Compose
でのボリュームの設定をするには、compose.yml
を編集します。
C:\Users\ユーザ名\Desktop\lesson5_1\compose.yml
を以下のように書き換えて下さい。
compose.yml
services:
redis-server:
image: 'redis'
networks:
my-network:
ipv4_address: 172.20.0.10
volumes:
- redis_data:/data
app:
build: .
ports:
- '4000:8000'
environment:
PORT: 8000
networks:
my-network:
ipv4_address: 172.20.0.20
networks:
my-network:
external: true
volumes:
redis_data:
ボリュームの作成
compose.yml
volumes:
redis_data:
ボリュームを作成し、その名前をredis_data
に設定しています。これで、このcompse.yml
によって実行されるコンテナ内でredis_data
という名前のボリュームを利用することができるようになります。
コンテナで使用するボリュームを割り当てる
compose.yml
redis-server:
(省略)
volumes:
- redis_data:/data
redis-server
コンテナで利用するボリュームとコンテナ内のファイルシステムの接続を行なっています。
[ボリューム名]:[コンテナ内のパス]
という形式で設定を行います。
今回、使用しているredis
イメージではデータが/data
に保存される為、このように設定を行なっています。
データの永続化ができているか確認する
それでは、適切にボリュームの設定ができたか確かめてみましょう。
docker compose up —build -d
を実行して下さい。

--buildオプション
—buildオプションを付けているのは、設定を書き換えた為、イメージの再作成を行う必要がある為です。
更新ボタンを何回かクリックして、カウントアップします。

docker compose down
でコンテナの停止と同時に削除を行いましょう。

再び、docker compose up -d
でコンテナを立ち上げます。

http://localhost:4000にアクセスします。

コンテナを削除したにも関わらず、カウントが保持されていることが確認できました。
コンテナの外にデータを保存
ボリュームの実体
上記ではボリュームを用いることで、コンテナの外でDockerによりデータを管理できることを確認しました。
しかし、ボリュームはdocker compose up
時にDocker Composeによって自動的に作成されており、その正体が不確かなものに見えると思います。
ここではその正体を具体的に見ていきましょう。特に、ボリュームの具体的な保存先はどこになっているのでしょうか?
docker volume ls
コマンドを実行して下さい。このコマンドではボリュームの一覧を表示することができます。

lesson5_1_redis_data
という名前のボリュームが作成されていることが確認できます。
docker volume inspect lesson5_1_redis_data
を実行します。
ボリュームの詳しい情報を見ることができます。①”Mountpoint”
を確認して下さい。ここがボリュームの具体的な保存先になります。パスの始まりが/var/lib/docker/
になっています。
このように、ボリュームはDockerによって管理されていることが確認できました。
ボリュームの手動作成
先ほどの例では、docker compose up
時に自動的にボリュームが作成されましたが、dockerコマンドでもボリュームを作成することができます。
docker volume create my-volume
を実行して下さい。

docker volume ls
コマンドで一覧を表示すると、確かに作成されていることが確認できます。

それでは、この手動で作成したボリュームを訪問者カウンターの例で使用してみることにしましょう。
C:\Users\ユーザ名\Desktop\lesson5_1\compose.yml
を以下のように書き換えてください。
compose.yml
services:
redis-server:
image: 'redis'
networks:
my-network:
ipv4_address: 172.20.0.10
volumes:
- my-volume:/data
app:
build: .
ports:
- '4000:8000'
environment:
PORT: 8000
networks:
my-network:
ipv4_address: 172.20.0.20
networks:
my-network:
external: true
volumes:
my-volume:
external: true
外部ボリュームの利用
compose.yml
volumes:
my-volume:
external: true
Docker Composeの外部で作成されたボリュームを使用していることを示すためexternal: true
としています。
コンテナで使用するボリュームを割り当てる
compose.yml
redis-server:
(省略)
volumes:
- my-volume:/data
ボリューム名をredis_data
から、先ほど作成したmy-volume
に変更しています。
データの永続化ができているか確認する
docker compose up --build -d
でコンテナを立ち上げましょう。

http://localhost:4000にアクセスします。

訪問者のカウントがリセットされています。
カウントアップして、データを変更してみます。

そして、繰り返しになりますが、同じことをしてみましょう。docker compose down
でコンテナを停止&削除し、docker compose up -d
で再び立ち上げます。

再度、http://localhost:4000にアクセスします。

手動で作成したボリュームを利用して、データを永続化できていることが確認できました。
コンテナとホストマシン上のデータを共有する
さて、これまで確認した例では、ボリュームの利用によりDockerによって管理されたファイルシステムをデータの保存先として使用していました。
しかし、Dockerにはその他にもコンテナで使用するデータを指定するバインドマウントと呼ばれる方法があります。
バインドマウントを使用すれば、ホストマシン上の特定のファイルやディレクトリをコンテナ上で扱えるようになります。
Docker Composeでバインドマウントを利用する
・ステップ1
C:\Users\ユーザ名\Desktop\lesson5_1\
内にcache
という名前で新しいディレクトリを作成して下さい。
この例ではこれをコンテナとの共有ディレクトリとして使用します。
・ステップ2
C:\Users\ユーザ名\Desktop\lesson5_1\compose.yml
を以下のように書き換えてください。
compose.yml
services:
redis-server:
image: 'redis'
networks:
my-network:
ipv4_address: 172.20.0.10
volumes:
- ./cache:/data
app:
build: .
ports:
- '4000:8000'
environment:
PORT: 8000
networks:
my-network:
ipv4_address: 172.20.0.20
networks:
my-network:
external: true
パスの指定
compose.yml
redis-server:
(省略)
volumes:
- ./cache:/data
[ホストマシン上の相対パス]:[コンテナ内のパス]
という形式でパスを指定します。
相対パスはcompose.yaml
ファイルからの位置になります。
バインドマウントが設定できているか確認する
準備ができたので、docker compose up --build -d
でコンテナを立ち上げましょう。

http://localhost:4000にアクセスします。

訪問者のカウントがリセットされています。
カウントアップして、データを変更してみます。

また、同じことをします。docker compose down
でコンテナを停止&削除し、docker compose up -d
で再び立ち上げます。

再度、http://localhost:4000にアクセスします。

コンテナを削除しても、データがホストマシン上に保存されているのでカウントが保持されています。
バインドマウント
そして、ホストマシン上のデータを確認してみると、C:\Users\ユーザ名\Desktop\lesson5_1\cache\dump.rdb
というファイルが新しく作成されていることが確認できます。

コンテナ内ではこのホストマシン上のデータを利用しているため、コンテナを削除してもデータを永続化することができています。
試しにこのデータを削除してみましょう。
そして再び、docker compose down
でコンテナを停止&削除し、docker compose up -d
で立ち上げ、http://localhost:4000にアクセスします。

データが削除されたので、カウントもリセットされています。
ボリュームとバインドマウントのどちらを使うか
これまでで、ボリュームとバインドマウントを利用しましたが、その違いを整理しておきましょう。
ボリューム
ボリュームを利用する場合、新しいディレクトリがホストマシン上のDockerが管理するディレクトリ内に作成されます。そして、その内容はDockerによって直接管理されます。
一度作成されたボリュームは、例で起動したコンテナ以外でも簡単に利用することができます。
バインドマウント
バインドマウントを利用する場合は、ホストマシン上の指定したディレクトリをコンテナ内に接続します。(このことをマウントすると言います)
このデータを他のコンテナでも利用しようと考えた場合、パスの指定をしなければなりません。ボリュームと比較して、再利用性に難があることが分かります。
セキュリティの問題
バインドマウントでは、ホストマシン上のディレクトリとコンテナをマウントします。そのため、コンテナ内からホストマシン上のファイルシステムを操作することが可能になります。これによって、コンテナで実行している内容とは関係のないホストマシン上のデータの操作も許すことになり、セキュリティ上の問題が発生します。
一方、ボリュームはDockerによって管理されているので、ホストマシン上のファイルシステムを操作することはできません。
まとめ
総合的な観点からみて、バインドマウントはボリュームよりも古い機能であることもあり、比較的機能が限定されています。
上記であげた点以外にも、ボリュームはバインドマウントよりもバックアップやデータの移行が簡単であることや、基本的な性能が高い点などがあげられます。
Docker公式のリファレンスでも、できるだけボリュームを使用することが推奨されているので、特に理由がなければボリュームの利用を考えるようにしましょう。

Lesson 5
Chapter 5
TODOアプリを作成する
このchapterでは、Docker Composeを用いて、TODOアプリ(APIサーバー)の作成を行います。
いままで学習した内容を活用して作成しますので、分からないところは適宜、前のchapterに戻って復習してみて下さい。
内容は学習例として必要最低限のものにしており、以下のようなリクエストを受け付けるようになっています。
メソッド | URL | リクエストボディー | 内容 |
---|---|---|---|
GET | / | Welcome to Todo Appメッセージの表示 | |
GET | /todos | TODO一覧 | |
POST | /todos | title: タイトル名 | TODO追加 |
GET | /todos/:id | ID指定でTODO取得 | |
DELETE | /todos/:id | ID指定でTODO削除 |
構築するアプリについて
このTODOアプリは次の2つのコンテナで構成されています。
appコンテナ
Expressフレームワークを用いたAPIサーバーです。
node:aplineイメージを使用して構築します。
dbコンテナ
MySQLイメージを使用したデータベースです。
最低限の設定のみを行なっています。
chpater5で作成するTODOアプリ
ファイルの準備
・ステップ1
これまでと同様に作業ディレクトリを作成します。
ここでは、仮にC:\Users\ユーザ名\Desktop\lesson5_5
としましょう。
初めにAPIサーバーに必要なファイルであるindex.js
とpackage.json
を用意します。作業ディレクトリ内に以下の内容で作成してください。
index.js
import express from "express";
import { Sequelize, DataTypes } from "sequelize";
const app = express();
app.use(express.json());
const sequelize = new Sequelize('my_sql_db', 'root', 'root', {
host: 'db',
dialect: 'mysql',
});
try {
await sequelize.authenticate();
console.log("接続されました")
} catch (error) {
console.error("接続エラーが発生しました", error);
}
const Todo = sequelize.define("Todo", {
title: {
type: DataTypes.STRING
}
});
await sequelize.sync()
.then(() => {
console.log("同期されました");
})
.catch((err) => {
console.log("同期エラーが発生しました" + err.message);
});
app.get("/", async (req, res) => {
res.send("Welcom to Todo App");
});
app.get("/todos", (req, res) => {
Todo.findAll()
.then(data => {
res.send(data)
});
});
app.post("/todos", (req, res) => {
Todo.create({ title: req.body.title })
.then(data => {
res.send(data);
});
});
app.get("/todos/:id", (req, res) => {
Todo.findOne({
where: {
id: req.params.id
}
})
.then(data => {
res.send(data);
});
});
app.delete("/todos/:id", async (req, res) => {
const todo = await Todo.findOne({
where: {
id: req.params.id
},
});
if (todo) {
await todo.destroy();
return res.send(`id: ${req.params.id}のデータを削除しました`);
}
res.send(`id: ${req.params.id}のデータが見つかりませんでした`);
});
app.listen(process.env.PORT, () => {
console.log("listening on port", process.env.PORT);
});
package.json
{
"name": "lesson5_5",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"start": "node index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.18.2",
"mysql2": "^3.0.1",
"sequelize": "^6.28.0"
}
}
本題とは関係がないので中身の詳細についての解説は行いませんが、データベースとの接続に関する部分のみ後で触れます。
また、コードを最小限にする為、実践的には必要なエラーハンドリングなどを省略していますのでご留意ください。
・ステップ2
次にappコンテナの元になるイメージを構築するためにDockerfileを作成していきましょう。
内容としてはこのLessonのchapter1で用意したものと同じなりますが、ここでは最初からひとつずつ見ていきましょう。
ベースイメージの指定
Dockerfile
FROM node:alpine
今回はAPIサーバーにExpressフレームワークを使用するので、ベースイメージはnode
を使用します。その中でも軽量なnode:alpilne
イメージを選択しています。
npmパッケージのインストール
node
イメージには既にNode.jsとNPMがインストールされているので、アプリの構築に必要な次の作業は、package.json
の用意とnpmを使った依存関係のインストールになります。
Dockerfile
WORKDIR '/app'
COPY package.json .
RUN npm install
指定した作業ディレクトリに、ホストマシン上に用意したpackage.json
ファイルをコピーします。
後はnpm install
を実行してやることで、パッケージのインストールが完了し、アプリを実行する準備が整います。
ソースコードをコンテナ内に準備
Dockerfile
COPY index.js .
COPY
命令でホストマシン上のindex.js
をコンテナ内にコピーします。
アプリの実行
Dockerfile
CMD ["npm", "start"]
CMD
命令でアプリを実行するためのコマンドを指定します。
作業ディレクトリ内に作成するDockerfileは、最終的に以下のような内容になります。
Dockerfile
FROM node:alpine
WORKDIR '/app'
COPY package.json .
RUN npm install
COPY index.js .
CMD ["npm", "start"]
・ステップ3
以上の手順で、appコンテナの準備が完了しました。dbコンテナについてはcompose.yml
ファイル内で定義していきます。
イメージの指定
compose.yml
services:
db:
image: mysql
イメージにはmysql
を指定します。
環境変数の設定
compose.yml
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: my_sql_db
MYSQL_ROOT_PASSWORD
はmysqlのルートユーザーのパスワードになります。mysql
イメージの利用にあたり、唯一設定が必須になる環境変数で,
これ以外の環境変数は設定しなくても動作するようになっています。
MYSQL_DATABASE
は、作成するデータベースの名前になります。設定しておくと、イメージの作成時に自動で指定した名前でデータベースを作成してくれます。今回はNode側のindex.js
でここで作成したデータベース名を使用するので設定しています。
ボリュームの設定
compose.yml
volumes:
- mysql-db-volume:/var/lib/mysql
(省略)
volumes:
mysql-db-volume:
mysql-db-volume
という名前でボリュームを設定し、それをコンテナ内の/var/lib/mysql
にマウントします。
mysqlイメージでは/var/lib/mysql
がデータの保存場所になっているので、そこを指定しています。
これでdbコンテナの準備は完了です。
appコンテナの依存関係の指定
compose.yml
app:
build: .
depends_on:
- db
ports:
- '4000:8000'
environment:
PORT: 8000
appコンテナについてはDockerfile
ファイルで指定したイメージを使うので、build
でDockerfile
のパスを指定しています。
また、depends_on
で依存関係を指定することでdbコンテナを起動してから、appコンテナを起動するようにしています。
こうすることでappコンテナからデータベースに接続するときにdbコンテナが起動しておらず接続エラーとなることを防いでいます。
ports
とenvironment
に関しては訪問者カウンターの例と同様に設定しています。
復習ですが、ports
でホストマシン側のポートを4000番に設定しているので、同じようにlocalhost:4000にアクセスすることで、アプリに接続できます。
最終的には、以下の内容でcompose.yml
ファイルを作成して下さい。
compose.yml
services:
db:
image: mysql
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: my_sql_db
volumes:
- mysql-db-volume:/var/lib/mysql
app:
build: .
depends_on:
- db
ports:
- '4000:8000'
environment:
PORT: 8000
volumes:
mysql-db-volume:
これでC:\Users\ユーザ名\Desktop\lesson5_5
内にindex.js
、package.json
、Dockerfile
、compose.yml
の4つのファイルを準備できました。
Docker Composeでアプリを起動する
それでは、docker compose up
でコンテナを起動してみましょう。

このような出力が出れば、無事に起動できています。
http://localhost:4000にアクセスしてみましょう。

Welcome to Todo App
が表示されればホストマシンからもコンテナにアクセスできています。
無事、起動に成功できれば良いですが、場合によってはデータベースとの接続のところでエラーが発生してしまう場合があります。
原因としてはindex.js
でmysqlに接続する際、データベースが稼働しておらず接続エラーになるという内容です。
アプリの実行に失敗する場合
chapter2ではcompose.yml
の書き方について学習しましたが、depend_on
についての解説の際、コンテナの起動の順番を決めることはできても、稼働を確約できないと説明したことを思い出して下さい。
このchapter5で用意したcompose.yml
ではdbコンテナ -> appコンテナ
という順番でコンテナを起動するように設定を行えています。
しかし、例えばdbコンテナが起動してから実際にmysqlのデータベースが動作するまで時間がかかった場合や、そもそもデータベースの起動に失敗している場合は、appコンテナで接続エラーが起きてしまいます。
ここでは、コンテナの起動の順番に限らず、dbコンテナ内でデータベースが稼働しているかを確認してからappコンテナを起動するように設定を見直してみましょう。
コンテナ内の稼働状況を制御する
まず、appコンテナの設定に関しては、depends_on
を以下のように書きます。
compose.yml
app:
build: .
depends_on:
db:
condition: service_healthy
新しくcondition
という項目を追加しています。
condition
には次のいずれかの値を設定します。
(1)service_started
: 依存サービスが起動されるまで待機する
起動する順番を決めるものなので、condition
項目を書かないのと同じです。
(2)service_healthy
: 依存サービスのヘルスチェックが完了するまで待機する
依存サービスで何らかの処理が完了するまで、待機します。どのような処理を行うかは依存サービス内のhealthcheck
という項目で設定します。
(3)service_completed_successfully
: 依存サービスが正常終了するまで待機する
依存サービスで何らかの処理を行い、コンテナが終了するまで待機します。前処理を行うためのサービスを定義するときに使われます。
ここでは、condition: service_healthy
としているので、dbコンテナ側に、実行するヘルスチェック処理を書いていきます。
dbコンテナ側でヘルスチェックコマンドを設定する
compose.yml
db:
(省略)
healthcheck:
test: "mysql --password=root --execute 'show databases;'"
interval: 3s
timeout: 30s
retries: 5
start_period: 0s
healthcheck
の項目はそれぞれ以下のような内容で設定します。
・test
: ヘルスチェックを行うコマンド
・interaval
: チェックの間隔
・timeout
: test
で指定したコマンドのタイムアウト時間
・retries
: リトライ回数
・start_period
: コンテナ起動直後からヘルスチェックコマンドを実行するまでの時間
ここでは、ヘルスチェックコマンドをmysqlに接続してデータベースの一覧を表示するコマンドに設定し、このコマンドの実行に成功できれば、データベースを使える準備ができていると判定しています。
compose.yml
を最終的に以下のように作成します。
compose.yml
services:
db:
image: mysql
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: my_sql_db
volumes:
- mysql-db-volume:/var/lib/mysql
healthcheck:
test: "mysql --password=root --execute 'show databases;'"
interval: 3s
timeout: 30s
retries: 5
start_period: 0s
app:
build: .
depends_on:
db:
condition: service_healthy
ports:
- '4000:8000'
environment:
PORT: 8000
volumes:
mysql-db-volume:
docker compose —build -d
でコンテナを立ち上げましょう。
これで全ての準備が整いました。以降では実際にリクエストを送信してアプリが動作するか確認していきましょう。
curlでリクエスト送信
リクエストの送信にはWindows 10から標準で搭載されるようになったcurl
を使用します。
最初にTODOの一覧をGETリクエストで取得してみましょう。
curl.exe ”http://localhost:4000/todos”
を実行して下さい。

データが空であることが確認できます。それでは、POSTリクエストを行なってデータを追加してみましょう。
curl.exe -X POST -H “Content-type: application/json” -d ‘{¥”title¥”: ¥"Learn Docker¥”}’ “http://localhost:4000/todos”
を実行して下さい。

TODOにデータが追加されました。
idを指定してデータを取得してみましょう。curl.exe “http://localhost:4000/todos/1”
を実行します。

先ほど作成したTODOを取得できました。
次に、DELETEリクエストで作成したTODOを削除します。curl.exe -X DELETE “http://localhost:4000/todos/1”
を実行して下さい。

データを削除することができました。
再び、TODOの一覧を取得してみると、空になっています。

最後に次のステップで利用するデータを準備します。POSTリクエストで好きなデータを追加してみて下さい。

この例ではLearn Docker
、Learn SQL
、Learn Node
というtitleで3つデータを追加しています。
コンテナの中に入ってみる
先ほどは起動したコンテナにホストマシンからcurl
でリクエストを行なってデータを操作しました。
今度は起動しているコンテナの中に入ってデータベースを操作してみましょう。
Docker Composeが実行しているコンテナ内でコマンドを実行するにはdocker compose exec
コマンドを使用します。
docker compose exec [サービス名] [コンテナ内で実行するコマンド]
という形式で使います。
今回はdbサービスの中でデータベースに接続したいので、docker compose exec db mysql —pasword=root
を実行して下さい。

mysqlに接続することができました。
データベースの一覧を表示してみましょう。show databases;
を入力します。

アプリで使用しているmy_sql_db
データベースが確認できます。
my_sql_db
内でいくつかクエリを実行してみましょう。まずは使用するデータベースをuse my_sql_db
で指定します。

次に、show tables;
でテーブルの一覧を取得してみましょう。

Node側で作成していたTodosテーブルが確認できます。
先ほど、コンテナの外からcurl
で追加したデータを取得してみましょう。
select * from Todos;
を入力して下さい。

Todosテーブルのデータの一覧を表示することができます。
このようにdocker compose exec
コマンドでコンテナの中に入って、クエリを叩くことでmysqlを直接操作できることを確認しました。
本LessonはMySQLを対象にしていないのでこれ以上は触れませんが、その他にも様々なクエリを実行できることを確かめてみて下さい。
Dockerを利用している恩恵として、操作に失敗してもコンテナやボリュームを再作成すれば、また1から試行することができます。
まとめ
このchapterではLessonのまとめとして、Docker ComposeでAPIとDBの2つのコンテナを用いてTODOアプリを作成しました。
また、Lesson5を通して、Docker Composeによって複数のコンテナを定義して実行する方法を学習しましたが、最初の内は実現したいことに対する設定ファイルの用意の仕方などが分からなくなったりすることもあるかと思います。
理解してしまえば難しいことはないのですが、特にDockerfile
とcompose.yml
の違いについては以下のように明確に区別しておいて下さい。
ファイル | 指定する内容 |
---|---|
Dockerfile | どのようなイメージを作成するか |
compose.yml | どのようにコンテナを起動するか |
そして設定について分からない点があれば、Dockerfile
についてはLesson4を、compose.yml
についてはLesson5を遡って復習してみて下さい。
ただ設定の項目については全てを網羅しているわけではありません。Lesson4と5で学習した内容を基本に、新たに行いたいことがあれば各自で調べてみて下さい。
日本語で読める公式のリファレンスとして、Dockerfileと、docker composeがあります。
