Lesson 5

複数のコンテナを同時に実行する

Lesson 5 Chapter 1
Docker Composeを体験する

これまでのLessonでは、docker runコマンドを用いてコンテナを実行する方法を学習しました。

Lesson3とLesson4では、それぞれ以下の方法でコンテナを実行しています。

  • Lesson3: DockerHubから取得したイメージからコンテナを実行
  • Lesson4: Dockfileを用いて作成したイメージからコンテナを実行

しかし、複数のコンテナ間の通信やお互いの関係などを制御して、まとめて実行することができませんでした。

image_5_1_1.png 複数のコンテナをまとめて実行?

Docker Composeとは

そこで登場するのがDocker Composeになります。

Docker Composeはcompose.ymlというファイルを使って設定を行うことで、複数のコンテナをどう連携して実行するかを定義することができます。

image_5_1_2.png Lesson5で学習する内容

このchapterでは、詳しい解説を後回しにして、まずはDocker Composeを体験してもらいます。設定ファイルについての詳細は次のchapterで学習します。

Docker Composeで複数のコンテナを実行する

それでは、実際にDocker Composeを用いて複数のコンテナを実行してみましょう。

例として、サイトの訪問者をカウントするサーバーを作成します。必要なファイルを用意しましたので、以下の手順を実行して下さい。

・ステップ1

まずは作業ディレクトリを作成して下さい。

ここでは、仮にC:\Users\ユーザ名\Desktop\lesson5_1としましょう。package.jsonindex.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.jsonindex.jsDockerfilecompose.ymlの4つのファイルを準備しました。

作業ディレクトリ内にいることを確認して、docker compose upコマンドを実行して下さい。

image_5_1_3.png

compose.ymlで指定した内容でコンテナを実行するための処理が始まります。

初期実行時は少し時間がかかりますが、しばらくするとこのような出力が表示されます。

image_5_1_4.png

そうしたら、ブラウザを起動してhttp://localhost:4000にアクセスして下さい。 image_5_1_5.png

このように訪問者数を表示するページが表示されます。

更新ボタンをクリックしてみて下さい。訪問者数がカウントアップされます。

image_5_1_6.png

上記の例では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からコンテナ一覧を表示します。

image_5_1_7.png

lesson5_1という名前のコンテナをクリックするとその詳細がさらに表示されます。

ステータスがRunnning(起動中)になっている2つのコンテナを確認できます。それぞれ「lesson5_1-app-1」と「lesson5_1-redis-server-1」という名前がついています。

また①で示した範囲をご覧下さい。この項目ではコンテナの元になっているイメージが表示されています。

Imagesからイメージの一覧を見てみると、「redis」と「lesson5_1-app」という名前でイメージが2つ作成されていることも確認できます。

image_5_1_8.png

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の画面がこのようになっており、コンテナが起動したままになっているので停止させましょう。

image_5_1_4.png

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

image_5_1_9.png

Stoppedと表示され、コンテナが停止します。

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

image_5_1_10.png

ここでは停止したコンテナが削除されずに残っていることに注意して下さい。

今度はオプションをつけて、コンテナを実行します。docker compose up -dと入力して下さい。

image_5_1_11.png

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

今度はdocker container lsコマンドで起動中のコンテナを見てみましょう。

image_5_1_12.png

確かに起動されています。

それでは、docker compose downコマンドを実行して下さい。

image_5_1_13.png

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

image_5_1_14.png

コンテナが削除されていることが確認できます。

コンテナを停止する方法は2通りある

このようにdocker compose upで起動したコンテナを停止させる方法には主に2通りあることが分かります。

最初にコンテナを停止させた方法では、docker compose upでフォアグラウンドで実行中のものに対してCtrl + Cを入力して停止させました。

この時、コンテナは停止されるだけで削除はされませんでした。

これと同じように、docker compose up -dでバックグラウンドで実行中のものに対して、コンテナを削除せずに停止するにはdocker compose stopというコマンドを使用します。

image_5_1_15.png

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を実行してみて下さい。

image_5_2_1.png

Docker Composeのバージョンが2になっていることが確認できます。ここでいうバージョンでは上記のversionという項目で指定した値とは違って、docker composeコマンドで実行するプログラムであるDocker Composeのバージョンのことを指しています。

このDocker Compose V2でのyamlファイルの読み取りの形式はCompose Specというものに準拠しており、versionの項目で形式を指定するのは現在非推奨になっています。

そのため、chpater1の例ではversioncompose.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-serverappの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となっています。

image_5_3_1.png

実は、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_defaultredis-serverという名前で接続される。

処理3

services以下で指定したappという名前でコンテナが作成され、lesson5_1_defaultappという名前で接続される。

最終的には図で表すとこのようなネットワークが構築されます。

image_5_3_2.png docker compose upで自動的に作成されたネットワーク

コンテナは隔離された実行環境である

ここで、コンテナの基本を再確認しておきたいと思います。

コンテナは隔離された実行環境でした。そのため、Docker上で構築されたコンテナ間のネットワークにはホストマシン上からはアクセスすることができません。

上記のlesson5_1_defaultというネットワークもコンテナ間の通信に利用されるもので、ホストマシン上で利用できるものではないということに注意しておきましょう。

ネットワークで割り当てられた名前を利用する

これまで、chapter1で体験してもらった例のindex.jsの内容は本題とは関係が薄いこともあり解説を行なっていませんでしたが、1つ注意して見てもらいたい点があります。

node側でredisと接続を行うときにはcreateClienturl: “redis://[IPアドレス or ホスト名]”という形式のオブジェクトを渡します。

index.js
const client = createClient({
  url: "redis://redis-server"
});
                    

chapter1の例では上記のように、redis-serverというホスト名を入力していますが、これは、docker compose up時に自動的に作成されたネットワークによって使用可能になっています。

そのため、compose.ymlで指定した名前と異なるものを与えると接続エラーが起きてしまいます。

image_5_3_3.png compose.ymlで指定した名前でネットワークが作成される

Docker Composeは便利で多くのことを自動で行なってくれる分、初心者にとっては何が起こっているのか分からないという点もあります。

ここでは、compose.ymlで指定したサービス名をもとにネットワークが作成され、コンテナ間でその名前を用いて接続できるようになっているということを頭に入れておきましょう。

手動でネットワークを作成する

上記ではDocker Composeにより、自動的にネットワークが作成される様子を確認しました。

次に、手動でネットワークを作成し、compose.ymlに設定を書き込んでネットワークを構築してみましょう。

まずは、Docker Composeにより自動で作成されたネットワークの詳細を見てみましょう。

docker network lsコマンドで、dockerネットワークの一覧を表示できます。

image_5_3_4.png

lesson5_1_defaultという名前でネットワークが作成されています。

docker network inspect lesson5_1_defaultで詳しい情報を見てみましょう。

image_5_3_5.png

このようになっています。特に③、④で示した範囲で設定されている項目に注目して下さい。

それでは、これと同様に機能するネットワークを手動で作成してみましょう。

ネットワークの作成には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を実行して下さい。

image_5_3_6.png

⑤同じアドレスを使用していて作成ができないというエラーが出ます。

docker network rm lesson5_1_defaultで自動で作成されたネットワークを削除しましょう。

image_5_3_7.png

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

image_5_3_8.png

無事、作成されました。

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オプションをつけています。

image_5_3_9.png

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

image_5_1_3.png

ネットワークが正しく設定され、コンテナ間の通信が行えています。

docker compose down時の動作の違い

docker compose downでコンテナを停止、削除しましょう。

image_5_3_10.png

コンテナが削除されましたが、ここでcompose.ymlnetworkの設定を行なっていないときとの違いを比べてみて下さい。

compose.ymlnetworkの設定を行なっていないときは、Docker Composeがネットワークを作成&管理していたので、docker compose down時に⑥ネットワークも削除されていました。

image_5_3_11.png Docker Composeがネットワークを自動で作成している場合

しかし、今回はネットワークをDocker Composeの外部に作成しているので、docker compose down時に削除されません。

docker network lsでも確認してみましょう。

image_5_3_11.png

⑦作成したネットワークが残っていることが確認できます。

まとめ

この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で学んだように、コンテナレイヤーと呼ばれる読み書き可能なレイヤーに保存されます。そして、コンテナの削除時にはそのレイヤーと一緒にデータも削除されます。

また、データを隔離せずにコンテナとコンテナ、あるいはコンテナとホストマシンで共有を行いたい場合があります。例をあげると下記のようなケースが該当します。

  1. Dockerを用いた開発中に、ホストOS側で変更した内容が実行中のコンテナ内でも反映されてほしい
  2. APIとサーバー間の異なるコンテナ同士で同じデータを共有したい

このような場合にコンテナとホストOS、あるいはコンテナ間でデータを共有する方法をこのchapterで学習していきます。

コンテナの削除でデータも削除されることを確認する

再度、訪問者カウンターの例を用います。

chapter3でネットワークの設定を追加しましたが、ファイルはそのままで問題ありません。

作業ディレクトリに移動して、docker compose up -dでバックグランドでコンテナを立ち上げます。

image_5_4_1.png

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

更新ボタンを何度かクリックし、カウントアップして下さい。

image_5_4_2.png

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

image_5_4_3.png

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

image_5_4_2.png

再度http://localhost:4000にアクセスしても、カウントが保持されています。

このようにコンテナを再起動しても、データはコンテナ内に保存されていることが確認できます。

それでは、今度はコンテナを削除して再起動する例を見てみましょう。

docker compose downでコンテナを削除します。

image_5_4_4.png

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

image_5_4_5.png

訪問者のカウントがリセットされていることが確認できます。

このようにコンテナを削除してしまうと、コンテナ内に保存されたデータも当然一緒に削除されてしまいます。

image_5_4_6.png コンテナの削除と同時にデータも消えてしまう

コンテナの外でデータを保管する

上記の例では、コンテナの削除と同時にコンテナ内のデータも削除されてしまいました。

よって、データを永続的に保持するにはコンテナの外でデータを保管する必要があります。

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を実行して下さい。

image_5_4_7.png

--buildオプション

—buildオプションを付けているのは、設定を書き換えた為、イメージの再作成を行う必要がある為です。

更新ボタンを何回かクリックして、カウントアップします。

image_5_4_2.png

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

image_5_4_8.png

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

image_5_4_9.png

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

image_5_4_10.png

コンテナを削除したにも関わらず、カウントが保持されていることが確認できました。

image_5_4_11.png コンテナの外にデータを保存

ボリュームの実体

上記ではボリュームを用いることで、コンテナの外でDockerによりデータを管理できることを確認しました。

しかし、ボリュームはdocker compose up時にDocker Composeによって自動的に作成されており、その正体が不確かなものに見えると思います。

ここではその正体を具体的に見ていきましょう。特に、ボリュームの具体的な保存先はどこになっているのでしょうか?

docker volume lsコマンドを実行して下さい。このコマンドではボリュームの一覧を表示することができます。

image_5_4_12.png

lesson5_1_redis_dataという名前のボリュームが作成されていることが確認できます。

docker volume inspect lesson5_1_redis_dataを実行します。 image_5_4_13.png

ボリュームの詳しい情報を見ることができます。①”Mountpoint”を確認して下さい。ここがボリュームの具体的な保存先になります。パスの始まりが/var/lib/docker/になっています。

このように、ボリュームはDockerによって管理されていることが確認できました。

ボリュームの手動作成

先ほどの例では、docker compose up時に自動的にボリュームが作成されましたが、dockerコマンドでもボリュームを作成することができます。

docker volume create my-volumeを実行して下さい。

image_5_4_14.png

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

image_5_4_15.png

それでは、この手動で作成したボリュームを訪問者カウンターの例で使用してみることにしましょう。

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でコンテナを立ち上げましょう。

image_5_4_16.png

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

image_5_4_5.png

訪問者のカウントがリセットされています。

カウントアップして、データを変更してみます。

image_5_4_10.png

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

image_5_4_17.png

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

image_5_4_10.png

手動で作成したボリュームを利用して、データを永続化できていることが確認できました。

コンテナとホストマシン上のデータを共有する

さて、これまで確認した例では、ボリュームの利用により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でコンテナを立ち上げましょう。

image_5_4_18.png

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

image_5_4_5.png

訪問者のカウントがリセットされています。

カウントアップして、データを変更してみます。

image_5_4_10.png

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

image5_4_20.png

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

image_5_4_10.png

コンテナを削除しても、データがホストマシン上に保存されているのでカウントが保持されています。

image_5_4_19.png バインドマウント

そして、ホストマシン上のデータを確認してみると、C:\Users\ユーザ名\Desktop\lesson5_1\cache\dump.rdbというファイルが新しく作成されていることが確認できます。

image_5_4_21.png

コンテナ内ではこのホストマシン上のデータを利用しているため、コンテナを削除してもデータを永続化することができています。

試しにこのデータを削除してみましょう。

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

image_5_4_5.png

データが削除されたので、カウントもリセットされています。

ボリュームとバインドマウントのどちらを使うか

これまでで、ボリュームとバインドマウントを利用しましたが、その違いを整理しておきましょう。

ボリューム

ボリュームを利用する場合、新しいディレクトリがホストマシン上の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イメージを使用したデータベースです。

最低限の設定のみを行なっています。

image_5_5_1.png chpater5で作成するTODOアプリ

ファイルの準備

・ステップ1

これまでと同様に作業ディレクトリを作成します。

ここでは、仮にC:\Users\ユーザ名\Desktop\lesson5_5としましょう。

初めにAPIサーバーに必要なファイルであるindex.jspackage.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ファイルで指定したイメージを使うので、buildDockerfileのパスを指定しています。

また、depends_onで依存関係を指定することでdbコンテナを起動してから、appコンテナを起動するようにしています。

こうすることでappコンテナからデータベースに接続するときにdbコンテナが起動しておらず接続エラーとなることを防いでいます。

portsenvironmentに関しては訪問者カウンターの例と同様に設定しています。

復習ですが、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.jspackage.jsonDockerfilecompose.ymlの4つのファイルを準備できました。

Docker Composeでアプリを起動する

それでは、docker compose upでコンテナを起動してみましょう。

image_5_5_2.png

このような出力が出れば、無事に起動できています。

http://localhost:4000にアクセスしてみましょう。

image_5_5_3.png

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”を実行して下さい。

image_5_5_4.png

データが空であることが確認できます。それでは、POSTリクエストを行なってデータを追加してみましょう。

curl.exe -X POST -H “Content-type: application/json” -d ‘{¥”title¥”: ¥"Learn Docker¥”}’ “http://localhost:4000/todos”を実行して下さい。

image_5_5_5.png

TODOにデータが追加されました。

idを指定してデータを取得してみましょう。curl.exe “http://localhost:4000/todos/1”を実行します。

image_5_5_6.png

先ほど作成したTODOを取得できました。

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

image_5_5_7.png

データを削除することができました。

再び、TODOの一覧を取得してみると、空になっています。

image_5_5_8.png

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

image_5_5_9.png

この例ではLearn DockerLearn SQLLearn Nodeというtitleで3つデータを追加しています。

コンテナの中に入ってみる

先ほどは起動したコンテナにホストマシンからcurlでリクエストを行なってデータを操作しました。

今度は起動しているコンテナの中に入ってデータベースを操作してみましょう。

Docker Composeが実行しているコンテナ内でコマンドを実行するにはdocker compose execコマンドを使用します。

docker compose exec [サービス名] [コンテナ内で実行するコマンド]という形式で使います。

今回はdbサービスの中でデータベースに接続したいので、docker compose exec db mysql —pasword=rootを実行して下さい。

image_5_5_10.png

mysqlに接続することができました。

データベースの一覧を表示してみましょう。show databases;を入力します。

image_5_5_11.png

アプリで使用しているmy_sql_dbデータベースが確認できます。

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

image_5_5_12.png

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

image_5_5_13.png

Node側で作成していたTodosテーブルが確認できます。

先ほど、コンテナの外からcurlで追加したデータを取得してみましょう。

select * from Todos;を入力して下さい。

image_5_5_14.png

Todosテーブルのデータの一覧を表示することができます。

このようにdocker compose execコマンドでコンテナの中に入って、クエリを叩くことでmysqlを直接操作できることを確認しました。

本LessonはMySQLを対象にしていないのでこれ以上は触れませんが、その他にも様々なクエリを実行できることを確かめてみて下さい。

Dockerを利用している恩恵として、操作に失敗してもコンテナやボリュームを再作成すれば、また1から試行することができます。

まとめ

このchapterではLessonのまとめとして、Docker ComposeでAPIとDBの2つのコンテナを用いてTODOアプリを作成しました。

また、Lesson5を通して、Docker Composeによって複数のコンテナを定義して実行する方法を学習しましたが、最初の内は実現したいことに対する設定ファイルの用意の仕方などが分からなくなったりすることもあるかと思います。

理解してしまえば難しいことはないのですが、特にDockerfilecompose.ymlの違いについては以下のように明確に区別しておいて下さい。

ファイル 指定する内容
Dockerfile どのようなイメージを作成するか
compose.yml どのようにコンテナを起動するか

そして設定について分からない点があれば、DockerfileについてはLesson4を、compose.ymlについてはLesson5を遡って復習してみて下さい。

ただ設定の項目については全てを網羅しているわけではありません。Lesson4と5で学習した内容を基本に、新たに行いたいことがあれば各自で調べてみて下さい。

日本語で読める公式のリファレンスとして、Dockerfileと、docker composeがあります。