自動で最新状態を維持! Docker Compose でコンテナのイメージを定期的に更新・再起動する方法

Pocket

本記事は、 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.19debian: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 でイメージに更新があったか条件分岐できることがわかった。

これをうまく使って自動でコンテナを更新し、快適で安全なコンテナライフを!

コメントを残す

メールアドレスが公開されることはありません。

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください