tva
← コラム

本番稼働中のサーバーでDocker Engineをアップグレードする

100個を超えるコンテナを稼働させているサーバーでDocker Engineをアップグレードすることは、即興が通用しない作業です。Hetzner本番サーバーでDocker Engine 28.1から28.2、Docker Compose 2.35から2.36への移行する際、当社はデータベース移行と同じ厳密さで臨みました。用意したのは、文書化した準備チェックリスト、検証済みのロールバック手順、正確な実行順序、作業完了を宣言する前のアップグレード後の検証です。

以下では、各手順の理由、実際に実行したコマンド、コンテナ数が多い環境で再起動ポリシーの設定ミスが連鎖して復旧を困難にする場合の注意点を詳しく記録します。

このアップグレードに正式な手順書が必要だった理由

Docker Engine 28.2ではbridgeネットワークのオプションとコンテナへの直接ルーティング用のデーモンオプションが追加され、レガシーなschema 1イメージのpullへの対応が削除されました。同じメンテナンス時間帯に、Composeプラグインは2.35.1から2.36.0へ更新されました。マイナーリリースですが、スタックの起動方法に関わる変更はテストする価値があります。特に、condition: service_healthyを指定したdepends_onブロックは確認が必要です。

1つの docker-compose.yml がデータベース、リバースプロキシ、アプリケーションコンテナ、バックグラウンドワーカーなど、相互依存する12のサービスを管理するサーバーでは、依存関係の解決方法が通知なく変わると、その問題がすぐには表面化しないことがあります。それは03:00に、ヘルスチェックが一度も実行されず、依存するコンテナが無限ループするときに表面化します。

実務上必要なのは、慎重さを目的にすることではなく、事前準備です。

準備チェックリスト

パッケージに触れる前に、以下のチェックリストを確認しました。各項目には理由があります。

1. 実行中のすべてのコンテナとその再起動ポリシーを一覧化する。

docker ps --format "table {{.Names}}	{{.Status}}	{{.Image}}" | sort
docker inspect $(docker ps -q) --format '{{.Name}} restart={{.HostConfig.RestartPolicy.Name}}' | sort

これが比較の基準になります。restart: always のコンテナはDockerデーモン再起動後に自動的に再起動を試みます。これは望ましい動作ですが、元のComposeファイルとイメージが正常と確認できている場合に限ります。アップグレード時にクラッシュループしているコンテナは、その後もクラッシュループし続けます。その状態を事前に把握しておくべきです。

2. アップグレード前にすべてのComposeファイルを検証する。

docker compose config --quiet 2>&1 | grep -i warning

docker-compose.ymlを含むすべてのディレクトリでこれを実行します。現在インストールされているComposeを使うため、既存の警告は確認できますが、更新先のバージョンとの互換性を証明するものではありません。その確認には、ステージング環境で更新先のバージョンをテストしてください。現在のComposeでは、ファイル先頭の古いversion:キーは無視され、警告が出ます。また、同じネットワーク上のサービスは名前で互いに接続できるため、従来のlinks:ディレクティブは不要です。警告は後続のリリースでエラーになる可能性があるため、今のうちに解消する価値があります。

3. ディスクの空き容量を確認する。

df -h /var/lib/docker
docker system df

アップグレードは新しいcontainerdシムを取得しDocker Engineバイナリを置き換えます。古いバイナリとその依存関係は常に自動的にクリーンアップされるわけではありません。1年間稼働しているサーバーでは、docker system df で数ギガバイトの回収可能なイメージレイヤーが明らかになることがよくあります。アップグレード中ではなく、前にこれをクリーンアップします。

docker system prune -f --volumes

匿名ボリュームに必要なデータが含まれていないと確認できる場合のみ --volumes を使用します。当社のサーバーでは、すべての永続データは名前付きの明示的に宣言されたボリュームからマウントされているため、これは安全でした。

4. 現在のDocker EngineバージョンとComposeバージョンを参照用ファイルに出力する。

docker version > /root/docker-pre-upgrade.txt
docker compose version >> /root/docker-pre-upgrade.txt
docker ps -a >> /root/docker-pre-upgrade.txt
dpkg-query -W docker-ce docker-ce-cli containerd.io docker-compose-plugin >> /root/docker-pre-upgrade.txt

10秒で完了し、問題が発生してアップグレード時に稼働していたコンテナを特定する必要がある場合に、ロールバックの明確な参照情報になります。

5. 下流システムに通知する。

当社のサーバーはセルフホスト型のSupabaseスタックといくつかのn8nワークフロー自動化インスタンスを実行しています。コンテナ再起動サイクル中に実行されるWebhookは失敗し、送信側が再試行しなければ、誰も気づかないままイベントが失われる可能性があります。稼働状況の監視ツールでメンテナンス時間帯を設定し、期間中はn8nの受信Webhookを無効にしました。

ロールバック計画

ロールバック計画は、実行する条件を事前に決めてこそ役立ちます。当社の条件は単純です。アップグレード前に稼働していたコンテナがアップグレード完了から10分後も稼働しておらず、docker compose up -d で復旧できない場合、Docker Engineバイナリをロールバックします。

DebianベースのシステムでDocker Engineをロールバックするには、以前のパッケージバージョンを固定します。開始前に正確なバージョン文字列を記録しました:

apt-cache policy docker-ce docker-ce-cli containerd.io docker-compose-plugin

アップグレード前の当社のサーバーの出力:

docker-ce:
  Installed: 5:28.1.1-1~debian.12~bookworm
  Candidate: 5:28.2.2-1~debian.12~bookworm

docker-compose-plugin:
  Installed: 2.35.1-1~debian.12~bookworm
  Candidate: 2.36.0-1~debian.12~bookworm

必要な場合のロールバックコマンド:

CONTAINERD_PREV=$(awk '$1 == "containerd.io" {print $2}' /root/docker-pre-upgrade.txt)
apt-get install -y --allow-downgrades   docker-ce=5:28.1.1-1~debian.12~bookworm   docker-ce-cli=5:28.1.1-1~debian.12~bookworm   containerd.io="${CONTAINERD_PREV}"   docker-compose-plugin=2.35.1-1~debian.12~bookworm

このコマンドはcontainerd.ioも手順4で参照用ファイルに保存したバージョンに固定し、--allow-downgradesを指定しています。apt-get -yは、非対話モードでパッケージをダウングレードする代わりに処理を中断するためです。

このコマンドをサーバーの /root/docker-rollback.sh にテキストファイルとして実行権限付きで保管し、障害対応中でも入力せずに実行できる状態にしました。

アップグレードプロセス

準備が完了すれば、実際のアップグレードは簡単です。順序が重要です:まずパッケージインデックスを更新し、インストールされるものを確認し、それからインストールします。

ステップ1:Dockerリポジトリのみのaptインデックスを更新する。

apt-get update -o Dir::Etc::sourcelist="sources.list.d/docker.list"   -o Dir::Etc::sourceparts="-"   -o APT::Get::List-Cleanup="0"

これにより、すべての取得元を更新するのではなく、Dockerリポジトリのみに更新を制限します。本番サーバーでは、Dockerメンテナンス時間帯中の無関係なソースからの意図しないパッケージ更新は不必要な不確定要因になります。

ステップ2:候補バージョンを確認する。

apt-cache policy docker-ce docker-ce-cli containerd.io docker-compose-plugin

進める前に候補が期待したものと一致することを確認します。候補がステージング環境でテストしたバージョンより新しいバージョンを示している場合、一時停止して評価します。

ステップ3:インストールする。

apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

これはDockerデーモンを停止し、バイナリを置き換え、デーモンを再起動します。restart: always のコンテナはデーモン再起動時に起動されます。コンテナが停止している時間は、適度な規模のサーバーでは通常30秒未満です。ただし、containerdの初期化にかかる時間によって異なります。

別のターミナルで再起動を監視します:

watch -n 2 'docker ps --format "table {{.Names}}	{{.Status}}" | sort'

ステップ4:インストールされたバージョンを確認する。

docker version
docker compose version

当社のサーバーでは、出力から以下を確認できました:

Docker Engine: 28.2.2
Docker Compose: v2.36.0

Docker ComposeのバージョンはDocker Engineとは独立しています。docker-compose-pluginパッケージのバージョン(ここでは2.36.0)がComposeのバージョンであり、aptインデックスもこれを追跡します。

アップグレード後の検証

アップグレードから10分後、項目を定めた検証を実施しました。これは必須です。デーモンの再起動でコンテナは起動しますが、ヘルスチェックに依存するコンテナが実際にチェックに合格したことは保証しません。

すべてのコンテナが期待された状態にあることを確認する。

docker ps -a --format "table {{.Names}}	{{.Status}}	{{.RunningFor}}" | sort

Restarting または Exited を示すコンテナは即座に調査が必要です。当社のサーバーでは、Supabaseのanalyticsサービスのコンテナ1つが Restarting (1) を示しました。アップグレード前にも低頻度のクラッシュループ状態でした。アップグレードが原因ではありませんでしたが、アップグレード後の確認でより明確に表面化しました。これはまさにアップグレード前の一覧化で把握すべき既存の問題です。

ヘルスチェックに合格していることを確認する。

docker inspect $(docker ps -q)   --format '{{.Name}} health={{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}'   | grep -v "health=none" | sort

明示的なヘルスチェックのないコンテナにはヘルスステータスがありません。Dockerのバージョンによっては、確認なしの{{.State.Health.Status}}は<no value>を表示するか、テンプレートエラーでコマンド全体を中断します。そのためテンプレートは先に.State.Healthの有無を確認し、grepでヘルスチェックのないコンテナを除外しています。重要なのは、他のコンテナがcondition: service_healthyを通じて依存しているものです。

すべてのスタックでCompose設定の検証を実行する。

for dir in /opt/*/; do
  if [ -f "${dir}docker-compose.yml" ]; then
    echo "--- $dir ---"
    docker compose -f "${dir}docker-compose.yml" config --quiet 2>&1
  fi
done

これにより、新しいバージョンのComposeが警告またはエラーとして扱うComposeファイルのディレクティブを検出します。当社のサーバーでは、3つのスタックが version キーの非推奨警告を発しました。これらは動作には影響しない警告ですが、次のメンテナンスサイクルで解決する価値があります。

Dockerデーモンログのエラーを確認する。

journalctl -u docker --since "1 hour ago" | grep -i -E "error|warn|fatal"

クリーンなアップグレードではデーモンログにエラーは発生しません。非推奨設定に関する警告は記録する価値がありますが、すぐに対応する必要はありません。

コンテナ間のネットワーク接続を確認する。

docker network ls
docker network inspect bridge --format '{{range .Containers}}{{.Name}} {{end}}'

2つ目のコマンドが表示するのは、デフォルトのbridgeネットワークだけです。docker network lsに表示される各カスタムネットワークでも同じコマンドを実行し、デーモン再起動後もカスタムブリッジネットワークに想定したコンテナが接続されていることを確認してください。さらに、依存関係のあるサービス間で少なくとも1つの実際の接続をテストしてください。

実施して分かったこと

アップグレードは2分以内に完了しました。アップグレード前に実行していたすべてのコンテナがその後も実行されていました。Composeの変更で最もすぐに気づいたのは、--dry-run 出力形式がより見やすくなったことです。新しいバージョンは構造化されたカラー出力を生成し、docker compose up が何を行うかを実行前に確認しやすくなっています。

depends_on のヘルスチェック順序は当社のスタックでCompose 2.35と同一に動作しました。事前にComposeファイルを検証していたので予想通りでしたが、確認できて安心しました。

1つの重要な発見がありました。コンテナ名は一意でなければならないため、Composeではcontainer_nameフィールドとscaleを併用できません。当社には、適切なレプリカ構成へ移行する前の設定が残り、両方を使っているスタックが1つありました。Composeはそのスタックの docker compose up で明確なエラーを発しました。修正は冗長な container_name フィールドを削除する1行の編集でした。

関連記事

関連記事