WatchtowerとDocker ComposeでHomeLabのDockerコンテナ更新を自動化する

HomeLab tutorial - IT technology blog
HomeLab tutorial - IT technology blog

問題:手動更新という隠れた時間泥棒

最初は何気ないことから始まる。Nextcloud、Nginx Proxy Manager、Vaultwardenといったいくつかのコンテナを立ち上げると、すべて順調に動く。そして3週間後、そのうちの1つにセキュリティパッチが来る。SSHで接続し、docker pullを実行し、コンテナを再起動し、ログを確認する。問題ない。

しかし3ヶ月後には、12個のコンテナが動いている。月次更新のもの、週次更新のもの、ほぼ毎日更新されるものもある。気づけば毎週末、イメージを最新に保つだけで1時間を費やしている。もはやHomeLabではなく、副業だ。

私もまさにこの状況に陥った。HomeLabは15個のサービスにまで膨らみ、更新作業が憂鬱になっていた。一度Vaultwardenのパッチを見逃したときは、すぐに居心地が悪くなった——CVEの議論中に同僚から「パッチ当てたバージョン使ってる?」と聞かれたのだ。あまり気持ちのいい瞬間ではなかった。

なぜこうなるのか:根本原因

Dockerイメージはデフォルトで自動更新されない。docker-compose up -dを実行すると、Dockerはローカルにキャッシュされているイメージをそのまま使用する。明示的にdocker-compose pullを実行し、その後docker-compose up -dを実行しない限り、コンテナは時間の中で凍結されたままだ。

典型的な手動ワークフローはこうなる:

# 手動更新の手順(つらい方法)
docker pull vaultwarden/server:latest
docker stop vaultwarden
docker rm vaultwarden
docker run -d --name vaultwarden ... vaultwarden/server:latest

それを12個のコンテナに掛け合わせてみよう——それぞれ異なるフラグ、ボリューム、ネットワーク設定がある。何かが実際に壊れるまで避け続ける理由がよくわかるはずだ。

3つのアプローチと、なぜほとんどの人が間違った選択をするのか

選択肢1:シンプルなCronジョブ

bashスクリプトでサービスをループしてdocker-compose pull && docker-compose up -dをスケジュール実行する人もいる。動くことは動くが、すぐに問題が露呈する:

  • 新しいイメージが実際に利用可能かどうかを認識しない
  • 通知がない——何が・いつ更新されたかわからない
  • 脆弱:1つの失敗したpullが複数のサービスの障害を引き起こす可能性がある
  • ロールバックの仕組みがない

選択肢2:Diun(Docker Image Update Notifier)

Diunは実行中のコンテナを監視し、レジストリに新しいイメージが登場すると通知を送る。ただし、それだけだ——自動的に何かを更新することはない。完全なコントロールが欲しい場合は最適だが、本当の自動化が欲しい場合はもどかしい。ほとんどのHomeLabの構成では、精密なツールを的外れな問題に適用しているようなものだ。

選択肢3:Watchtower(ここでの正解)

Watchtower自体がDockerコンテナとして実行される。他のコンテナを監視し、レジストリで新しいイメージを確認し、更新をpullしてコンテナを再起動する——すべてキーボードに触れることなく。既存のDocker Composeの構成と連携し、Slack、Email、Gotify、Telegram経由の通知もサポートしている。

HomeLabでの使用において、Watchtowerはバランスが取れている:セットアップが非常に簡単で、メンテナンスはほぼゼロ、実世界のエッジケースにも対応できる柔軟性がある。私は15個のサービスで6ヶ月以上運用してきた。アップストリームの破壊的変更によるコンテナの再起動が2回あったが、どちらも通知によってすぐに検出された。更新の見逃しはゼロだ。

Docker ComposeでWatchtowerをセットアップする

ステップ1:Watchtowerサービスを作成する

Watchtowerを独自のdocker-compose.ymlファイルに追加する。アプリケーションスタックとは分けて管理することで、個別に管理・更新しやすくなる。

# /opt/watchtower/docker-compose.yml
services:
  watchtower:
    image: containrrr/watchtower
    container_name: watchtower
    restart: unless-stopped
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - WATCHTOWER_CLEANUP=true
      - WATCHTOWER_POLL_INTERVAL=86400
      - WATCHTOWER_NOTIFICATIONS=slack
      - WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL=https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK
      - WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER=watchtower-homelab
      - TZ=Asia/Tokyo

コピペする前に理解しておくべき3つの環境変数がある:

  • WATCHTOWER_CLEANUP=true — 更新後に古いイメージレイヤーを削除する。これを省略すると、ディスクがダングリングイメージで埋まる。忙しいHomeLabでは月に2〜5GBほどになることも。
  • WATCHTOWER_POLL_INTERVAL=86400 — 24時間ごとに更新を確認する(値は秒単位)。ほとんどのHomeLabには日次が適切なペースだ。毎時チェックは過剰で、公開レジストリに不必要な負荷をかける。
  • TZ — ローカルのタイムゾーンに設定する。設定しないとログのタイムスタンプと通知時刻がUTCになり、ログを確認するたびに頭の中で時差計算が必要になる。

起動する:

cd /opt/watchtower
docker-compose up -d

ステップ2:自動更新させたくないコンテナを除外する

すべてのものを自動更新すべきではない。データベースが最もわかりやすい例だ——適切なマイグレーション計画なしにPostgreSQLやMariaDBのメジャーバージョンが上がると、データが静かに破損する可能性がある。Watchtowerにそれらを触らせてはいけない。

Watchtowerにスキップさせたいコンテナにラベルを付ける:

# アプリのdocker-compose.yml
services:
  postgres:
    image: postgres:15
    container_name: postgres
    labels:
      - "com.centurylinklabs.watchtower.enable=false"
    volumes:
      - postgres_data:/var/lib/postgresql/data

Watchtowerはこのラベルを読み取り、そのコンテナを完全に無視する。バージョンアップに手動の介入が必要なものすべてに適用しよう:データベース、メジャーフレームワークのバージョン、複雑なマイグレーション手順があるサービス。

ステップ3:重要なサービスにはオプトインモードを使う

より厳密なコントロールが欲しい場合は、Watchtowerをオプトインモードに切り替えよう。例外を除外しながらすべてを更新する代わりに、明示的に安全とマークしたコンテナのみを更新する。

# watchtower docker-compose.yml — オプトインモード
environment:
  - WATCHTOWER_LABEL_ENABLE=true   # ラベル付きコンテナのみ更新

自動更新させたい各コンテナには:

labels:
  - "com.centurylinklabs.watchtower.enable=true"

コントロールは増えるが、管理するラベルも増える。多くのサービスを運用するHomeLabでは、「不要なものを除外する」デフォルトのアプローチの方が摩擦が少ない場合が多い。オプトインは、本番環境に近いものを運用していて、自動更新される各サービスに明示的な承認が必要な場合に向いている。

ステップ4:通知を設定する

サイレントで運用するのは悪い習慣だ。Watchtowerが何かを更新してコンテナの再起動に失敗した場合、数日後ではなく数分以内に知りたい——火曜日からRSSリーダーが同期されていないことに気づいて初めてわかるのでは遅すぎる。

Telegram(ボット経由でスマートフォンに直接届く):

environment:
  - WATCHTOWER_NOTIFICATION_URL=telegram://BOT_TOKEN@telegram?chats=CHAT_ID

BOT_TOKEN@BotFatherから取得し、CHAT_IDはボットにメッセージを送ってからgetUpdates APIエンドポイントを確認することで取得できる。WatchtowerはTelegram向けにshoutrrr URL形式を使用している。

Gotify(セルフホストのプッシュ通知)を使う場合:

environment:
  - WATCHTOWER_NOTIFICATIONS=gotify
  - WATCHTOWER_NOTIFICATION_GOTIFY_URL=http://gotify.yourdomain.com
  - WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN=your_app_token

記録として残したいならメールも使える:

environment:
  - WATCHTOWER_NOTIFICATIONS=email
  - [email protected]
  - [email protected]
  - WATCHTOWER_NOTIFICATION_EMAIL_SERVER=smtp.yourdomain.com
  - WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT=587
  - WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER=smtp_user
  - WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD=smtp_password

ステップ5:手動で1回限りの更新を実行する

24時間では長すぎることもある。重大なCVEが公開されたばかりで、今すぐすべてにパッチを当てる必要がある場合だ。Watchtowerにはまさにこのための1回実行モードがある:

docker run --rm \
  -v /var/run/docker.sock:/var/run/docker.sock \
  containrrr/watchtower \
  --run-once \
  --cleanup

これにより新しいイメージをpullし、更新されたコンテナを再起動し、古いレイヤーをクリーンアップして、クリーンに終了する。実行中のWatchtowerインスタンスには影響しない。

現実的なHomeLab構成

Watchtowerを組み込んだ典型的な構成はこうなる——ほとんどのサービスで自動更新を有効にし、データベースは明示的に除外する:

# /opt/apps/docker-compose.yml
services:
  vaultwarden:
    image: vaultwarden/server:latest
    container_name: vaultwarden
    restart: unless-stopped
    # ラベル不要 — Watchtowerがデフォルトで更新する
    volumes:
      - vaultwarden_data:/data

  nextcloud:
    image: nextcloud:latest
    container_name: nextcloud
    restart: unless-stopped
    volumes:
      - nextcloud_data:/var/www/html

  nextcloud_db:
    image: mariadb:10.11
    container_name: nextcloud_db
    restart: unless-stopped
    labels:
      - "com.centurylinklabs.watchtower.enable=false"  # 自動更新禁止
    environment:
      - MYSQL_ROOT_PASSWORD=yourpassword
      - MYSQL_DATABASE=nextcloud
    volumes:
      - nextcloud_db_data:/var/lib/mysql

volumes:
  vaultwarden_data:
  nextcloud_data:
  nextcloud_db_data:

注意すべき点

Watchtowerは魔法ではない。実際の落とし穴がいくつかある:

  • latestタグのイメージのみ対応vaultwarden/server:1.30.0のようにバージョンをピン留めしている場合、Watchtowerは更新先がない。追跡しているタグのダイジェスト変更を検出することで機能するためだ。
  • マイナーリリースにおける破壊的変更 — よく管理されたプロジェクトでは稀だが、起こりうる。Nextcloudは特に、データベースのマイグレーションが必要なマイナーリリースがあった。データベースバックエンドを持つサービスなど、気にかけるサービスのチェンジログを必ず確認しよう。
  • ディスクスペース — 常にWATCHTOWER_CLEANUP=trueを設定すること。15個のコンテナが毎週更新されるHomeLabでは、クリーンアップしないイメージが1ヶ月以内に10GB以上を消費する可能性がある。
  • プライベートレジストリ — プライベートレジストリからpullするには認証情報が必要だ。Watchtowerは~/.docker/config.json(コンテナにマウントする)またはレジストリごとの明示的な環境変数をサポートしている。

動作確認する

Watchtowerのログを確認して、正常に実行されポーリングしていることを確かめる:

docker logs watchtower --tail 50 -f

正常な実行はこのようになる:

time="2026-03-30T09:00:00Z" level=info msg="Checking all containers (except explicitly disabled)"
time="2026-03-30T09:00:12Z" level=info msg="Found new vaultwarden/server:latest image"
time="2026-03-30T09:00:35Z" level=info msg="Stopping vaultwarden (abc123def456)"
time="2026-03-30T09:00:40Z" level=info msg="Creating vaultwarden"
time="2026-03-30T09:00:41Z" level=info msg="Session done" Found=1 Updated=1 Failed=0 Skipped=14 Scanned=15

注目すべきはFailed=0だ。通知を設定すれば、このサマリーが毎回のポーリングサイクルの後に自動的にTelegramや受信トレイに届く——SSHは不要だ。

Share: