本記事は、 Docker/コンテナ仮想環境 Advent Calendar 2024 8日目の記事だ。
「Docker は開発向けの環境だ、運用するなら K8s などのコンテナオーケストレーションツールを使え!」…とひとは言う。
しかし、 DB やアプリ等々の複数のコンテナをひとつのマシンで動かすなら、 Docker Compose でビルドまで担わせてしまう手軽さと手っ取り早さはなかなか捨てがたい。
趣味の範囲や、チームの範囲でしか使わず、そんながっつり作り込んだシステムでも無ければ、ベースイメージが更新される度に自動的にコンテナを差し替えてしまいたくなる。
そんな状況で、実行しているイメージやビルドするベースイメージに更新があった場合のみ、自動でコンテナを Recreate して再アップさせる cron ジョブを考えてみる。
Compose で定期 pull とビルドしてイメージの更新があったら再 up する cron コマンド
cd /path/to/dirname/; { docker compose --ansi never pull --no-parallel; docker compose --ansi never --progress=plain build --pull; } 2>&1 | bash -c "tee >( logger -i -t user/docker-auto-rebuild -p user.info )" | grep -E '\s+Pull complete\s*$|^#[0-9]+\s+resolve\s+' && docker compose --ansi never up -d 2>&1 | logger -i -t user/docker-auto-recreate -p user.info
こんなコマンドを、 cron に登録してやろう。
動作確認したバージョンは、 Docker Engine v27.3.1, Docker Compose vv2.29.7 だ。
$ docker version --format "Client: {{.Client.Version}}, Server: {{.Server.Version}}"
Client: 27.3.1, Server: 27.3.1
$ docker compose version
Docker Compose version v2.29.7
実行ログは journalctl
を使って以下のように実行したら見れるぞ。
$ journalctl -t user/docker-auto-rebuild -t user/docker-auto-recreate --since "1day ago"
解説
サンプル Docker Compose
以下のような感じで、 alpine:latest
(alpine:3.21
) と python:3-bullseye
をそれぞれそのまま使うコンテナ2つと、 alpine:3.19
と debian:bookworm
をそれぞれ元に Dockerfile でビルドして使うコンテナ2つの、合計4つを compose.yaml
で用意する。
$ mkdir dirname dirname/build-alpine dirname/build-bookworm
$ cd dirname
$ cat <<'EOF' > build-alpine/Dockerfile
ARG ALPINE_VERSION
FROM alpine:${ALPINE_VERSION}
RUN apk add --no-cache openssh-server openjdk17-jre-headless mysql-client
EOF
$ cat <<'EOF' > build-bookworm/Dockerfile
ARG DEBIAN_VERSION
FROM debian:${DEBIAN_VERSION}
RUN apt-get update && apt-get install -y --no-install-recommends netcat-openbsd openssh-server openjdk-17-jre-headless mariadb-client && rm -rf /var/lib/apt/lists/*
$ cat <<'EOF' > compose.yaml
x-vars:
nc-ash-command: &nc-ash-command |
ash -c 'function hoge(){ echo -e "HTTP/1.0 200 OK\n\n$(cat /etc/os-release)"; }; while true; do hoge | nc -lp $$NCPORT; done'
nc-bash-command: &nc-bash-command |
bash -c 'function hoge(){ echo -e "HTTP/1.0 200 OK\n\n$(cat /etc/os-release)"; }; while true; do hoge | nc -l $$NCPORT -q 1; done'
py-command: &py-command |
python -c "import os; import types; from subprocess import Popen, PIPE; from http.server import HTTPServer;from http.server import BaseHTTPRequestHandler; CustomHandler=types.new_class('CustomHandler',bases=(BaseHTTPRequestHandler,),exec_body=lambda CustomHandler: CustomHandler.update({'do_GET': lambda self: (self.send_response(200), self.end_headers(), self.wfile.write(Popen(['cat', '/etc/os-release'], stdout=PIPE).stdout.read()))})); HTTPServer(('', int(os.getenv('PYPORT'))), CustomHandler).serve_forever()"
services:
pureimage-alpine:
image: alpine:latest
ports:
- "4000:4000"
environment:
NCPORT: "4000"
command: *nc-ash-command
buildimage-alpine:
build:
context: ./build-alpine
args:
ALPINE_VERSION: '3.19'
ports:
- "4001:4001"
environment:
NCPORT: "4001"
command: *nc-ash-command
pureimage-bullseye:
image: python:3-bullseye
ports:
- "4002:4002"
environment:
PYPORT: "4002"
command: *py-command
buildimage-bookworm:
build:
context: ./build-bookworm
args:
DEBIAN_VERSION: 'bookworm'
ports:
- "4003:4003"
environment:
NCPORT: "4003"
command: *nc-bash-command
EOF
$
各コンテナに引数に色々ごちゃごちゃ与えているが、 http で cat /etc/os-release
の内容を返すプロセスを nc
コマンド等で動かしているだけだ。
(Debian bullseye のコンテナに nc
コマンドがない為、 Docker Hub イメージをそのまま使うコンテナだけ nc
じゃなくて python
で同等の機能を実現している。)
docker compose up
して、各コンテナが動いていることを確認する。
$ docker compose up -d
[+] Running 4/0
✔ Container dirname-buildimage-bookworm-1 Started 0.0s
✔ Container dirname-buildimage-alpine-1 Started 0.0s
✔ Container dirname-pureimage-bullseye-1 Started 0.0s
✔ Container dirname-pureimage-alpine-1 Started 0.0s
$
$ for p in {4000..4003}; do curl http://localhost:$p/; done
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.21.0
PRETTY_NAME="Alpine Linux v3.21"
HOME_URL="https://alpinelinux.org/"
BUG_REPORT_URL="https://gitlab.alpinelinux.org/alpine/aports/-/issues"
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.19.4
PRETTY_NAME="Alpine Linux v3.19"
HOME_URL="https://alpinelinux.org/"
BUG_REPORT_URL="https://gitlab.alpinelinux.org/alpine/aports/-/issues"
PRETTY_NAME="Debian GNU/Linux 11 (bullseye)"
NAME="Debian GNU/Linux"
VERSION_ID="11"
VERSION="11 (bullseye)"
VERSION_CODENAME=bullseye
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
NAME="Debian GNU/Linux"
VERSION_ID="12"
VERSION="12 (bookworm)"
VERSION_CODENAME=bookworm
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"
$
この状態で、 compose.yaml
が直接参照するイメージを更新する docker compose pull
と、 Dockerfile
のをベースイメージを更新してビルドする docker compose build --pull
をそれぞれ実行してみる。
出力をパイプ処理しやすいように、 compose
コマンドには --ansi never --progress=plain
オプションをつける。
compose pull 実行時の出力
まず、 docker compose pull
の場合。
以下のように、 イメージの更新の有無にかかわらず <イメージ名> Pulled
の文字が出力されるが、イメージの更新があった場合のみ Pull complete
が出てくることがわかる。
compose で直接参照しているイメージに更新がある場合:
$ docker compose --ansi never pull --no-parallel;
buildimage-alpine Skipped - No image to be pulled
buildimage-bookworm Skipped - No image to be pulled
pureimage-bullseye Pulling
pureimage-alpine Pulling
38a8310d387e Pulling fs layer
cd606f6f489e Pulling fs layer
925257a7168e Pulling fs layer
dcb34ce34679 Pulling fs layer
f3364194d183 Pulling fs layer
7fb47e2b8f77 Pulling fs layer
4d7ffda1f3b2 Pulling fs layer
965a37552948 Pulling fs layer
7fb47e2b8f77 Waiting
dcb34ce34679 Waiting
4d7ffda1f3b2 Waiting
f3364194d183 Waiting
965a37552948 Waiting
38a8310d387e Downloading 1.854MB/3.644MB
38a8310d387e Download complete
38a8310d387e Extracting 786.4kB/3.644MB
38a8310d387e Extracting 3.644MB/3.644MB
38a8310d387e Pull complete
pureimage-alpine Pulled
cd606f6f489e Downloading 531.7kB/53.74MB
cd606f6f489e Downloading 53.74MB/53.74MB
cd606f6f489e Verifying Checksum
cd606f6f489e Download complete
925257a7168e Downloading 156.4kB/15.56MB
925257a7168e Downloading 13.86MB/15.56MB
925257a7168e Verifying Checksum
925257a7168e Download complete
dcb34ce34679 Downloading 527.6kB/54.75MB
dcb34ce34679 Downloading 51.12MB/54.75MB
dcb34ce34679 Verifying Checksum
dcb34ce34679 Download complete
f3364194d183 Downloading 527.6kB/197.1MB
f3364194d183 Downloading 195MB/197.1MB
f3364194d183 Verifying Checksum
f3364194d183 Download complete
7fb47e2b8f77 Downloading 63.13kB/6.052MB
7fb47e2b8f77 Downloading 3.132MB/6.052MB
7fb47e2b8f77 Verifying Checksum
7fb47e2b8f77 Download complete
4d7ffda1f3b2 Downloading 281.8kB/27.75MB
4d7ffda1f3b2 Downloading 25.77MB/27.75MB
4d7ffda1f3b2 Verifying Checksum
4d7ffda1f3b2 Download complete
965a37552948 Downloading 249B/249B
965a37552948 Download complete
cd606f6f489e Extracting 557.1kB/53.74MB
cd606f6f489e Extracting 53.74MB/53.74MB
cd606f6f489e Pull complete
925257a7168e Extracting 163.8kB/15.56MB
925257a7168e Extracting 15.56MB/15.56MB
925257a7168e Pull complete
dcb34ce34679 Extracting 557.1kB/54.75MB
dcb34ce34679 Extracting 54.75MB/54.75MB
dcb34ce34679 Pull complete
f3364194d183 Extracting 557.1kB/197.1MB
f3364194d183 Extracting 197.1MB/197.1MB
f3364194d183 Pull complete
7fb47e2b8f77 Extracting 65.54kB/6.052MB
7fb47e2b8f77 Extracting 6.052MB/6.052MB
7fb47e2b8f77 Pull complete
4d7ffda1f3b2 Extracting 294.9kB/27.75MB
4d7ffda1f3b2 Extracting 27.75MB/27.75MB
4d7ffda1f3b2 Pull complete
965a37552948 Extracting 249B/249B
965a37552948 Extracting 249B/249B
965a37552948 Pull complete
pureimage-bullseye Pulled
$
compose で直接参照しているイメージに更新がない場合:
$ docker compose --ansi never pull --no-parallel;
buildimage-bookworm Skipped - No image to be pulled
pureimage-bullseye Pulling
buildimage-alpine Skipped - No image to be pulled
pureimage-alpine Pulling
pureimage-alpine Pulled
pureimage-bullseye Pulled
compose build 実行時の出力
次に docker compose build
の場合。
以下のように、ベースイメージに更新があった場合のみ #<数字> resolve <リポジトリの取得元>
の文字列が出てくることがわかる。
Dockerfile のベースイメージに更新がある場合(buildimage-bookworm
のほうのみ):
$ docker compose --ansi never --progress=plain build --pull;
#0 building with "default" instance using docker driver
#1 [buildimage-bookworm internal] load build definition from Dockerfile
#1 transferring dockerfile: 253B done
#1 WARN: InvalidDefaultArgInFrom: Default value for ARG debian:${DEBIAN_VERSION} results in empty or invalid base image name (line 2)
#1 DONE 0.0s
#2 [buildimage-alpine internal] load build definition from Dockerfile
#2 transferring dockerfile: 160B done
#2 WARN: InvalidDefaultArgInFrom: Default value for ARG alpine:${ALPINE_VERSION} results in empty or invalid base image name (line 2)
#2 DONE 0.1s
#3 [buildimage-alpine internal] load metadata for docker.io/library/alpine:3.19
#3 DONE 1.7s
#4 [buildimage-bookworm internal] load metadata for docker.io/library/debian:bookworm
#4 ...
#5 [buildimage-alpine internal] load .dockerignore
#5 transferring context: 2B done
#5 DONE 0.0s
#6 [buildimage-alpine 1/2] FROM docker.io/library/alpine:3.19@sha256:7a85bf5dc56c949be827f84f9185161265c58f589bb8b2a6b6bb6d3076c1be21
#6 DONE 0.0s
#7 [buildimage-alpine 2/2] RUN apk add --no-cache openssh-server openjdk17-jre-headless mysql-client
#7 CACHED
#8 [buildimage-alpine] exporting to image
#8 exporting layers done
#8 writing image sha256:5f5f5dd8bef742c8b13a27ac3fb80a668a4a35ada05281be1f0a65d03c97e72f done
#8 naming to docker.io/library/dirname-buildimage-alpine done
#8 DONE 0.0s
#9 [buildimage-alpine] resolving provenance for metadata file
#9 DONE 0.0s
#4 [buildimage-bookworm internal] load metadata for docker.io/library/debian:bookworm
#4 DONE 2.2s
#10 [buildimage-bookworm internal] load .dockerignore
#10 transferring context: 2B done
#10 DONE 0.0s
#11 [buildimage-bookworm 1/2] FROM docker.io/library/debian:bookworm@sha256:17122fe3d66916e55c0cbd5bbf54bb3f87b3582f4d86a755a0fd3498d360f91b
#11 resolve docker.io/library/debian:bookworm@sha256:17122fe3d66916e55c0cbd5bbf54bb3f87b3582f4d86a755a0fd3498d360f91b 0.0s done
#11 sha256:17122fe3d66916e55c0cbd5bbf54bb3f87b3582f4d86a755a0fd3498d360f91b 8.52kB / 8.52kB done
#11 sha256:ec54b6327d5099ab29b38d70f7290e42d8769ef676fc262b34a18b688104f61b 1.02kB / 1.02kB done
#11 sha256:ff869c3288a47c9625a60473a3d5108ec45bd095a00e23568a82ee8b95d12954 453B / 453B done
#11 sha256:fdf894e782a221820acf469d425b802be26aedb5e5d26ea80a650ff6a974d488 0B / 48.50MB 0.1s
#11 sha256:fdf894e782a221820acf469d425b802be26aedb5e5d26ea80a650ff6a974d488 48.50MB / 48.50MB 1.2s done
#11 extracting sha256:fdf894e782a221820acf469d425b802be26aedb5e5d26ea80a650ff6a974d488 6.9s done
#11 DONE 8.6s
#12 [buildimage-bookworm 2/2] RUN apt-get update && apt-get install -y --no-install-recommends netcat-openbsd openssh-server openjdk-17-jre-headless mariadb-client && rm -rf /var/lib/apt/lists/*
#12 0.402 Get:1 http://deb.debian.org/debian bookworm InRelease [151 kB]
#12 0.460 Get:2 http://deb.debian.org/debian bookworm-updates InRelease [55.4 kB]
#12 0.467 Get:3 http://deb.debian.org/debian-security bookworm-security InRelease [48.0 kB]
#12 0.604 Get:4 http://deb.debian.org/debian bookworm/main amd64 Packages [8789 kB]
#12 0.843 Get:5 http://deb.debian.org/debian bookworm-updates/main amd64 Packages [2712 B]
#12 0.916 Get:6 http://deb.debian.org/debian-security bookworm-security/main amd64 Packages [214 kB]
#12 2.342 Fetched 9260 kB in 2s (4668 kB/s)
#12 2.342 Reading package lists...
#12 4.192 Building dependency tree...
#12 4.435 Reading state information...
#12 4.648 The following additional packages will be installed:
#12 4.648 ...
#12 4.653 Suggested packages:
#12 4.653 ...
#12 4.653 Recommended packages:
#12 4.653 ...
#12 5.171 The following NEW packages will be installed:
#12 5.171 ...
#12 5.366 0 upgraded, 64 newly installed, 0 to remove and 0 not upgraded.
#12 5.366 Need to get 72.5 MB of archives.
#12 5.366 After this operation, 370 MB of additional disk space will be used.
#12 5.366 Get:1 http://deb.debian.org/debian bookworm/main amd64 perl-modules-5.36 all 5.36.0-7+deb12u1 [2815 kB]
...
#12 7.746 Get:64 http://deb.debian.org/debian bookworm/main amd64 openjdk-17-jre-headless amd64 17.0.13+11-2~deb12u1 [43.8 MB]
#12 11.60 debconf: delaying package configuration, since apt-utils is not installed
#12 11.64 Fetched 72.5 MB in 6s (11.8 MB/s)
#12 11.68 Selecting previously unselected package perl-modules-5.36.
(Reading database ... 6089 files and directories currently installed.)
#12 11.70 Preparing to unpack .../00-perl-modules-5.36_5.36.0-7+deb12u1_all.deb ...
#12 11.71 Unpacking perl-modules-5.36 (5.36.0-7+deb12u1) ...
...
#12 35.68 done.
#12 DONE 35.8s
#13 [buildimage-bookworm] exporting to image
#13 exporting layers
#13 exporting layers 2.2s done
#13 writing image sha256:4bf8dd00e55b32d43358346a3bcbb8aca8d71d929d111e02439d642a3486cac1 done
#13 naming to docker.io/library/dirname-buildimage-bookworm 0.0s done
#13 DONE 2.3s
#14 [buildimage-bookworm] resolving provenance for metadata file
#14 DONE 0.0s
$
Dockerfile のベースイメージに更新がある場合:
$ docker compose --ansi never --progress=plain build --pull;
#0 building with "default" instance using docker driver
#1 [buildimage-bookworm internal] load build definition from Dockerfile
#1 transferring dockerfile: 253B done
#1 WARN: InvalidDefaultArgInFrom: Default value for ARG debian:${DEBIAN_VERSION} results in empty or invalid base image name (line 2)
#1 DONE 0.0s
#2 [buildimage-alpine internal] load build definition from Dockerfile
#2 transferring dockerfile: 160B done
#2 WARN: InvalidDefaultArgInFrom: Default value for ARG alpine:${ALPINE_VERSION} results in empty or invalid base image name (line 2)
#2 DONE 0.0s
#3 [buildimage-alpine internal] load metadata for docker.io/library/alpine:3.19
#3 DONE 0.6s
#4 [buildimage-bookworm internal] load metadata for docker.io/library/debian:bookworm
#4 DONE 0.6s
#5 [buildimage-alpine internal] load .dockerignore
#5 transferring context: 2B done
#5 DONE 0.0s
#6 [buildimage-bookworm internal] load .dockerignore
#6 transferring context: 2B done
#6 DONE 0.0s
#7 [buildimage-alpine 1/2] FROM docker.io/library/alpine:3.19@sha256:7a85bf5dc56c949be827f84f9185161265c58f589bb8b2a6b6bb6d3076c1be21
#7 DONE 0.0s
#8 [buildimage-alpine 2/2] RUN apk add --no-cache openssh-server openjdk17-jre-headless mysql-client
#8 CACHED
#9 [buildimage-bookworm 1/2] FROM docker.io/library/debian:bookworm@sha256:17122fe3d66916e55c0cbd5bbf54bb3f87b3582f4d86a755a0fd3498d360f91b
#9 DONE 0.0s
#10 [buildimage-bookworm 2/2] RUN apt-get update && apt-get install -y --no-install-recommends netcat-openbsd openssh-server openjdk-17-jre-headless mariadb-client && rm -rf /var/lib/apt/lists/*
#10 CACHED
#11 [buildimage-alpine] exporting to image
#11 exporting layers done
#11 writing image sha256:5f5f5dd8bef742c8b13a27ac3fb80a668a4a35ada05281be1f0a65d03c97e72f done
#11 naming to docker.io/library/dirname-buildimage-alpine 0.0s done
#11 DONE 0.0s
#12 [buildimage-bookworm] exporting to image
#12 exporting layers done
#12 writing image sha256:80a58fd4eec0df83dab603e18ad61772cb348b5b4dd39fa012fe2ea5250b8757 0.0s done
#12 naming to docker.io/library/dirname-buildimage-bookworm done
#12 DONE 0.0s
#13 [buildimage-alpine] resolving provenance for metadata file
#13 DONE 0.0s
#14 [buildimage-bookworm] resolving provenance for metadata file
#14 DONE 0.0s
grep で条件分岐
上記ような違いを元に、いずれかのイメージ更新があったことを grep -E '\s+Pull complete\s*$|^#[0-9]+\s+resolve\s+'
で検出し、ヒットしたら docker compose up -d
でコンテナの再作成を行っているのが、 cron コマンドの正体だ。
途中、 docker compose
の出力を tee
やパイプで logger
に渡して、 journalctl
ログを見れるようにもしている。
コンテナに更新がない場合の挙動
但し、他のイメージに更新があって docker compose up
が実行されたら、 たとえ Dockerfile
にベースイメージに更新がなくてキャッシュが効いていたとしても、コンテナは再起動される。
上記の例だと buildimage-alpine
のイメージは更新かかっていないが、 docker compose up
のログを確認すると以下のようにコンテナの再起動がかかっていることがわかる。
$ journalctl -t user/docker-auto-recreate --since "1day ago"
user/docker-auto-recreate[5684]: Container dirname-buildimage-alpine-1 Created
user/docker-auto-recreate[5684]: Container dirname-pureimage-alpine-1 Recreate
user/docker-auto-recreate[5684]: Container dirname-buildimage-bookworm-1 Recreate
user/docker-auto-recreate[5684]: Container dirname-pureimage-bullseye-1 Recreate
user/docker-auto-recreate[5684]: Container dirname-pureimage-alpine-1 Recreated
user/docker-auto-recreate[5684]: Container dirname-buildimage-bookworm-1 Recreated
user/docker-auto-recreate[5684]: Container dirname-pureimage-bullseye-1 Recreated
user/docker-auto-recreate[5684]: Container dirname-buildimage-alpine-1 Starting
user/docker-auto-recreate[5684]: Container dirname-pureimage-bullseye-1 Starting
user/docker-auto-recreate[5684]: Container dirname-buildimage-bookworm-1 Starting
user/docker-auto-recreate[5684]: Container dirname-pureimage-alpine-1 Starting
user/docker-auto-recreate[5684]: Container dirname-pureimage-alpine-1 Started
user/docker-auto-recreate[5684]: Container dirname-buildimage-alpine-1 Started
user/docker-auto-recreate[5684]: Container dirname-pureimage-bullseye-1 Started
user/docker-auto-recreate[5684]: Container dirname-buildimage-bookworm-1 Started
一方、 compose.yaml
が直接参照するイメージのほうは、更新がなかったら docker compose up
が実行されてもコンテナの再起動はかからない。
まとめ
いかがだろうか?
docker compose
も --ansi
や --progress
オプションをつけておけば、 grep でイメージに更新があったか条件分岐できることがわかった。
これをうまく使って自動でコンテナを更新し、快適で安全なコンテナライフを!