Docker Compose について調べてみた。

記事の目的

こんにちわ、そしてはじめまして、 ダニーです。
3回目の投稿になります。(既にネタ切れ・・・困った、継続的にブログ投稿出来る人尊敬する。)

突然ですが!
当社のローカル開発環境は現在 Docker のコンテナ上で開発を行っています。
当たり前のように利用してる Docker ですが、実は今でも手探りだったり、ググって動かしてなんか動いてるからヨシ!・・・なぁーんてこともあったりして、わかってないなぁーってなることがまだまだあるので、今回当社のローカルの開発環境の構成に似せた環境作りをして理解を深めようというのが、ブログ記事の目的となります。

対象者は、Docker 使ってる人・これから使ってみようと思ってる人向けの記事となります。

Docker って改めてどうなの?

Docker とは、各実行環境を仮想的にコンテナ化して各オペレーティングシステム(Windwos / Mac / Linux など)やクラウド上でアプリケーションの構築(build)・共有(share)・実行(run)をすることができるソフトウェアプラットフォームです。(だと思っています。)

当社のローカルの開発環境では、Docker Compose で複数のコンテナを定義して一つのアプリケーション環境を構築しています。
普段開発するときは、ローカルで環境を構築・実行をコマンド一発で準備できるので、実際 Docker って便利だと実感してます。(ありがたや)
他にも恩恵として、実行環境構築とアプリケーション構成をコード化できることで、メンバー同士が環境を共有できるのがやっぱりいいですよね。
それ以外にも言葉を恐れない言い方になりますが、クラウドプラットフォームでの Docker 利用でどんな環境のアプリケーション構成でもすばやくデプロイやスケールができて、それをコードで実行ができるのも特徴がありますよね。
(今回はその辺は切り込まないです。クラウド・インフラ周りはよくわからんのでまた別の機会で!)

Docker の開発環境の構成を構築してみよう

今回はローカル環境の構築をメインに、それに関わる範囲を紹介しようと思います。

まずは構成の構想

今回は当社の構成を参考に

  • frontend : フロントエンド環境用のコンテナ
  • backend : バックエンド環境用のコンテナ
  • db : MySQL環境用のコンテナ

辺りの構成を作ってみようと思います。
ここで起動のイメージですが、

  1. db:MySQLサーバーの起動が完了する
  2. backend:npm run start で db コンテナの MySQLサーバーにアクセスできる状態
  3. frontend:npm run dev で Next.js 起動して 「http://localhost:3000/」アクセスできる状態各コンテナの起動準備が完了してる状態

という感じで各コンテナが起動準備が完了してる状態を目標にします。

ディレクトリ構成

構成はこちらです。

./sample
├── db                                   :dbコンテナ用のディレクトリ
│     └── docker-entrypoint-initdb.d
├── backend                              :backendコンテナ用のディレクトリ
│     ├── Dockerfile
│     └── ・・・
├── frontend                             ;frontendコンテナ用のディレクトリ
│     ├── Dockerfile
│     └── ・・・
└── docker-compose.yml

まずは各コンテナ毎にディレクトリを分けます。

メリットは、各コンテナを 分離することでリポジトリ管理を分離して管理をしやすくしたりなどがあります。(私は単純にわかりやすいので好きです。)

デメリットは、frontend / backend で共通で管理したいものも分離しちゃうので、型情報とか共通化したいときひと手間必要になると思います。(この辺いい方法あったら教えてほしい)

docker-compose.yml を書きます。

今回の構築に関する docker-compose.yml の中身はこちらです。

version: '3.9'

x-mysql_root_password: &MYSQL_ROOT_PASSWORD "sample"
x-mysql_database: &MYSQL_DATABASE "sample"
x-mysql_user: &MYSQL_USER "sample"
x-mysql_password: &MYSQL_PASSWORD "sample"
x-mysql_host_name: &MYSQL_HOST_NAME "db"

services:
  db:
    image: mysql:8.0.31
    # platform: linux/arm64/v8 # m1 mac とかでやる場合はプラットフォーム arm64 系列を指定あげるといい
    environment:
      MYSQL_ROOT_PASSWORD: *MYSQL_ROOT_PASSWORD
      MYSQL_DATABASE: *MYSQL_DATABASE
      MYSQL_USER: *MYSQL_USER
      MYSQL_PASSWORD: *MYSQL_PASSWORD
      TZ: "Asia/Tokyo"
    ports:
      - 3306:3306
    volumes:
      - db-data-volume:/var/lib/mysql
      - ./db/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
    healthcheck:
      test:
        [
          "CMD",
          "mysqladmin",
          "ping",
          "-u",
          "sample",
          "-psample"
        ]
      timeout: 20s
      retries: 10
  backend:
    build:
      context: .
      dockerfile: backend/Dockerfile
    working_dir: /app
    tty: true
    ports:
      - 4000:4000
    volumes:
      - ./backend:/app
      - backend_node_modules:/app/node_modules
    depends_on:
      db:
        condition: service_healthy
    environment:
      NODE_ENV: "development"
      MYSQL_ROOT_PASSWORD: *MYSQL_ROOT_PASSWORD
      MYSQL_DATABASE: *MYSQL_DATABASE
      MYSQL_USER: *MYSQL_USER
      MYSQL_PASSWORD: *MYSQL_PASSWORD
      MYSQL_HOST_NAME: *MYSQL_HOST_NAME
    command: >
      ash -c "npm run start"
  frontend:
    build:
      context: .
      dockerfile: frontend/Dockerfile
    working_dir: /app
    volumes:
      - ./frontend:/app
      - frontend_node_modules:/app/node_modules
    depends_on:
      - backend
    tty: true
    ports:
      - 3000:3000
    command: >
      ash -c "npm run dev"
volumes:
  frontend_node_modules:
  backend_node_modules:
  db-data-volume:

中身の解説

version: '3.9'

version 情報は、公式のリファレンスを確認するとあくまでバージョン情報を「参考」にするだけってことがわかりました。(脳死で付けてた勢)

公式リファレンス:https://docs.docker.com/compose/compose-file/#version-top-level-element

日本語ドキュメントも載せておきます。(本ページにも記載ありますが、一部情報が古いかもなんで英語OKな方は公式がおすすめ

https://docs.docker.jp/compose/compose-file/index.html#version
x-mysql_root_password: &MYSQL_ROOT_PASSWORD "sample"
x-mysql_database: &MYSQL_DATABASE "sample"
x-mysql_user: &MYSQL_USER "sample"
x-mysql_password: &MYSQL_PASSWORD "sample"
x-mysql_host_name: &MYSQL_HOST_NAME "db"

ここは docker-compose の仕様ではなく、YAMLの「アンカー/エイリアス」機能が docker-compose.yml で利用出来るようになったようなので利用しました。(各コンテナ定義で使われます)
x-」で始まる文字列を指定することで、Composeファイル内で警告なくあらゆる構造が利用出来ます。
「x-mysql_root_password: &MYSQL_ROOT_PASSWORD “sample”」 を例にあげると

  • x-mysql_root_password というフィールドを作ります。
  • 上記に対して、&MYSQL_ROOT_PASSWORD のエイリアスを設定して 「sample」という値を定義します
  • 上記を展開する場合は「*MYSQL_ROOT_PASSWORD」で展開(各コンテナの environment で使用)

ということが可能になります。
※Docker のリファレンスにも記載ありました。

公式:https://docs.docker.com/compose/compose-file/#extension
日本語:https://docs.docker.jp/compose/compose-file/index.html#compose-spec-extension

ただ、リファレンス見ただけだと、理解が追いつかんかったので結局自分で調べながらした結果この記述に落ち着きました。

services:

services は各コンテナを一つにまとめる役割があります。
各コンテナを一つの「servise」 として各コンテナの定義をネストして記述していきます。
今回だと、「db service」「backend service」「frontend service」ってな感じですね。

公式:https://docs.docker.com/compose/compose-file/#services-top-level-element
日本語:https://docs.docker.jp/compose/compose-file/index.html#services
  db:
    image: mysql:8.0.31
    # platform: linux/arm64/v8 # m1 mac とかでやる場合はプラットフォーム arm64 系列を指定あげるといい
    environment:
      MYSQL_ROOT_PASSWORD: *MYSQL_ROOT_PASSWORD
      MYSQL_DATABASE: *MYSQL_DATABASE
      MYSQL_USER: *MYSQL_USER
      MYSQL_PASSWORD: *MYSQL_PASSWORD
      TZ: "Asia/Tokyo"
    ports:
      - 3306:3306
    volumes:
      - db-data-volume:/var/lib/mysql
      - ./db/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
    healthcheck:
      test:
        [
          "CMD",
          "mysqladmin",
          "ping",
          "-u",
          "sample",
          "-psample"
        ]
      timeout: 20s
      retries: 10

ここからは、各service 単位で見ていきます。
まずdb service のコンテナ定義をここで行っています。

image:mysql:
    8.0.31 のimageを使うことを宣言
platform:
    linux/arm64/v8 のプラットフォーム環境で MySQL を実行する宣言(m1 mac 等の arm 環境の時必要だが、コンテナに arm 対応イメージがあるかは確認が必要)
environment:
    db コンテナに対して、環境変数を宣言(ここで、アンカー/エイリアスが使われています)
ports:
    ホストの3306ポートで来たものをコンテナの3306のポートへ転送をする宣言
    ※定義内容は「ホストポート:コンテナポート」
volumes:
    ファイル・Volume への同期を宣言
healthcheck:
    dbコンテナ起動後にMySQLサーバーが起動してそれの起動準備が出来ているか?のチェックを走らせる宣言

それぞれの説明はこんな感じです。
それぞれは書いてる通り、db のコンテナに

  • 何のイメージ使うのか?
  • どのプラットフォーム使うのか?
  • 環境変数はなにを定義するのか?
  • ポートどれを転送するのか?
  • 同期するボーリュームはどれか?

って感じで定義を書いています。

ここで注目したいのは、「 healthcheck 」です。
これは、コンテナ起動後サービスが「正常」かどうかを判断させるためのものです。

公式:https://docs.docker.com/compose/compose-file/#healthcheck
日本語;https://docs.docker.jp/compose/compose-file/index.html#healthcheck

test で定義してるコマンドは、mysql が起動中であるかの確認用コマンドを記述しています。
これを、timeout 20s / retries 10 回 に設定して実行してもらってます。
これによって、立ち上げるときに「Container sample-db-1 Waiting」となり、このとき test で定義してるコマンドが正常に帰って来るのを timeout / retries の設定期間待ってる感じですね。

正常に起動が完了すると「Container sample-db-1 Healthy」となり、後続のコンテナが続々起動していきます。

という感じで、db コンテナ service の紹介は以上です。
※Volumeの「docker-entrypoint-initdb.d」 はイメージ側のお話なんで割愛(気になる人はdocker hub の オフィシャル見よう)

Dockerhub;https://hub.docker.com/_/mysql
  backend:
    build:
      context: .
      dockerfile: backend/Dockerfile
    working_dir: /app
    tty: true
    ports:
      - 4000:4000
    volumes:
      - ./backend:/app
      - backend_node_modules:/app/node_modules
    depends_on:
      db:
        condition: service_healthy
    environment:
      NODE_ENV: "development"
      MYSQL_ROOT_PASSWORD: *MYSQL_ROOT_PASSWORD
      MYSQL_DATABASE: *MYSQL_DATABASE
      MYSQL_USER: *MYSQL_USER
      MYSQL_PASSWORD: *MYSQL_PASSWORD
      MYSQL_HOST_NAME: *MYSQL_HOST_NAME
    command: >
      ash -c "npm run start"

次は backend service のコンテナ定義です。(説明済みは割愛)

build:
    ソース(Dockerfile)からコンテナイメージを作成するための構築情報を宣言
working_dir:
    /app を作業ディレクトリにする宣言
tty:
    サービスコンテナに TTY を使って実行する宣言
depends_on:
    サービス間の起動順番を終了順番の依存を宣言
command:
    ash Shell 使って、「npm run start」コンテナ起動時に実行する宣言

ここでの注目は「build」「depends_on」です。
db コンテナ は image を宣言していましたが、backend は Dockerfile で構成を宣言してコンテナを作成しています。
build で定義している、context / dockerfile 宣言で dockerfile を含むディレクトリと使用する Dockerfile を指定しています。

公式:https://docs.docker.com/compose/compose-file/build/#build-definition
日本語:https://docs.docker.jp/compose/compose-file/build.html#build

backend コンテナの Dockerfile は最低限のこんな感じで構成だけ雑に作りました。

FROM node:18.12.1-alpine3.17
# FROM --platform=arm64 node:18.12.1-alpine3.17 # m1 mac 等の arm 環境なら platform 指定してあげよう

RUN apk update && \
  apk add --no-cache --virtual=install-list git

depends_on では、起動順と終了順を定義しています。
ここでは、

  • db コンテナ の起動のあとに、backend コンテナを起動する。
  • 終了のときは、backend コンテナを先に終了させる。

と、起動と終了を制御しています。

そしてもう一つ注目なのは「condition: service_healthy」です。
これは db コンテナで説明した 「healthcheck」 が活躍します。
「condition: service_healthy」を定義することで、db コンテナの MySQL サーバーが正常に起動したことを確認してからbackend コンテナの起動を開始するので、エラーなく起動させることが出来ます。
これを知らなかったとき 「docker compose up -d」 で初回コマンド叩くとき db コンテナの起動は成功してるけど、MySQL サーバーの起動は完了してなくて、毎度初回起動失敗して「なんでやねん!」って叫んでたのはいい思い出。(いやめちゃめちゃキレてましたw)

公式:https://docs.docker.com/compose/compose-file/#depends_on
日本語:https://docs.docker.jp/compose/compose-file/index.html#depends-on

これで、backend コンテナ service の説明は以上です。

  frontend:
    build:
      context: .
      dockerfile: frontend/Dockerfile
    working_dir: /app
    volumes:
      - ./frontend:/app
      - frontend_node_modules:/app/node_modules
    depends_on:
      - backend
    tty: true
    ports:
      - 3000:3000
    command: >
      ash -c "npm run dev"

次は frontend service のコンテナ定義です。
・・・が! backend のコンテナ定義で説明済みなんで、注目する所なかったですわ。
Dockerfile も backend のコピペでOKです。

FROM node:18.12.1-alpine3.17
# FROM --platform=arm64 node:18.12.1-alpine3.17 # m1 mac 等の arm 環境なら platform 指定してあげよう

RUN apk update && \
  apk add --no-cache --virtual=install-list git

なので!割愛!!

volumes:
  frontend_node_modules:
  backend_node_modules:
  db-data-volume:

volumes: これ各コンテナにもありましたよね?
もちろんちゃんと関係してます。
volumes とは、コンテナ内のデータを継続的に利用するための永続化するための領域を定義しています。
ここでいうと、

  • frontend_node_modules
  • backend_node_modules
  • db-data-volume

という名前で領域を定義して、各コンテナ内の同期したいPathを定義します。

それによって、再起動させても継続的にデータが利用できるという仕組みです。

公式:https://docs.docker.com/compose/compose-file/#volumes-top-level-element
日本語:https://docs.docker.jp/compose/compose-file/index.html#volumes-top-level-element

Docker Desktop からも確認出来ます。

命名規則は特に指定がなければ、「{docker-compose.yml のカレントディレクトリ名}_{service名}_{volume名}」という感じで決まるようですね。(意識してなかったからなるほど納得した)

いざ起動

ここまで作成しましたので、無事コンテナ起動できるかやってみます。
まずは、docker compose up -d でコンテナを立ち上げて見ましょう。
初回は、db / backend / frontend のコンテナ構成がありませんので、作る所から始まります。

一度作成されれば、以降は起動させるだけになります。(エコ)

では 早速 forntend のコンテナから覗いてみましょう。
今回は、動かすだけなんで中身は割愛しますが、やったことは Next.js の Getiing Started を参考にインストールしました。

Next.js 公式: https://nextjs.org/docs

frontend の 状況を Docker Desktop で見ると、無事起動して npm run dev が実行されていました。

結果、http://localhost:3000/ で Next.js が動作しましたので成功です!🎉

続いて、backend コンテナの方を確認しましょう。
backend は 今回 TypeORM を利用しました。

TypeORM公式:https://typeorm.io/

インストール 〜 Quick Start までやってみた感じです。

結果はこちら

無事、Insert は成功してるっぽいので、db コンテナ に入っているかも確認してみましょう。

無事成功してるようなので、backend コンテナ → db コンテナ へ アクセスも出来ているようです。🎉

まとめ

いかがでしたでしょうか?
今回は Docker Compose での構成 〜 起動までを題材にしてみました。
ブログ書いてるときに思ったこととして、実際業務で使ってるけどリファレンスに目を通してて知らないもの結構多かったです。
今回のブログを通して、色々深堀りするいい機会だなと感じたので、皆さんもDocker を利用した開発環境作りを試して見てはいかがでしょうか!

小話:最近知った Docker Desktop Extension が良かったって話

まとめしたあとですけど、Docker Desktop の Extensions 最近便利なのがあったの思い出したので、ちょっとご紹介しようかなと思います。(もう少しつづくぞい)

今回紹介する Extension はこちら です。

  • Logs Explorer
  • Volumes Backup & Share

Logs Explorer

開発してて

  • エラーのときコンテナのログ見るけど、CLI でもいいけど、さらっと見たいとき(CLIめんどくさい気分のとき)
  • ログから特定のログだけキーワード検索したいけど、CLI ・・・以下略

みたいなときないですか?
そこでこちらの Extension です。
Logs Explorer は各サービスコンテナのログ出力を提供しています。
ログの検索機能やコンテナのフィルターなどが出来ます。

さらっと確認したいときめっちゃ便利だったので、紹介しました。

Volumes Backup & Share

当社で開発してるときによくある話なんですが、

  • 開発後のユニットテスト以外にも手動テストする時にDBの中身一回初期化したい(邪魔なデータ消したい)
  • 日時で状態が変わるようなテストで、日付未来にしたけど戻す時正常な状態に戻したいから一回初期化したい。(事故防止)
  • だけど、いちいちDBの初期化するのめんどくさい・・・
  • 初期化した時のDBのVolume使い回せたら楽なんだけどなぁ・・・・(チラ)

はい、これを解決してくれる Extension がこちらです!
こちら Volume を import / export する機能を提供してくれる Extension です。
これによって、業務のテストする時に初期状態の db の Volume を export しといて、テスト終わったら、初期状態の Volume を import してサクッと戻すなんてことが可能です。
逆に、テスト結果を export することで、メンバーに共有(Share) することも可能なので、原因調査する時にDBのVolume共有して、サクッと一緒に調査始めるなぁーんてことも可能です。(再現データ作る時とかの手間がグッと短縮出来る!)

使い方も操作したい Volume を選択して、 ACTIONS 項目の import / export 選択と保存場所とファイル名指定するだけなんでシンプルです。

export

import

当社開発の時にメリットがありすぎたので、ご紹介しました。
デメリットとしては、DB更新された時に再度作り直さないといけないとかですが、リリースタイミング等に作り直しておけば、きれいな状態のDBが何度でも利用出来るので、今の所気になったりはしてないです。

今度こそ本当に以上です!
長々とお付き合い頂きありがとうございました!