この記事は、Dockerとdocker-composeの概要とTipsについて、今さらながらまとめたものである。
1. Dockerとdocker-composeの概要
1.1. Docker
ホストマシンのLinuxカーネルを利用して、ホストマシンとは別のLinuxディストリビューションで仮想環境を起動できるソフトウェアで、正式には「Dockerエンジン」。
Docker上で動作する各仮想環境は「コンテナ」と呼ばれる。
より詳細には、こちらを参照。
1.1.1. 使い方
Dockerfile
というファイルに、コンテナの設定(各ソフトウェアのインストールなど)を記述する。
例. とあるGoアプリケーションのDockerfile
# ベースイメージ
FROM golang:1.21.1
# モックモジュールのインストール
RUN go install go.uber.org/mock/mockgen@latest
# /appディレクトリの作成&移動
WORKDIR /app
# プロジェクトのコピー
COPY . .
# 必要なGoモジュールをダウンロード
RUN go mod download
# Goアプリケーション実行ファイルをビルドしてルートディレクトリに配置
RUN GOOS=linux GOARCH=amd64 go build -o /sample_app ./cmd/sample_app/*.go
# Goアプリケーションの実行
CMD ["/sample_app"]
docker
コマンドで、Dockerfileを元にイメージを作成したり、イメージをDockerレジストリへpush・pullしたり、イメージからコンテナを起動・停止したりできる。
以下の各 docker コマンドの動作のイメージを示す。
Docker イメージ操作関連のコマンド
- docker build
- docker login
- docker push
- docker pull
Docker コンテナ操作関連のコマンド
- docker create
- docker start
- docker run
- docker pause
- docker unpause
- docker start
- docker stop
- docker rm
図中のステータスについては、後ほどdocker ps
コマンドと関連付けて詳細に説明する。
Docker Compose
「ローカル開発環境の構築」向けのコンテナオーケストレーションツールのデファクトスタンダード。
Docker Composeを利用する上で、各コンテナの元になるDockerfile
やイメージは別途用意する必要がある。
使い方
docker-compose.yml
というファイルに、以下のようなコンテナオーケストレーションの設定を記述する。
- コンテナを作成する際の元になるDockerfile or イメージ
- コンテナを作成・起動する際のdockerコマンドのオプションに相当するもの
- コンテナの起動順序、起動条件
例. とあるサンプルアプリのローカル開発環境を構築するためのdocker-compose.yml
services:
# Spanner エミュレータ
spanner-emulator:
image: gcr.io/cloud-spanner-emulator/emulator:1.5.22
ports:
- "9010:9010"
- "9020:9020"
# Spanner エミュレータ起動後に流すスクリプト
# 指定したプロジェクトID, インスタンスID, データベースIDのデータベースを作成する
spanner-emulator-init-script:
image: gcr.io/google.com/cloudsdktool/google-cloud-cli:alpine
platform: linux/amd64
command: >
bash -c 'gcloud config configurations create emulator || gcloud config configurations activate emulator &&
gcloud config set auth/disable_credentials true &&
gcloud config set project ${GCP_PROJECT_ID} &&
gcloud config set api_endpoint_overrides/spanner ${SPANNER_EMULATOR_URL}/ --quiet&&
gcloud spanner instances create ${SPANNER_INSTANCE_ID} --config=emulator-config --description="Test Instance" --nodes=1 &&
gcloud spanner databases create ${SPANNER_DATABASE_ID} --instance=${SPANNER_INSTANCE_ID}'
depends_on:
- spanner-emulator
# Spanner エミュレータのヘルスチェック用コンテナ
spanner-emulator-healthcheck:
image: curlimages/curl
depends_on:
- spanner-emulator
platform: linux/amd64
entrypoint: tail -f /dev/null
healthcheck:
test: curl -f ${SPANNER_EMULATOR_URL}/v1/projects/${GCP_PROJECT_ID}/instances/${SPANNER_INSTANCE_ID}/databases/${SPANNER_DATABASE_ID}
interval: 5s
timeout: 10s
retries: 10
start_period: 10s
# GoアプリケーションのAPIサーバ
api:
build:
context: ../../
dockerfile: ./build/packages/docker/Dockerfile.sample_app
ports:
- "8080:8080"
env_file:
- .env
tty: true
volumes:
- type: bind
source: ../../
target: /app/
depends_on:
spanner-emulator-healthcheck:
condition: service_healthy
# Goアプリケーションのヘルスチェック用コンテナ
api-healthcheck:
image: fullstorydev/grpcurl:latest-alpine
depends_on:
- api
platform: linux/amd64
entrypoint: tail -f /dev/null
healthcheck:
test: |
grpcurl -plaintext -d '{"name": "sample"}' ${API_HOST} api.SampleService.CreateSample
interval: 5s
timeout: 10s
retries: 10
start_period: 10s
# 全てのサービスの起動が完了したことの確認用
all-service-up:
image: alpine
depends_on:
api-healthcheck:
condition: service_healthy
entrypoint: echo "all services up"
docker-compose.yml
を用意した上で、docker-compose
コマンドを実行することで、主に以下の操作を行う。
- 各コンテナのイメージの作成(
docker-compose build
) - 依存関係を考慮した各コンテナの作成・起動(
docker-compose up
) - 各コンテナの削除(
docker-compose down
)
2. Tips
2.1. 起動しているコンテナ内で任意のコマンドを実行する
docker exec -it `docker ps -qf name=<コンテナ名(の一部)>` <コマンド>
注意点
docker ps -qf name=<コンテナ名(の一部)>
で、コンテナが複数ヒットする場合は、コマンドを実行するコンテナが不確実になってしまうため注意。
不要なコンテナをあらかじめ停止
または削除
しておくと、docker psの表示対象外となるため多少避けられる。
2.1.1. 起動しているコンテナ内へログインする
上記の<コマンド>
を シェルを起動するコマンド(/bin/bash
とか)にする。
2.2. dockerイメージから元になったDockerfileの内容を知る
docker history <イメージ> --no-trunc --format '{{ json .CreatedBy }}'| tail -r
例として、この記事内の とあるGoアプリケーションのDockerfile のDockerfileを元に作成したイメージに対して、上記コマンドを実行してみると以下のように出力された。
"/bin/sh -c #(nop) ADD file:7a0adbde6e967e2bcaafa69f04fabdec993025645c8d0d51acc991a31b404eed in / "
"/bin/sh -c #(nop) CMD [\"bash\"]"
"/bin/sh -c set -eux; apt-get update; apt-get install -y --no-install-recommends ca-certificates curl gnupg netbase sq wget ; rm -rf /var/lib/apt/lists/*"
"/bin/sh -c apt-get update && apt-get install -y --no-install-recommends git mercurial openssh-client subversion procps && rm -rf /var/lib/apt/lists/*"
"/bin/sh -c set -eux; apt-get update; apt-get install -y --no-install-recommends g++ gcc libc6-dev make pkg-config ; rm -rf /var/lib/apt/lists/*"
"/bin/sh -c #(nop) ENV PATH=/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
"/bin/sh -c #(nop) ENV GOLANG_VERSION=1.21.1"
"/bin/sh -c set -eux; arch=\"$(dpkg --print-architecture)\"; arch=\"${arch##*-}\"; url=; case \"$arch\" in 'amd64') url='https://dl.google.com/go/go1.21.1.linux-amd64.tar.gz'; sha256='b3075ae1ce5dab85f89bc7905d1632de23ca196bd8336afd93fa97434cfa55ae'; ;; 'armel') export GOARCH='arm' GOARM='5' GOOS='linux'; ;; 'armhf') url='https://dl.google.com/go/go1.21.1.linux-armv6l.tar.gz'; sha256='f3716a43f59ae69999841d6007b42c9e286e8d8ce470656fb3e70d7be2d7ca85'; ;; 'arm64') url='https://dl.google.com/go/go1.21.1.linux-arm64.tar.gz'; sha256='7da1a3936a928fd0b2602ed4f3ef535b8cd1990f1503b8d3e1acc0fa0759c967'; ;; 'i386') url='https://dl.google.com/go/go1.21.1.linux-386.tar.gz'; sha256='b93850666cdadbd696a986cf7b03111fe99db8c34a9aaa113d7c96d0081e1901'; ;; 'mips64el') url='https://dl.google.com/go/go1.21.1.linux-mips64le.tar.gz'; sha256='3aa007a41f533b50eae2491bbd29926ada09357367a8aa05e7e50ec50c78acf9'; ;; 'ppc64el') url='https://dl.google.com/go/go1.21.1.linux-ppc64le.tar.gz'; sha256='eddf018206f8a5589bda75252b72716d26611efebabdca5d0083ec15e9e41ab7'; ;; 'riscv64') url='https://dl.google.com/go/go1.21.1.linux-riscv64.tar.gz'; sha256='fac64ed26e003f49f1d77f6d2c4cf951422aecbce12232d9ec1bf4585fc44ee1'; ;; 's390x') url='https://dl.google.com/go/go1.21.1.linux-s390x.tar.gz'; sha256='a83b3e8eb4dbf76294e773055eb51397510ff4d612a247bad9903560267bba6d'; ;; *) echo >&2 \"error: unsupported architecture '$arch' (likely packaging update needed)\"; exit 1 ;; esac; build=; if [ -z \"$url\" ]; then build=1; url='https://dl.google.com/go/go1.21.1.src.tar.gz'; sha256='bfa36bf75e9a1e9cbbdb9abcf9d1707e479bd3a07880a8ae3564caee5711cb99'; echo >&2; echo >&2 \"warning: current architecture ($arch) does not have a compatible Go binary release; will be building from source\"; echo >&2; fi; wget -O go.tgz.asc \"$url.asc\"; wget -O go.tgz \"$url\" --progress=dot:giga; echo \"$sha256 *go.tgz\" | sha256sum -c -; GNUPGHOME=\"$(mktemp -d)\"; export GNUPGHOME; gpg --batch --keyserver keyserver.ubuntu.com --recv-keys 'EB4C 1BFD 4F04 2F6D DDCC EC91 7721 F63B D38B 4796'; gpg --batch --keyserver keyserver.ubuntu.com --recv-keys '2F52 8D36 D67B 69ED F998 D857 78BD 6547 3CB3 BD13'; gpg --batch --verify go.tgz.asc go.tgz; gpgconf --kill all; rm -rf \"$GNUPGHOME\" go.tgz.asc; tar -C /usr/local -xzf go.tgz; rm go.tgz; if [ -n \"$build\" ]; then savedAptMark=\"$(apt-mark showmanual)\"; apt-get update; apt-get install -y --no-install-recommends golang-go; export GOCACHE='/tmp/gocache'; ( cd /usr/local/go/src; export GOROOT_BOOTSTRAP=\"$(go env GOROOT)\" GOHOSTOS=\"$GOOS\" GOHOSTARCH=\"$GOARCH\"; ./make.bash; ); apt-mark auto '.*' > /dev/null; apt-mark manual $savedAptMark > /dev/null; apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; rm -rf /var/lib/apt/lists/*; rm -rf /usr/local/go/pkg/*/cmd /usr/local/go/pkg/bootstrap /usr/local/go/pkg/obj /usr/local/go/pkg/tool/*/api /usr/local/go/pkg/tool/*/go_bootstrap /usr/local/go/src/cmd/dist/dist \"$GOCACHE\" ; fi; go version"
"/bin/sh -c #(nop) ENV GOTOOLCHAIN=local"
"/bin/sh -c #(nop) ENV GOPATH=/go"
"/bin/sh -c #(nop) ENV PATH=/go/bin:/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
"/bin/sh -c mkdir -p \"$GOPATH/src\" \"$GOPATH/bin\" && chmod -R 1777 \"$GOPATH\""
"/bin/sh -c #(nop) WORKDIR /go"
"RUN /bin/sh -c go install go.uber.org/mock/mockgen@latest # buildkit"
"WORKDIR /app"
"COPY . . # buildkit"
"RUN /bin/sh -c go mod download # buildkit"
"RUN /bin/sh -c GOOS=linux GOARCH=amd64 go build -o /sample_app ./cmd/sample_app/*.go # buildkit"
"CMD [\"/sample_app\"]"
2.3. ローカルに存在する全てのイメージを一覧表示する
docker image ls -a
2.4. ローカルに存在する全てのコンテナを一覧表示する
docker ps -a
実行すると、以下のような形で表示される。
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3790a90b5c14 alpine "echo 'all services …" 6 days ago Exited (0) 6 days ago sample_app-all-service-up-1
6cc2aa94fcb1 sample_app-api "/sample_app" 6 days ago Up 6 days 0.0.0.0:8080->8080/tcp sample_app-api-1
964e0e1cb230 gcr.io/google.com/cloudsdktool/google-cloud-cli:alpine "bash -c 'gcloud con…" 6 days ago Exited (0) 6 days ago sample_app-spanner-emulator-init-script-1
91f6234133e0 gcr.io/cloud-spanner-emulator/emulator:1.5.22 "./gateway_main --ho…" 6 days ago Up 6 days 0.0.0.0:9010->9010/tcp, 0.0.0.0:9020->9020/tcp sample_app-spanner-emulator-1
2.4.1. docker psで一覧表示されるコンテナのSTATUSについて
docker ps
や後述のdocker-compose ps
で表示されるSTATUS
は、本記事で示したコンテナのライフサイクルの図に登場する状態と対応付けると、以下のようになる。
docker psのSTATUS | 図中のステータス | 説明 |
---|---|---|
Created | CREATED | イメージからコンテナが作成された状態。コンテナのスペック(CPU, メモリなど)や、コンテナ起動時に内部で実行されるコマンドといったコンテナの設定が決まった状態。 |
Up | RUNNING | コンテナが正常に動作している状態。docker start を実行すると以下のようなフローでRUNNINGになる模様(実際に実験した結果より)。1. コンテナにCPU, メモリ等が割り当てられる 2. 当該コンテナのイメージに元となる Dockerfile に記述していたFROM, RUN, COPY が順に実行される(初回のCREATED->RUNNING時のみ)3. バインドマウントが適用される 4. ENTRYPOINT, CMD が実行される |
Up (paused) | PAUSED | コンテナが一時停止した状態。コンテナ内の全てのプロセスが一時停止したような(kill -STOP を実行したような)状態。 |
Exited (0) | EXITED | コンテナを正常にシャットダウンさせた状態。コンテナ動作中に生成・更新されたコンテナ内のファイルは残る。 |
Exited (0以外) | EXITED | コンテナを強制的にシャットダウンさせた状態。コンテナ動作中に生成・更新されたコンテナ内のファイルは残る。以下のようなケースでこのSTATUSになる。 - docker stop を実行したが、シャットダウンがタイムアウトになった- docker stop 実行中にCtrl + Cで強制終了させた |
表示すらされない | DELETED (or 一度もCREATEDになっていない) | コンテナ自体がそもそも作成されたことがないか、コンテナが削除された状態。コンテナを削除するとコンテナ内に存在していたファイルも消し飛ぶ。 |
なお、-a
を付けない場合、ステータスがEXITEDなコンテナは一覧表示されない。
2.5. Docker利用によるディスク容量の逼迫へ対処する
Dockerを使っていると簡単に数十GB容量食ってしまうため、定期的に不要なファイルを削除したくなる。
# Dockerで使われてるディスク容量を調査
# イメージ, コンテナ, ローカルボリューム, ビルドキャッシュそれぞれの使用量が表示される
docker system df
# ビルドキャッシュを削除
docker builder prune -f
# 全てのコンテナを停止(ステータスExitedに)
docker stop $(docker ps -q)
# Exitedになってる全てのコンテナを削除
docker container prune -f
# 存在する(ステータスがCreated でも Running でも Exited でも Pausedでもない)どのコンテナとも紐づかないイメージを全て削除
docker image prune -f
# 存在するどのコンテナとも紐づかないボリュームを全て削除
docker volume rm $(docker volume ls -qf dangling=true)
2.6. コンテナを起動しっぱなしにする
Dockerfile
のCMD
やENTRYPOINT
に、実行しても終了しないプロセスを起動するコマンドを記述する。
例. tail -f /dev/null
2.7. ホスト側のディレクトリ、ファイルをコンテナへ引き渡しつつのコンテナ初回起動処理をDockerfileに書きたい。
ビルド時にビルドコンテキストを指定 + Dockerfile
のCOPY
で実現できる。
COPY <ホスト側のパス> <コンテナ側のパス>
ホスト側のパス
ビルドコンテキストで指定したディレクトリを基準とする相対パスで記述する。
コンテナ側のパス
コンテナ側のパスは、絶対パスもしくはWORKDIR
を基準とする相対パスで記述する。
COPY
は、以下のような挙動をしてくれる。
- イメージのビルド時に、ホスト側に存在する
<ホスト側のパス>
以下のディレクトリ・ファイルをイメージ上へコピー - コンテナの初回起動時に、コンテナ内の
<コンテナ側のパス>
直下へコピー
ちなみに、ビルドコンテキストに含まれないホスト側のディレクトリ・ファイルは、COPYコマンドで触ることすらできないので注意。
例.
xxx/
yyy/
aaa.go
zzz/
bbb.go
ビルドコンテキストのパスとしてxxx/を指定してビルドした場合、
COPY ../zzz/bbb.go .` といった記述はできない。
2.7.1. ビルドコンテキスト
docker build
実行時に、Dockerエンジン側へ一時的に引き渡すディレクトリ・ファイル。
Dockerfile
内のCOPY
で使われる。
docker build
コマンドでのビルド時の指定方法
docker build -f <Dockerfileのパス> <ビルドコンテキストのパス>
-f
がない場合は、カレントディレクトリのDockerfileを使ってビルドされ、ビルドコンテキストもカレントディレクトリとみなされる。
docker-compose build
コマンドでのビルド時の指定方法
docker-compose.yml
内のbuild
パラメータに付随するcontext
パラメータ(必須)で、相対パスで指定する。
相対パスの基準は「docker-compose.ymlファイルが存在するディレクトリ」になる。
docker build
コマンドとは異なり、docker-compose build
では、デフォルトのパスが適用されることはなく、明示的に指定する必要がある。
2.8. 軽量なベースイメージを使う
以前は軽量ベースイメージといえばalpine
一択な印象だったが、最近ではdistroless, slim
といったものも出てきているらしい(要調査)。
2.9. マルチステージビルド
1つのDockerfile内に以下のような設定を記述できる。
最初のステージで
- 使いたいパッケージが入ったサイズが大きめのベースイメージを指定
- COPYでホスト側から多めにディレクトリ・ファイルコピー
- 実行ファイルをビルド
次のステージで
- 最低限のパッケージを含む軽量ベースイメージを指定
- COPYで前ステージのイメージから本番利用で必要になる実行ファイルのみコピー
どのステージまでビルドするのかは、docker build
の--target
オプションや、docker-compose.yml
でのbuild.target
で指定できる。
これにより、以下のようなことができる。
- 前のステージではデバッグツールなどを盛り込んだ重めのイメージを作成(開発環境向き)
- 後のステージでは実行ファイルとこれを動作させるのに必要な最低限のパッケージを盛り込んだ軽量なイメージを作成(本番環境向き)
2.10. Dockerfile内で記述する各要素の実行タイミング
実験して挙動を確認した感じ、以下のようになっている模様。
-
FROM, RUN, COPY
は、初回のコンテナ起動時(≒docker start
実行時)のみ、その順序で実行される。 -
CMD, ENTRYPOINT
は、毎回のコンテナ起動時に実行される。 - バインドマウント(
docker run
コマンドのオプションとして指定できる)は、毎回の起動時にCMD, ENTRYPOINT
の直前に実行される。
2.11. docker-composeコマンド関連のTips
2.11.1. docker-compose.ymlの検証
docker-compose -f <docker-compose.ymlのパス> -p <docker-composeのプロジェクト名> config
記述に問題があればその箇所をエラーで出力してくれる。
また、環境変数の適用状態も確認できる。
2.11.2. docker-compose up
各サービスのイメージをpullまたはビルドし、コンテナを作成・起動する
各サービスについて、デフォルトで以下の処理を一気通貫で実行してくれる。
- (イメージがなければ)イメージのpull(
docker pull
相当)またはビルド(docker build
相当) - (コンテナが存在していなければ)コンテナの作成(
docker create
相当) - コンテナの起動(
docker start
相当)
docker-compose -f <docker-compose.ymlのパス> -p <docker-composeのプロジェクト名> up -d
-f
: docker-compose.ymlへのパスを指定。指定しない場合は、カレントディレクトリ直下のdocker-compose.yml が使われる
-p
: プロジェクト名を指定。プロジェクト名は、各サービスのコンテナ名の一部としても使われる("<プロジェクト名>-<サービス名(docker-compose.yml内で指定)>-X")
-d
: バックグラウンドでコンテナを実行。
注意
docker-compose up
でのイメージのビルドについては、「ビルドキャッシュを使わない」のオプション指定ができないという制約がある。
ビルドキャッシュを使わないようにビルドしたい場合は、docker-compose build
を別途使うようにする。
所感: docker-compose up
万能すぎる
docker start
や docker create
に相当するdocker-composeのコマンドも個別に用意されているが、どういった場面で使うのか今のところ謎・・・
2.11.3. docker-compose build
各サービスのイメージをビルドする
各サービスについて、docker build
相当の処理をしてくれる。
docker-compose -f <docker-compose.ymlのパス> build --no-cache
-f
: docker-compose.ymlへのパスを指定できる。指定しない場合、カレントディレクトリ直下のdocker-compose.ymlが使われる
--no-cache
: キャッシュを利用せずにイメージをビルド。
2.11.3.1. 所感: ビルドキャッシュについて
イメージのビルドの高速化にこだわるのであれば、ビルドキャッシュを使うことを検討する必要がありそう。
ただし、ビルドキャッシュについては複雑な仕様があるようで、正確に把握していないと痛い目をみそう。
高速化にこだわる必要がないのであれば、ビルドキャッシュを使わないようにビルドするのが無難と考える。
2.11.4. docker-compose down
各サービスのコンテナを停止・削除する
各サービスのコンテナについて、docker stop
、docker delete
相当の処理を一気通貫で実行してくれる。
docker-compose -f <docker-compose.ymlのパス> -p <docker-composeのプロジェクト名> down
-f
: docker-compose.ymlへのパスを指定。指定しない場合は、カレントディレクトリ直下のdocker-compose.yml が使われる
-p
: プロジェクト名を指定。プロジェクト名は、各サービスのコンテナ名の一部としても使われる("<プロジェクト名>-<サービス名(docker-compose.yml内で指定)>-X")
注意
docker-compose up
で指定した docker-compose.yml および プロジェクト名と一致しない場合は、うまく動作しない。
2.11.5. docker-compose ps
各サービスのコンテナを一覧表示する
各サービスのコンテナについて、docker ps
相当の処理を実行。
docker-compose -f <docker-compose.ymlのパス> -p <docker-composeのプロジェクト名> ps
-f
: docker-compose.ymlへのパスを指定。指定しない場合は、カレントディレクトリ直下のdocker-compose.yml が使われる
-p
: プロジェクト名を指定。プロジェクト名は、各サービスのコンテナ名の一部としても使われる("<プロジェクト名>-<サービス名(docker-compose.yml内で指定)>-X")
-a
: つけない場合、コンテナのステータスがEXITEDなものは一覧に表示されない。
注意
docker-compose up
で指定した docker-compose.yml および プロジェクト名と一致しない場合は、うまく動作しない。
2.11.5.1. 各サービスのコンテナの一覧表示を見やすくする
結論としては、以下のコマンドを実行する。
# watch -n <更新間隔> 'docker-compose -f <ステータスを表示したいサービスが記述されているdocker-compose.ymlへのパス> -p <docker-compose up で指定するプロジェクト名> ps -a --format "table <表示したい項目>"'
# 具体例.
watch -n 0.05 'docker-compose -p sample_app -f deploy/docker-compose/docker-compose.yml ps -a --format "table {{.Service}}\t{{.Status}}"'
ステータスがEXITEDであるコンテナも表示対象にする
-a
をつけない場合は、ステータスが CREATED, RUNNING, PAUSED なコンテナのみ一覧表示される。
-a
をつけることで、EXITEDなものも対象になる。
※削除されているコンテナについてはそもそも表示されない
EXITEDなコンテナのみ表示対象外にしたい状況はあまり思いつかないため、とりあえずつけておくのが無難と考える
表示項目を制限する
デフォルトでは表示項目が多く、ターミナルで折り返しが発生してしまい非常に見づらい。
--format
オプションで表示項目を制限できる。例えば、
--format "table {{.Service}}\t{{.Status}}"
とすることで、サービス名とそのステータスのみを表示することができる。
ステータスをリアルタイムに表示し続ける
docker-compose.yml
やDockerfile
の設定をトライアンドエラーで調整する際、コンテナのステータスを確認したいことがある。都度docker-compose ps
を実行するのがめんどくさい。
「各コンテナがどんなステータス遷移をしているのかをリアルタイムに把握したい」ことも多い。
そんなときには、linuxコマンドwatch
を併用する。
# インストール(Macの場合)
brew install watch
# リアルタイムに表示
watch -n <更新間隔(秒)> <docker-comopse psコマンド>
2.12. docker-compose.ymlのTips
2.12.1. 各サービスのコンテナの起動順序を制御する
2.12.1.1. depends_on
とhealthcheck
depends_on
は、各コンテナの起動開始順序を制御できるだけであり、これだけだと前のコンテナの起動の開始がなされていれば、すぐに次のコンテナの起動が開始されてしまう。
しかし実際のところ、各コンテナの起動条件として、先に起動していてほしいコンテナについては、「そのコンテナがある状態になっていることまで確認した上で起動したい」はずである。
docker-compose
標準機能として、depends_onで指定したコンテナについて、追加のオプション"condition"で先に開始してほしいコンテナが満たすべきステータスを設定できる。
また、先に開始していてほしいコンテナ側のステータスを決定するための"healthcheck"という機能が用意されている。
depends_on
とhealthcheck
を併用することで、詳細にコンテナの開始条件を制御できる。
サンプルコード
- service2はservice1のステータスがhealthyになったらコンテナを開始できる。
- service1のヘルスチェックの設定は以下の通り。
- ヘルスチェックの成功条件は、「service1コンテナ内で、curlでlocalhostへのHTTPリクエストを投げるコマンドを実行する。ステータスコード200が返ってきたら成功、それ以外は失敗」とする。
- ヘルスチェックは最大10回連続で失敗するまで実行する。
- ヘルスチェックは5秒間隔で実行する。
- ヘルスチェックコマンド実行後、10秒経過してもコマンドの実行が終わらなければ失敗扱いとする。コンテナを開始してから10秒の間は、ヘルスチェックコマンドが失敗しても失敗回数にカウントしない。この期間も成功は認める。
services:
service1:
...
healthcheck:
test: curl -f http://localhost
interval: 5s
timeout: 10s
retries: 10
start_period: 10s
service2:
...
depends_on:
service1:
condition: service_healthy
オプションの解説:
test
そのサービスのコンテナ内部で実行するヘルスチェック用コマンドを記述する。
成功時に0, 失敗時に0以外の終了コードを返すようになっていれば何でもOK。
この例で記載しているcurl の -fオプションは、ステータスコード200なら終了コードを0にして、それ以外は終了コード0以外とするもの。 -fをつけない場合はどんなステータスコードでも終了コード0になる。シェルの話になるが、直前に実行したコマンドの戻り値は"$?"で確認できる。
interval
ヘルスチェックの実行間隔
timeout
ヘルスチェック用コマンド実行をタイムアウトにするしきい値
retries
リトライ回数
start_period
ヘルスチェック失敗をカウントしない時間。コンテナを開始してからの時間で設定。
2.12.1.2. helthcheckの弱点
healthcheck
は、指定したコマンドをステータスチェック対象のコンテナの内部で実行してステータスチェックするものであるため、distroless
をベースイメージとするような「チェックに必要なコマンドがインストールされないようなコンテナ」だとヘルスチェックに必要なコマンドが実行できず、チェックできないという問題がある。
回避策の例としては以下のようなものがある。
- 外形監視用のコンテナを別途起動する(後述)
- ヘルスチェック対象の軽量イメージをラップした独自イメージを頑張って作る
- wait-for-it使う
- dockerize使う
2.12.1.2. ヘルスチェックのために外形監視用のコンテナを用意する
個人的に作っているGoアプリケーションのローカル開発環境のdocker-compose.ymlで発生した問題を元に説明する。
元々の状況
Spannerエミュレータコンテナ(spanner-emulator)起動後、コンテナ外からSpannerエミュレータコンテナ内のある名前のデータベースに接続できる状態になってからGoアプリケーションコンテナ(api)を起動したい。
データベースに接続できる状態か?は、HTTPのAPIで確認できる。ゆえにspanner-emulatorサービスに対してhealthcheckを設定するのが正攻法。以下のようにhealthcheck, depends_onを設定したい。
しかし、spanenr-emulatorの元になっているイメージには、HTTPアクセスするためのcurlコマンドがインストールされていないため、正攻法でのチェックが不可能である。
services:
# Spanner エミュレータ
spanner-emulator:
image: gcr.io/cloud-spanner-emulator/emulator:1.5.22
ports:
- "9010:9010"
- "9020:9020"
healthcheck:
test: curl -f ${LOCAL_HOST_URL}/v1/projects/${GCP_PROJECT_ID}/instances/${SPANNER_INSTANCE_ID}/databases/${SPANNER_DATABASE_ID} # curlがインストールされていないため実行不可能
interval: 5s
timeout: 10s
retries: 10
start_period: 10s
# Spanner エミュレータ起動後に流すスクリプト
# 指定したプロジェクトID, インスタンスID, データベースIDのデータベースを作成する
spanner-emulator-init-script:
image: gcr.io/google.com/cloudsdktool/google-cloud-cli:alpine
platform: linux/amd64
command: >
bash -c 'gcloud config configurations create emulator || gcloud config configurations activate emulator &&
gcloud config set auth/disable_credentials true &&
gcloud config set project ${GCP_PROJECT_ID} &&
gcloud config set api_endpoint_overrides/spanner ${SPANNER_EMULATOR_URL}/ --quiet&&
gcloud spanner instances create ${SPANNER_INSTANCE_ID} --config=emulator-config --description="Test Instance" --nodes=1 &&
gcloud spanner databases create ${SPANNER_DATABASE_ID} --instance=${SPANNER_INSTANCE_ID}'
depends_on:
- spanner-emulator
# GoアプリケーションのAPIサーバ
api:
build:
context: ../../
dockerfile: ./build/packages/docker/Dockerfile.sample_app
ports:
- "8080:8080"
env_file:
- .env
tty: true
volumes:
- type: bind
source: ../../
target: /app/
depends_on:
spanner-emulator:
condition: service_healthy
回避策
これを回避するために、spanner-emulator
を外形監視するためのspanner-emulator-healthcheck
サービスを以下のように定義した。
-
curl
を使えるイメージをベースとしてコンテナを起動 -
spanner-emulator
コンテナの起動をトリガーにコンテナを起動 - コンテナが起動しっぱなしになるよう
tail -f /dev/null
をコンテナ起動時に実行するコマンドとして設定 -
spanenr-emulator
コンテナからステータスコード200が返って来たらspanner-emulator-healthcheck
コンテナのヘルスチェック成功となるよう設定
そしてapi
サービスでは、spanner-emulator-healthcheck
サービスの状態がhealthy
になったら起動という設定をしている。
services:
# Spanner エミュレータ
spanner-emulator:
image: gcr.io/cloud-spanner-emulator/emulator:1.5.22
ports:
- "9010:9010"
- "9020:9020"
# Spanner エミュレータ起動後に流すスクリプト
# 指定したプロジェクトID, インスタンスID, データベースIDのデータベースを作成する
spanner-emulator-init-script:
image: gcr.io/google.com/cloudsdktool/google-cloud-cli:alpine
platform: linux/amd64
command: >
bash -c 'gcloud config configurations create emulator || gcloud config configurations activate emulator &&
gcloud config set auth/disable_credentials true &&
gcloud config set project ${GCP_PROJECT_ID} &&
gcloud config set api_endpoint_overrides/spanner ${SPANNER_EMULATOR_URL}/ --quiet&&
gcloud spanner instances create ${SPANNER_INSTANCE_ID} --config=emulator-config --description="Test Instance" --nodes=1 &&
gcloud spanner databases create ${SPANNER_DATABASE_ID} --instance=${SPANNER_INSTANCE_ID}'
depends_on:
- spanner-emulator
# Spanner エミュレータのヘルスチェック用コンテナ
spanner-emulator-healthcheck:
image: curlimages/curl
depends_on:
- spanner-emulator
platform: linux/amd64
entrypoint: tail -f /dev/null
healthcheck:
test: curl -f ${SPANNER_EMULATOR_URL}/v1/projects/${GCP_PROJECT_ID}/instances/${SPANNER_INSTANCE_ID}/databases/${SPANNER_DATABASE_ID}
interval: 5s
timeout: 10s
retries: 10
start_period: 10s
# GoアプリケーションのAPIサーバ
api:
build:
context: ../../
dockerfile: ./build/packages/docker/Dockerfile.sample_app
ports:
- "8080:8080"
env_file:
- .env
tty: true
volumes:
- type: bind
source: ../../
target: /app/
depends_on:
spanner-emulator-healthcheck:
condition: service_healthy
ヘルスチェック用コンテナの削除
普通にdocker-compose up するとヘルスチェック用のコンテナが起動しっぱなしになる。
ヘルスチェック用コンテナは起動判断のみ必要なものであるため、
docker-compose up
を実行した後にdocker-compose rm -fsv spanner-emulator-healthcheck
を実行するようなスクリプトを組んでおくとより便利。
2.12.2. 起動中のコンテナへログインできるようにする
docker-compose.yml
のデバッグのために、コンテナ内へログインして色々コマンド実行したいときに一時的に記述することがある。
ENTRYPOINTやCMDで、コンテナが起動しっぱなしになるようなプロセスを起動している前提で、tty: true
を記述する。
2.12.3. docker-compose.yml 内で展開する環境変数を設定し、使用する
docker-compose.ymlが存在するディレクトリに、環境変数を列挙した.envを配置する。
この環境変数はコンテナ内には渡されない。
docker-compose.yml上で${環境変数}
を記述することで展開できる。
2.12.4. 各サービスのコンテナ向けの環境変数を設定する
環境変数を列挙したファイル(★)を作成し、docker-compose.yml
の各サービスのenv_file
パラメータで★のファイルを指定する。
2.12.4. DockerfileのCMDやENTRYPOINTをdocker-compose.ymlから上書きする
docker-compose.yml
内でcommand
やentorypoint
を指定すると上書きできる。
2.12.5. ホスト側のディレクトリ・ファイルをコンテナ内と同期する
ローカル開発環境を起動しつつ、ホスト側でのソースコードの変更を即コンテナ内へも反映したい時に。
バインドマウントの設定で可能。以下のように記述する。
volumes:
- type: bind
source: ../../
target: /app/
source
: バインドしたいホスト側のディレクトリを相対パスで指定する。基準ディレクトリはdocker-compose.ymlが配置されているディレクトリ。
target
: バインドされるコンテナ側のディレクトリを、コンテナ内の絶対パスで指定する。
コンテナ起動時のバインドマウントの適用タイミングは、先述の通り。
バインドマウント適用前に、コンテナ側のディレクトリ内部に存在していたファイル・ディレクトリは破棄されるような形になることに注意。
参考