本文へ移動
購読
GUIDE#devtools更新 2026.09.26

docker compose restartの使い分けとRestartingループの直し方

Restarting (1)・(127)・(137)・(0)を終了コードとOOMKilledで切り分けて直す手順と、restartとup -dの違い、再起動ポリシーの実測比較。

level
中級
time
約45分
stack
Ubuntu 26.04.1 LTS, Docker Engine 29.1.3(docker.io 29.1.3-0ubuntu4.1), Docker Compose 2.40.3(docker-compose-v2 2.40.3+ds1-0ubuntu1), docker:29.1.3-dind(Engine 29.1.3, Compose v5.0.0), python:3.12-slim(Python 3.12.14), alpine:3(3.24.2, BusyBox 1.37.0)
author
@ryoma_t
// 前提条件
Docker EngineとCompose v2が入ったLinuxホストで、sudoなしでdockerコマンドを実行できること / compose.yamlの基本(services・image・ports・volumes)が読めること
docker compose up -dの直後に4つのサービスがRestarting (137)・(0)・(1)・(127)を繰り返し、docker inspectで終了コード・OOMKilled・再起動回数を並べた画面
画像:nagoya.dev(実際の画面・実行結果をもとに作成)

docker compose restartとは、Composeが作成済みのコンテナを止めてから、同じコンテナのまま起動し直すコマンドです。compose.yamlや.envを書き換えてもその変更は入らず、設定を反映させるにはコンテナを作り直すdocker compose up -dを使います。

コンテナが「Restarting」のまま起動しない、restartしたのに直らない、というときに原因を自分で絞り込むための手順です。検証にはDocker Engine 29.1.3とCompose 2.40.3を使い、終了コード1・127・137・0の4種類の再起動ループをわざと起こして、docker compose ps・logs・docker inspect・docker eventsの出力から原因を特定して直すまでを、実行結果のまま載せています。再起動ポリシー4種類の挙動は、手動停止やデーモン再起動まで計測して1枚の表にまとめました。

この記事でわかること

  • restartはコンテナIDも設定も変えず、up -dは設定のハッシュが変わったときだけ作り直すという仕組みの違い
  • Restarting (1)・(127)・(137)・(0)の意味と、OOMKilledやログと組み合わせた切り分け表
  • 再起動の待ち時間が0.18秒から倍々に伸びて約60秒で頭打ちになる実測と、10秒以上動くとリセットされる挙動
  • no・on-failure・always・unless-stoppedを、手動停止・デーモン再起動・デーモン強制終了で比べた結果
  • ループを止めて中を調べる方法(stop・docker update・compose run)と、戻し忘れの落とし穴

Restartingループを4種類再現する

検証環境はUbuntu 26.04.1 LTSのdocker.io(Docker Engine 29.1.3)とdocker-compose-v2(Compose 2.40.3)です。再起動ポリシーの判定と再起動の待ち時間はdockerd側の処理で、Composeはrestart:の値をコンテナ作成時に設定として渡すだけです。

terminal
$ docker version --format "Engine {{.Server.Version}}"
Engine 29.1.3
$ docker compose version
Docker Compose version 2.40.3+ds1-0ubuntu1

壊れ方の違う4つのサービスを1つのプロジェクトに入れます。作業ディレクトリに、Webサーバーの待ち受けアドレスを書いた.envと、配信用のindex.htmlを作ります。

terminal
mkdir -p ~/restart-lab && cd ~/restart-lab
printf 'WEB_BIND=192.168.1.10\n' > .env && echo v1 > index.html

次のcompose.yamlを置きます。コメントに書いた終了コードで落ちるよう、サービスごとに1か所ずつ間違えてあります。

compose.yaml
name: docker-compose-restart-loop-troubleshooting

services:
  web:        # .env の値が不正 → exit 1
    image: python:3.12-slim
    command: ["python", "-m", "http.server", "8080", "--bind", "${WEB_BIND}", "--directory", "/srv"]
    ports:
      - "127.0.0.1:18080:8080"
    volumes:
      - ./index.html:/srv/index.html:ro
    restart: unless-stopped

  worker:     # イメージにないコマンド → exit 127
    image: alpine:3
    command: ["sh", "-c", "set -e; while true; do curl -fsS -o /dev/null http://web:8080/; sleep 30; done"]
    restart: unless-stopped

  batch:      # メモリ上限超え → exit 137
    image: python:3.12-slim
    command: ["python", "-c", "data = bytearray(64 * 1024 * 1024); print('loaded', len(data))"]
    mem_limit: 32m
    memswap_limit: 32m
    restart: on-failure

  cron:       # すぐ終了するプロセス → exit 0
    image: alpine:3
    command: ["crond"]
    restart: unless-stopped

name:はプロジェクト名で、省略するとcompose.yamlのあるディレクトリ名が使われます。コンテナ名は「プロジェクト名-サービス名-連番」になるので、既存の環境と重ならない名前を固定しました。webの192.168.1.10は、ホストのLAN側アドレスを待ち受けに書いてしまった想定の値です。batchは64MiBのバイト列を作る処理にmem_limit: 32mを付け、memswap_limitを同じ値にしてスワップを使えなくしています。一度きりの処理なので、batchだけは失敗したときにだけ再起動するon-failureです。ポートは127.0.0.1にだけ公開しています。

起動すると、4つともStartedと表示されて終了コード0で戻ります。

terminal(抜粋)
$ docker compose up -d
 Network docker-compose-restart-loop-troubleshooting_default  Created
 Container docker-compose-restart-loop-troubleshooting-worker-1  Started
 Container docker-compose-restart-loop-troubleshooting-web-1  Started
 Container docker-compose-restart-loop-troubleshooting-cron-1  Started
 Container docker-compose-restart-loop-troubleshooting-batch-1  Started

少し待ってから状態を見ます。--formatでサービス名と状態だけに絞ると、長いコンテナ名に邪魔されずに読めます。

terminal
$ docker compose ps -a --format 'table {{.Service}}\t{{.Status}}'
SERVICE   STATUS
batch     Restarting (137) 7 seconds ago
cron      Restarting (0) 7 seconds ago
web       Restarting (1) 6 seconds ago
worker    Restarting (127) 7 seconds ago

Restarting (137)は「直前に終了コード137で終わり、次の起動を待っている」状態です。-aを付けないdocker compose psでも、Restartingのコンテナは同じように表示されました。4つとも同じRestartingなのに括弧の中の数字はすべて違い、この数字が切り分けの出発点になります。

docker compose up -dはコンテナを起動した時点で終わり、中のプロセスがその直後に落ちても終了コードは0のままです。デプロイ用のスクリプトやCIでup -dの成否だけを見ていると、上の状態でも成功として記録されます。up -dの後にdocker compose psを実行し、STATUSにRestartingやExitedが無いことを確かめる行を足しておきます。

終了コードとOOMKilledで原因を切り分ける

psの括弧は最後の1回分しか示しません。docker inspectで終了コード(ExitCode)、メモリ不足でカーネルに止められたか(OOMKilled)、再起動回数(RestartCount)を1行ずつ並べます。docker compose ps -aqはプロジェクト内の全コンテナのIDを返します。

terminal
$ docker inspect --format '{{index .Config.Labels "com.docker.compose.service"}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} restarts={{.RestartCount}}' $(docker compose ps -aq)
batch exit=137 oom=true restarts=9
cron exit=0 oom=false restarts=9
web exit=1 oom=false restarts=9
worker exit=127 oom=false restarts=9

ループ中のコンテナは、起動してから次に落ちるまでの短い間だけ、ExitCodeが0に、OOMKilledがfalseに戻ります。検証でもタイミングによってはbatchがexit=0 oom=falseと表示されたので、psのSTATUSがRestartingになっているサービスについて読むか、もう一度実行して確かめます。

ログも合わせて見ます。--tail 2で各サービスの最後の2行に絞りました。batchとcronは何も出力しません。

terminal
$ docker compose logs --tail 2 web worker
web-1  |     self.socket.bind(self.server_address)
web-1  | OSError: [Errno 99] Cannot assign requested address
worker-1  | sh: curl: not found
worker-1  | sh: curl: not found
$ docker compose logs batch cron

webは、Pythonのトレースバックの最後がOSError: [Errno 99] Cannot assign requested addressです。指定したアドレスがコンテナの中に無いのでbindできていません。コンテナは自分専用のネットワーク名前空間を持ち、見えるのはComposeが作ったブリッジネットワーク上のアドレスだけです。同じ設定の使い捨てコンテナをdocker compose runで起動して確かめると、ブリッジ側のアドレスしかありません。

terminal
$ docker compose run --rm --entrypoint sh web -c 'hostname -I'
172.18.0.2

workerはsh: curl: not foundと終了コード127です。127はシェルがコマンドを見つけられなかったときの値で、alpine:3にcurlが入っていないのが原因です。set -eがあるので、シェルはその終了コードのまま抜けています。イメージに何が入っているかはwhichで確かめます。

terminal(curl がないことの確認)
$ docker compose run --rm worker which curl wget
/usr/bin/wget

batchは137でOOMKilledがtrue、ログは空です。printに届く前に、カーネルがメモリ上限を超えたプロセスを止めています。128を超える終了コードは「シグナルで終了した」という印で、128を引いた番号がシグナル番号です。kill -lで名前に戻せます。

terminal
$ kill -l 137; kill -l 143
KILL
TERM

cronは終了コード0でログも空、つまりエラーではありません。BusyBoxのcrondはオプションを付けないとバックグラウンドに回る作りで(-bが既定)、最初に起動したプロセスはすぐ終わります。コンテナは最初のプロセス(PID 1)が終わった時点で止まるので、「正常終了→unless-stoppedが起動→正常終了」を繰り返していました。

terminal(抜粋)
$ docker compose run --rm --no-deps cron crond --help
Usage: crond [-fbS] [-l N] [-d N] [-L LOGFILE] [-c DIR]
	-f	Foreground
	-b	Background (default)
	-d N	Set log level, log to stderr

ここまでを表にまとめます。143と255、停止操作の後の137とOOMKilled=falseの組み合わせは、この後の章の計測で実際に出た値です。137とOOMKilled=falseはメモリ上限で止められたときにも出ることがあり、その理由は次のdocker eventsの説明で扱います。

終了コードOOMKilledログ・状況疑う原因次に見るもの
1falseアプリのエラー(トレースバックなど)設定値・接続先・権限ログの末尾、.envとcompose.yamlの値
127falsesh: コマンド名: not foundイメージに無いコマンドdocker compose run --rm サービス名 which コマンド名
127(STATUSがCreated)false空exec形式のcommandでコマンドが無い.State.Error(つまずきポイント参照)
137true空のことが多いメモリ上限(mem_limit)超え上限値と、処理が本当にそのメモリを必要とするか
137false停止操作の約10秒後SIGTERMを受け付けずSIGKILLされたinit: true、stop_grace_period
137false停止操作をしていないのに終わったメモリ上限超え(OOMの記録が取りこぼされた回)ほかの回のOOMKilledとoomイベント、上限を上げて再現するか
143false停止操作の直後SIGTERMで終了した(正常な停止)対応不要
0false空、または起動メッセージだけメインプロセスがすぐ終わる(デーモン化・一度きりの処理)フォアグラウンドで動かすオプション、restartの値
255false―dockerdが強制終了されたときに動いていた後述の再起動ポリシー表

時系列で追いたいときはdocker eventsを使います。--sinceと--until 0sを組み合わせると、過去のイベントを出力して終了します。--untilを付けなければ、新しいイベントを待ち受け続けます。

terminal
$ docker events --since 1m --until 0s --filter label=com.docker.compose.service=batch --format '{{.Time}} {{.Action}} exitCode={{.Actor.Attributes.exitCode}}' | tail -n 6
1790388859 die exitCode=137
1790388865 start exitCode=<no value>
1790388865 die exitCode=137
1790388878 start exitCode=<no value>
1790388878 oom exitCode=<no value>
1790388878 die exitCode=137

dieには終了コードが付き、メモリ上限で止められた回はその直前にoomが記録されます。startとoomにはexitCode属性が無いため<no value>と表示されます。ただし1790388865の回はstartの直後にdieだけが記録されていて、137で終わったのにoomが出ていません。この取りこぼしはinspectにも及びます。同じbatchの処理を同じメモリ上限のまま繰り返し起動して確かめたところ、oomイベントが記録された回は必ずOOMKilledがtrueで、記録されなかった回は137で終わっていてもOOMKilledがfalseでした。片方だけが残る回はありませんでした。OOMKilled=trueならメモリ上限で確定ですが、falseでもメモリ不足は否定できません。137で終わっていて、直前に停止や再起動の操作をしていないなら、まずメモリ上限を疑います。

docker compose restartとup -dの違い

webの原因は.envの値なので、0.0.0.0に直してからdocker compose restart webを実行します。前後で、コンテナIDとコンテナに保存されたコマンドライン(Config.Cmd)を比べます。

terminal
$ docker inspect --format '{{slice .Id 0 12}} {{.Config.Cmd}}' $(docker compose ps -aq web)
f52c78aedbff [python -m http.server 8080 --bind 192.168.1.10 --directory /srv]
$ sed -i 's/^WEB_BIND=.*/WEB_BIND=0.0.0.0/' .env && cat .env
WEB_BIND=0.0.0.0
$ docker compose restart web
 Container docker-compose-restart-loop-troubleshooting-web-1  Restarting
 Container docker-compose-restart-loop-troubleshooting-web-1  Started
$ docker compose ps web --format 'table {{.Service}}\t{{.Status}}'
SERVICE   STATUS
web       Restarting (1) 1 second ago
$ docker inspect --format '{{slice .Id 0 12}} {{.Config.Cmd}}' $(docker compose ps -aq web)
f52c78aedbff [python -m http.server 8080 --bind 192.168.1.10 --directory /srv]

コンテナIDはrestartの後もf52c78aedbffのままで、Config.Cmdには古い192.168.1.10が残っています。${WEB_BIND}の展開はComposeがコンテナを作る時点で済ませていて、その結果がコンテナの設定として保存されているためです。保存済みの設定のまま止めて起動し直すのがrestartなので、同じエラーで落ち続けます。

作り直すかどうかを、Composeは設定のハッシュで判断しています。docker compose config --hashは今のcompose.yamlと.envから計算した値、ラベルcom.docker.compose.config-hashはコンテナを作ったときの値です。

terminal
$ docker compose config --hash web
web 46f5eaea3a9eb3b2fe12b46af1f03cb03d5a6ff16b2502c39e8eaba7422333a6
$ docker inspect --format '{{index .Config.Labels "com.docker.compose.config-hash"}}' $(docker compose ps -aq web)
8a3e010ac97a0894f969174d1dec1ebf40e47476af407692593ce3df31d94b59
$ docker compose up -d web
 Container docker-compose-restart-loop-troubleshooting-web-1  Recreate
 Container docker-compose-restart-loop-troubleshooting-web-1  Recreated
 Container docker-compose-restart-loop-troubleshooting-web-1  Starting
 Container docker-compose-restart-loop-troubleshooting-web-1  Started
$ docker inspect --format '{{slice .Id 0 12}} {{.Config.Cmd}}' $(docker compose ps -aq web)
60e0f2f7d0af [python -m http.server 8080 --bind 0.0.0.0 --directory /srv]
$ docker compose ps web --format 'table {{.Service}}\t{{.Status}}'
SERVICE   STATUS
web       Up 7 seconds
$ curl -s http://127.0.0.1:18080/
v1

2つのハッシュが違うので、up -dはRecreate(削除して作り直し)を選び、新しいコンテナ60e0f2f7d0afには0.0.0.0が入りました。ハッシュが一致しているときのup -dは、動いているコンテナには手を付けずRunningと表示するだけで、止まっているコンテナなら起動だけします。

イメージの更新も同じ扱いです。docker compose pullでタグの指す先が新しくなっても、restartでは古いイメージのまま動き続けます。確かめるために小さなプロジェクトを別のディレクトリ(~/image-lab)に用意し、タグの付け替えでpull後と同じ状態を作りました。以降のpolicy-lab、daemon-lab、health-labも同じように、名前どおりのディレクトリにcompose.yamlを置いて、その中でコマンドを実行します。

image-lab/compose.yaml
name: docker-compose-restart-loop-troubleshooting-image

services:
  app:
    image: docker-compose-restart-loop-troubleshooting-app:latest
    command: ["sleep", "infinity"]
    init: true
terminal(image-lab)
$ docker image inspect --format '{{slice .Id 7 19}}' alpine:3 python:3.12-slim
294b683cb724
2f17fc044b57
$ docker tag alpine:3 docker-compose-restart-loop-troubleshooting-app:latest && docker compose up -d
$ docker inspect --format '{{slice .Id 0 12}} image={{slice .Image 7 19}}' $(docker compose ps -q app)
7129ba01ef89 image=294b683cb724
$ docker tag python:3.12-slim docker-compose-restart-loop-troubleshooting-app:latest && docker compose restart app
$ docker inspect --format '{{slice .Id 0 12}} image={{slice .Image 7 19}}' $(docker compose ps -q app)
7129ba01ef89 image=294b683cb724
$ docker compose up -d
 Container docker-compose-restart-loop-troubleshooting-image-app-1  Recreate
 Container docker-compose-restart-loop-troubleshooting-image-app-1  Recreated
 Container docker-compose-restart-loop-troubleshooting-image-app-1  Starting
 Container docker-compose-restart-loop-troubleshooting-image-app-1  Started
$ docker inspect --format '{{slice .Id 0 12}} image={{slice .Image 7 19}}' $(docker compose ps -q app)
1ee1b074073a image=2f17fc044b57

alpine:3(294b683cb724)で作ったコンテナは、タグをpython:3.12-slim(2f17fc044b57)に付け替えてrestartしてもalpineのままで、up -dで初めて作り直されました。確認が済んだらdocker compose down && docker rmi docker-compose-restart-loop-troubleshooting-app:latestで片付けます(rmiはタグを外すだけで、元のイメージは残ります)。

逆にrestartで足りるのは、マウントしたファイルの中身を変えたときです。アプリが起動時にファイルを読むなら、止めて起動し直すだけで新しい内容が入ります。ただし、ファイル1つだけをマウントしている場合は書き換え方に注意が要ります。

terminal
$ ls -i index.html
206063 index.html
$ sed -i 's/v1/v2/' index.html && ls -i index.html
206845 index.html
$ curl -s http://127.0.0.1:18080/
v1
$ docker compose exec web cat /srv/index.html
v1
$ docker compose restart web
 Container docker-compose-restart-loop-troubleshooting-web-1  Restarting
 Container docker-compose-restart-loop-troubleshooting-web-1  Started
$ curl -s http://127.0.0.1:18080/
v2

sed -iは別のファイルに書き出してから元の名前に置き換えるので、inode番号が206063から206845に変わっています。ファイル単位のバインドマウントはコンテナ起動時のファイルを指し続けるため、稼働中のコンテナからは古いv1が見え、restartでマウントし直した時点でv2になりました。ディレクトリごとマウントしておけば、置き換えた新しいファイルも稼働中のまま見えます(-v "$PWD/site:/srv:ro"の形で同じsed -iを試し、restartなしでv2が読めることを確認しました)。

何を変えたときにどのコマンドを使うか、手元の結果をまとめます。

変えたもの使うコマンド根拠
compose.yaml、.envdocker compose up -drestart後もコンテナIDとConfig.Cmdが変わらなかった
イメージ(pull後)docker compose pullの後にdocker compose up -drestart後もイメージIDが変わらなかった
Dockerfile(build:を使うサービス)docker compose up -d --build起動前にイメージをビルドし直す(公式リファレンスの説明。この記事では未検証)
マウントしたファイルの中身docker compose restart サービス名起動し直した時点で新しい内容が読まれた
何も変えずに作り直したいdocker compose up -d --force-recreatedocker updateで変えたポリシーもcompose.yamlの値に戻った(ループを止める章で確認)

再起動ポリシーの挙動比較:no・on-failure・always・unless-stopped

4つのポリシーの違いは、「どんな終わり方をしたときに起こすか」と「dockerdが起動し直したときに起こすか」の2点です。まず同じホスト上で、異常終了・正常終了・手動停止を比べます。確認用のpolicy-labでは、同じ中身のコンテナをポリシーだけ変えて4つ並べました。

policy-lab/compose.yaml
name: docker-compose-restart-loop-troubleshooting-policy

x-crash: &crash
  image: alpine:3
  command: ["sh", "-c", "sleep $${RUN_SECONDS}; exit $${EXIT_CODE}"]
  environment:
    RUN_SECONDS: ${RUN_SECONDS:-2}
    EXIT_CODE: ${EXIT_CODE:-1}

services:
  "no":
    <<: *crash
    restart: "no"
  on-failure-3:
    <<: *crash
    restart: on-failure:3
  always:
    <<: *crash
    restart: always
  unless-stopped:
    <<: *crash
    restart: unless-stopped

$${RUN_SECONDS}の$$はComposeの変数展開を止める書き方で、コンテナの中のシェルに${RUN_SECONDS}をそのまま渡します。値はenvironmentから入り、既定では2秒動いて終了コード1で終わります。"no"を引用符で囲むのは、YAMLの読み方によっては真偽値のfalseと解釈されるのを避けるためです。

terminal(policy-lab)
$ docker compose up -d
$ docker compose ps -a --format 'table {{.Service}}\t{{.Status}}'
SERVICE          STATUS
always           Restarting (1) 1 second ago
no               Exited (1) 15 seconds ago
on-failure-3     Exited (1) 7 seconds ago
unless-stopped   Restarting (1) 1 second ago
$ docker inspect --format '{{index .Config.Labels "com.docker.compose.service"}} restarts={{.RestartCount}}' $(docker compose ps -aq)
always restarts=6
no restarts=0
on-failure-3 restarts=3
unless-stopped restarts=6

異常終了(exit 1)では、noは1回で止まり、on-failure:3は3回再起動したところで止まりました。alwaysとunless-stoppedは起こし続けます。次は正常終了(exit 0)です。

terminal(policy-lab)
$ EXIT_CODE=0 docker compose up -d
$ docker compose ps -a --format 'table {{.Service}}\t{{.Status}}'
SERVICE          STATUS
always           Restarting (0) 5 seconds ago
no               Exited (0) 25 seconds ago
on-failure-3     Exited (0) 25 seconds ago
unless-stopped   Restarting (0) 5 seconds ago
$ docker inspect --format '{{index .Config.Labels "com.docker.compose.service"}} restarts={{.RestartCount}}' $(docker compose ps -aq)
always restarts=7
no restarts=0
on-failure-3 restarts=0
unless-stopped restarts=7

on-failureは終了コード0では一度も再起動しません。cronのRestarting (0)は、ポリシーがunless-stoppedだったから起きていたわけです。最後に、長く動くコンテナを手で止めます。

terminal(policy-lab)
$ RUN_SECONDS=600 docker compose up -d
$ docker compose ps -a --format 'table {{.Service}}\t{{.Status}}'
SERVICE          STATUS
always           Up Less than a second
no               Up Less than a second
on-failure-3     Up Less than a second
unless-stopped   Up Less than a second
$ time docker compose stop
 Container docker-compose-restart-loop-troubleshooting-policy-on-failure-3-1  Stopping
 Container docker-compose-restart-loop-troubleshooting-policy-unless-stopped-1  Stopping
 Container docker-compose-restart-loop-troubleshooting-policy-always-1  Stopping
 Container docker-compose-restart-loop-troubleshooting-policy-no-1  Stopping
 Container docker-compose-restart-loop-troubleshooting-policy-always-1  Stopped
 Container docker-compose-restart-loop-troubleshooting-policy-no-1  Stopped
 Container docker-compose-restart-loop-troubleshooting-policy-on-failure-3-1  Stopped
 Container docker-compose-restart-loop-troubleshooting-policy-unless-stopped-1  Stopped

real	0m10.358s
user	0m0.084s
sys	0m0.060s
$ docker compose ps -a --format 'table {{.Service}}\t{{.Status}}'
SERVICE          STATUS
always           Exited (137) 23 seconds ago
no               Exited (137) 23 seconds ago
on-failure-3     Exited (137) 23 seconds ago
unless-stopped   Exited (137) 23 seconds ago
$ docker inspect --format '{{index .Config.Labels "com.docker.compose.service"}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} restarts={{.RestartCount}}' $(docker compose ps -aq)
always exit=137 oom=false restarts=0
no exit=137 oom=false restarts=0
on-failure-3 exit=137 oom=false restarts=0
unless-stopped exit=137 oom=false restarts=0

手で止めた(docker compose stop)コンテナは、どのポリシーでも起こされませんでした。終了コードが137でも、on-failure-3は再起動していません。stopに10.358秒かかったのは、PID 1のshがSIGTERMを受け付けず、猶予の10秒が過ぎてからSIGKILLで止められたためです(対処はつまずきポイントで扱います)。

デーモン再起動と強制終了はDocker in Dockerで計測する

dockerd自体の再起動は、ほかのコンテナも動いているホストでは気軽に試せません。そこで、特権コンテナの中で別のdockerdを動かすDocker in Docker(DinD)を使いました。DinDコンテナをdocker restartすると、中のdockerdはSIGTERMを受けて動いているコンテナを止め、終了してから起動し直します。

--privilegedはコンテナにホストとほぼ同じ権限を渡します。DinDでの計測は検証専用のマシンで行い、本番のホストでは実行しないでください。

ホスト側に~/daemon-labを作り、次のcompose.yamlを置きます。on-failure-killだけはSIGTERMを無視するsleep infinityをPID 1にしていて、止められると終了コード137になります。ほかの4つはSIGTERMを受けるとtrapで終了コード0を返して抜けます。

daemon-lab/compose.yaml
name: docker-compose-restart-loop-troubleshooting-daemon

x-app: &app
  image: alpine:3
  command: ["sh", "-c", "trap 'exit 0' TERM; while sleep 1; do :; done"]

services:
  "no":
    <<: *app
    restart: "no"
  on-failure:
    <<: *app
    restart: on-failure
  always:
    <<: *app
    restart: always
  unless-stopped:
    <<: *app
    restart: unless-stopped
  on-failure-kill:
    image: alpine:3
    command: ["sleep", "infinity"]
    restart: on-failure

そのディレクトリで、DinDコンテナを起動してcompose.yamlをコピーします。--hostを明示しているのは、DinDイメージの既定で開くTCPの待ち受け(2376番)を使わず、UNIXソケットだけにするためです。

terminal(ホスト・daemon-lab)
$ docker run -d --privileged --memory 1g --name docker-compose-restart-loop-troubleshooting-dind docker:29.1.3-dind dockerd --host=unix:///var/run/docker.sock
$ docker exec docker-compose-restart-loop-troubleshooting-dind mkdir -p /root/daemon-lab && docker cp compose.yaml docker-compose-restart-loop-troubleshooting-dind:/root/daemon-lab/compose.yaml

「DinD内」と書いたブロックは、docker exec -it docker-compose-restart-loop-troubleshooting-dind shで入ったシェルでcd /root/daemon-labしてから実行しています。DinDイメージに入っているComposeはv5.0.0でした。

terminal(DinD内)
$ docker version --format "Engine {{.Server.Version}}" && docker compose version
Engine 29.1.3
Docker Compose version v5.0.0
$ docker compose up -d
$ docker compose ps -a --format 'table {{.Service}}\t{{.Status}}'
SERVICE           STATUS
always            Up 9 seconds
no                Up 9 seconds
on-failure        Up 9 seconds
on-failure-kill   Up 9 seconds
unless-stopped    Up 9 seconds

全部が動いている状態で、ホスト側からDinDコンテナを再起動します。-t 60は、中のdockerdがコンテナを止め終えるまで待つための猶予です。

terminal(ホスト)
$ time docker restart -t 60 docker-compose-restart-loop-troubleshooting-dind
docker-compose-restart-loop-troubleshooting-dind

real	0m11.389s
user	0m0.010s
sys	0m0.012s
$ docker logs docker-compose-restart-loop-troubleshooting-dind 2>&1 | grep -E 'using the force|Daemon shutdown complete'
time="2026-09-26T02:30:29.370960987Z" level=info msg="Container failed to exit within 10s of signal 15 - using the force" container=51aa6f440cb850f9f9fd068c9d00d811131a530273a84f00400e860353c9cd57
time="2026-09-26T02:30:29.463262374Z" level=info msg="Daemon shutdown complete"

SIGTERM(signal 15)に10秒応じなかったコンテナを強制終了してから、dockerdが終了しています。このIDはdocker ps -a --no-truncで調べるとon-failure-killでした。起動し直した後の状態です。

terminal(DinD内)
$ docker compose ps -a --format 'table {{.Service}}\t{{.Status}}'
SERVICE           STATUS
always            Up 8 seconds
no                Exited (0) 19 seconds ago
on-failure        Exited (0) 19 seconds ago
on-failure-kill   Up 8 seconds
unless-stopped    Up 8 seconds

alwaysとunless-stoppedが起動したのは想定どおりですが、on-failureは2つで結果が分かれました。SIGTERMで0を返したon-failureは起こされず、SIGKILLで137になったon-failure-killは起こされています。公式ドキュメントでは、on-failureはデーモンの再起動では再起動しないと説明されていますが、手元の計測では停止時の終了コードが0以外なら起動しました。次は、全部を手で止めてからデーモンを再起動します。

terminal(DinD内)
$ docker compose up -d
$ docker compose stop
$ docker compose ps -a --format 'table {{.Service}}\t{{.Status}}'
SERVICE           STATUS
always            Exited (0) 9 seconds ago
no                Exited (0) 9 seconds ago
on-failure        Exited (0) 9 seconds ago
on-failure-kill   Exited (137) Less than a second ago
unless-stopped    Exited (0) 9 seconds ago
terminal(ホスト)
$ time docker restart -t 60 docker-compose-restart-loop-troubleshooting-dind
docker-compose-restart-loop-troubleshooting-dind

real	0m1.292s
user	0m0.004s
sys	0m0.018s
terminal(DinD内)
$ docker compose ps -a --format 'table {{.Service}}\t{{.Status}}'
SERVICE           STATUS
always            Up 7 seconds
no                Exited (0) 19 seconds ago
on-failure        Exited (0) 19 seconds ago
on-failure-kill   Up 7 seconds
unless-stopped    Exited (0) 19 seconds ago

手で止めた後のデーモン再起動では、alwaysが起こされ、unless-stoppedは止まったままでした。ここでもon-failure-killは起動しています。自分でdocker compose stopしたつもりのコンテナでも、on-failureで停止時の終了コードが137なら、dockerdの再起動で戻ってくるということです。最後に、全部を動かした状態でDinDコンテナをdocker killし、停電に近い止まり方を再現しました。

terminal(DinD内)
$ docker compose up -d
terminal(ホスト)
$ docker kill docker-compose-restart-loop-troubleshooting-dind && docker start docker-compose-restart-loop-troubleshooting-dind
docker-compose-restart-loop-troubleshooting-dind
docker-compose-restart-loop-troubleshooting-dind

検証をやり直したときには、表示は同じでも、DinDコンテナが起動の直後に止まっていた回がありました。そのままDinDの中でコマンドを実行しようとすると、Error response from daemon: container … is not running(…はDinDコンテナのID)で断られます。killで止めたために前回のcontainerdのPIDファイルがコンテナ内に残り、起動し直した後にその番号のプロセスが存在したので、中のdockerdが起動を取りやめていました。docker ps -aでDinDコンテナが止まっていたら、もう一度docker startすれば起動します。

terminal(DinD内)
$ docker compose ps -a --format 'table {{.Service}}\t{{.Status}}'
SERVICE           STATUS
always            Up 8 seconds
no                Exited (255) 8 seconds ago
on-failure        Up 8 seconds
on-failure-kill   Up 8 seconds
unless-stopped    Up 8 seconds

DinDコンテナをdocker killすると全プロセスがSIGKILLで止まり、中のdockerdはコンテナを止める手順を踏めません。起動し直したdockerdは、動いていたはずのコンテナを終了コード255で止まったものとして扱い、no以外を起動しました。計測が終わったらdocker rm -fv docker-compose-restart-loop-troubleshooting-dindで、DinDコンテナと、中のdockerdのデータ(イメージが/var/lib/dockerとして宣言している匿名ボリューム)を消します。

結果をまとめます。「デーモン再起動」と「強制終了」の3列はDinD内のdockerd(Engine 29.1.3)で、ほかの列はホストのdockerdで計測した値です。

ポリシー異常終了(exit 1)正常終了(exit 0)compose stop後デーモン再起動(稼働中)stop後にデーモン再起動dockerd強制終了後
"no"止まる止まる止まる止まる止まる止まる(Exited (255))
on-failure再起動(:3なら3回で停止)止まる止まる停止時の終了コードが0なら止まる、0以外なら起動同左(137で止まったものは起動)起動
always再起動再起動止まる起動起動起動
unless-stopped再起動再起動止まる起動止まる起動

ホスト自体の再起動は、この環境では実施していません。Ubuntu/Debianではdocker.serviceが起動時に有効になるのが既定で(公式ドキュメント)、このサーバーでも有効でした。ホストの再起動後にdockerdが起動した時点で、上の表の「デーモン再起動」または「強制終了」の列と同じ判定になる想定ですが、確かめてはいません。本番で頼る前に、検証機で一度試してください。

terminal
$ systemctl is-enabled docker.service containerd.service
enabled
enabled

常駐させるサービスにはunless-stoppedを勧めます。落ちたら起こし、ホストが再起動しても戻り、調査のために止めたものは勝手に戻りません。alwaysは止めたはずのコンテナがデーモン再起動で戻ってくるので、stopで止めておく運用があるなら避けます。on-failureは一度きりのバッチ向けですが、上のとおりstopしても戻ることがあるため、止めたままにしたいときはdocker compose downでコンテナごと消します。

healthcheckがunhealthyになっても、再起動ポリシーは動きません。常に失敗するhealthcheckを付けたコンテナをrestart: alwaysで動かし、1分以上待った結果です。

health-lab/compose.yaml
name: docker-compose-restart-loop-troubleshooting-health

services:
  app:
    image: alpine:3
    command: ["sleep", "infinity"]
    init: true
    restart: always
    healthcheck:
      test: ["CMD", "false"]
      interval: 2s
      retries: 2
terminal(health-lab)
$ docker compose ps --format 'table {{.Service}}\t{{.Status}}'
SERVICE   STATUS
app       Up About a minute (unhealthy)
$ docker inspect --format 'health={{.State.Health.Status}} failing={{.State.Health.FailingStreak}} restarts={{.RestartCount}}' $(docker compose ps -q app)
health=unhealthy failing=42 restarts=0

連続42回失敗してもrestarts=0のままです。unhealthyを検知して入れ替えたいなら、外部のスクリプトや監視ツールで検知してdocker compose restartする仕組みが別に要ります。healthcheckの書き方と、depends_onで起動順の制御に使う方法はDocker Compose入門にまとめています。

再起動の間隔を実測する:バックオフと10秒ルール

落ちたコンテナは、毎回すぐに起動されるわけではありません。公式ドキュメントでは、待ち時間は100ミリ秒から始まって毎回2倍になり、上限は1分、再起動後に10秒以上動けば100ミリ秒に戻ると説明されています。restart-labのworkerで実際の間隔を測りました。

docker events --since 10mで後からさかのぼる方法は、ほかのコンテナも動いているホストでは当てになりません。過去のイベントとしてdockerdが返すのは直近256件だけで(公式ドキュメント)、手元でも10分前から集計したところ1区間分しか残っていませんでした。記録はコンテナを作り直す前に始めます。

まず、ループ中のworkerを一度消します。

terminal
$ docker compose rm -sf worker

別のターミナルで、workerのstartとdieを記録し始めます。--since 0sで過去分を出さず、--untilに5分後の時刻を渡しているので、記録は5分で自動的に終わります。

terminal(別のターミナル)
docker events --since 0s --until "$(date -d '+5 min' +%s)" --filter label=com.docker.compose.project=docker-compose-restart-loop-troubleshooting --filter label=com.docker.compose.service=worker --filter event=start --filter event=die --format '{{.TimeNano}} {{.Action}}' > worker-events.log

元のターミナルでworkerを作り直し、記録が終わったらdieから次のstartまでの間隔を計算します。

terminal
$ docker compose up -d worker
$ awk '$2=="start"{if(d) printf "%2d回目 %6.2f秒\n", ++n, ($1-d)/1e9; s=1} $2=="die"&&s{d=$1}' worker-events.log
 1回目   0.18秒
 2回目   0.29秒
 3回目   0.50秒
 4回目   0.91秒
 5回目   1.70秒
 6回目   3.30秒
 7回目   6.52秒
 8回目  12.89秒
 9回目  25.72秒
10回目  51.31秒
11回目  60.09秒
12回目  60.09秒
13回目  60.10秒
$ docker inspect --format 'restarts={{.RestartCount}} started={{.State.StartedAt}} finished={{.State.FinishedAt}}' $(docker compose ps -aq worker)
restarts=14 started=2026-09-26T02:29:10.245249164Z finished=2026-09-26T02:29:10.479361628Z

間隔は0.18秒、0.29秒、0.50秒と倍々で伸び、10回目の51.31秒の次からは60.09秒で頭打ちになりました。どの値もドキュメントの待ち時間より少しずつ長いのは、コンテナの起動処理にかかる時間が上乗せされるためです。StartedAtとFinishedAtの差を見ると、1回の実行は1秒もかからずに終わっています。RestartCountは14で、測れた13区間より1つ多いのは、終了した時点で次の起動を予約して数えるためです。

「10秒以上動けばリセット」も確かめます。policy-labを使い、9秒で落ちるalwaysと、11秒で落ちるunless-stoppedを同時に動かしました。

terminal(policy-lab)
$ docker compose down
terminal(別のターミナル・policy-lab)
docker events --since 0s --until "$(date -d '+90 sec' +%s)" --filter label=com.docker.compose.project=docker-compose-restart-loop-troubleshooting-policy --filter event=start --filter event=die --format '{{.TimeNano}} {{.Action}} {{index .Actor.Attributes "com.docker.compose.service"}}' > policy-events.log
terminal(policy-lab)
$ RUN_SECONDS=9 docker compose up -d always
$ RUN_SECONDS=11 docker compose up -d unless-stopped
$ awk '$2=="start"{if(d[$3]) printf "%-15s %6.2f秒\n", $3, ($1-d[$3])/1e9; s[$3]=1} $2=="die"&&s[$3]{d[$3]=$1}' policy-events.log
always            0.19秒
unless-stopped    0.18秒
always            0.29秒
unless-stopped    0.26秒
always            0.49秒
unless-stopped    0.17秒
always            0.90秒
unless-stopped    0.17秒
always            1.68秒
unless-stopped    0.19秒
always            3.30秒
unless-stopped    0.17秒
always            6.51秒
unless-stopped    0.18秒

9秒で落ちる側は0.19秒、0.29秒、0.49秒と伸び続け、11秒で落ちる側は0.17〜0.26秒のまま毎回すぐ起動しています。そのまま約10分動かすと、再起動回数は逆転しました。

terminal(policy-lab)
$ docker inspect --format '{{index .Config.Labels "com.docker.compose.service"}} restarts={{.RestartCount}} created={{.Created}}' $(docker compose ps -aq)
always restarts=16 created=2026-09-26T02:24:44.092509533Z
unless-stopped restarts=54 created=2026-09-26T02:24:44.553518621Z

10秒以上動いてから落ちるアプリには、バックオフが効きません。起動のたびに外部のAPIやDBへ接続する処理なら、その回数だけ相手に負荷がかかり、ログも同じ速さで増えます。RestartCountが大きいのに今はUpと表示されているサービスがあれば、この型を疑います。直すまでの間は、次の章の方法でループを止めておきます。

ループを止めて調べる:stop・docker update・compose run

調べている間もループが回っていると、ログが流れ続けて読みにくく、docker compose execでは中に入れません(つまずきポイント参照)。止め方は目的で選びます。

いちばん確実なのはdocker compose stop サービス名です。手で止めたコンテナは、前の章のとおりalwaysでもそのままでは起こされません。例外はデーモンの再起動で、alwaysと、停止時の終了コードが0以外のon-failureは戻ってきます。

docker update --restart=noは、コンテナを消さずに再起動ポリシーだけを外します。ループ中のcronで試しました。

terminal
$ docker update --restart=no $(docker compose ps -aq cron)
3ce8a6d10df62e754bd5d4322a61a392eb404f0b30219633eae804e20ed140a7
$ docker inspect --format 'policy={{.HostConfig.RestartPolicy.Name}} status={{.State.Status}} restarts={{.RestartCount}} started={{.State.StartedAt}}' $(docker compose ps -aq cron)
policy=no status=exited restarts=22 started=2026-09-26T02:27:57.635235158Z
$ docker compose ps -a cron --format 'table {{.Service}}\t{{.Status}}'
SERVICE   STATUS
cron      Exited (0) 16 seconds ago

updateを実行したのは02:27:09(UTC)で、StartedAtはその後の02:27:57です。update時点ですでに次の起動を待っていた分は取り消されず、1回起動してからExited (0)で止まりました。60秒待ちに入っているコンテナなら、止まるまで最大でその分かかります。すぐ止めたいならstopを使います。

調査が終わったら元のポリシーに戻します。ここでdocker compose up -dだけを実行しても戻りません。

terminal
$ docker compose up -d cron
 Container docker-compose-restart-loop-troubleshooting-cron-1  Starting
 Container docker-compose-restart-loop-troubleshooting-cron-1  Started
$ docker inspect --format 'policy={{.HostConfig.RestartPolicy.Name}} status={{.State.Status}}' $(docker compose ps -aq cron)
policy=no status=exited
$ docker compose up -d --force-recreate cron
 Container docker-compose-restart-loop-troubleshooting-cron-1  Recreate
 Container docker-compose-restart-loop-troubleshooting-cron-1  Recreated
 Container docker-compose-restart-loop-troubleshooting-cron-1  Starting
 Container docker-compose-restart-loop-troubleshooting-cron-1  Started
$ docker inspect --format 'policy={{.HostConfig.RestartPolicy.Name}} status={{.State.Status}}' $(docker compose ps -aq cron)
policy=unless-stopped status=restarting

設定のハッシュはcompose.yamlと.envから計算するので、docker updateで変えてもup -dから見れば「変更なし」です。up -dは止まっていたコンテナを起動するだけで、ポリシーはnoのまま残りました。--force-recreateで作り直すとunless-stoppedに戻ります(直後のrestartingは、この時点のcronがまだ直す前のcommandだったためです)。commandなどcompose.yamlの中身を書き換えてからup -dした場合は、作り直しが起きるのでポリシーも一緒に戻ります。

docker updateで外したポリシーを戻し忘れると、そのサービスはデーモンやホストの再起動後に上がってきません。調査用に外したら、作業メモにdocker compose up -d --force-recreate サービス名まで書いておきます。

中を調べるときは、ループ中のコンテナに入るのではなく、同じサービス定義から別のコンテナを1つ起動します。docker compose run --rm サービス名 コマンドは、サービスのcommandだけを差し替えたコンテナを作り、ポートは公開しません。イメージのENTRYPOINTそのものが落ちる原因なら、--entrypoint shで差し替えます。runで作ったコンテナには再起動ポリシーが付かないため、調べている途中で落としてもループしません。

terminal
$ docker compose run -d --name docker-compose-restart-loop-troubleshooting-debug worker sh -c 'exit 3'
$ docker inspect --format 'policy={{.HostConfig.RestartPolicy.Name}} status={{.State.Status}} exit={{.State.ExitCode}} restarts={{.RestartCount}}' docker-compose-restart-loop-troubleshooting-debug
policy=no status=exited exit=3 restarts=0
$ docker rm docker-compose-restart-loop-troubleshooting-debug

restart: unless-stoppedはrunのコンテナに引き継がれず、終了コード3で終わった後もrestarts=0のままです。調べる流れは「stopで止める→runで中を見る→compose.yamlを直す→up -d」を基本にし、docker updateは「動かし続けたいが、落ちても起こさないでほしい」ときだけに使います。

4つのループを直す

原因に合わせて直したcompose.yamlです。webの.envは前の章で修正済みです。

compose.yaml(修正後)
name: docker-compose-restart-loop-troubleshooting

services:
  web:
    image: python:3.12-slim
    command: ["python", "-m", "http.server", "8080", "--bind", "${WEB_BIND}", "--directory", "/srv"]
    init: true
    ports:
      - "127.0.0.1:18080:8080"
    volumes:
      - ./index.html:/srv/index.html:ro
    restart: unless-stopped

  worker:
    image: alpine:3
    command: ["sh", "-c", "set -e; while true; do wget -q -O /dev/null http://web:8080/; sleep 30; done"]
    init: true
    restart: unless-stopped

  batch:
    image: python:3.12-slim
    command: ["python", "-c", "data = bytearray(64 * 1024 * 1024); print('loaded', len(data))"]
    mem_limit: 128m
    memswap_limit: 128m
    restart: on-failure

  cron:
    image: alpine:3
    command: ["crond", "-f", "-d", "8"]
    init: true
    restart: unless-stopped
  • web:init: trueを追加しました。PID 1にdocker-init(tini)を置き、SIGTERMをpythonに届けます。付けないとrestartのたびに10秒待たされます(つまずきポイントで計測)。
  • worker:curlをBusyBoxのwgetに替えました。alpine:3に最初から入っている/usr/bin/wgetです。起動コマンドの中でapk add curlする書き方は、起動のたびにパッケージを取りに行くので、ネットワーク障害のときに別のループの原因になります。curlが要るならDockerfileでイメージに入れます。
  • batch:mem_limitとmemswap_limitを128mにしました。上限を上げる前に、データを丸ごとメモリに載せる作りが本当に必要かを確かめます。
  • cron:crond -f -d 8にしました。-fでフォアグラウンドに留まり、-d 8でログを標準エラーに出すのでdocker compose logsで読めます。
terminal(抜粋)
$ docker compose up -d
 Container docker-compose-restart-loop-troubleshooting-cron-1  Recreate
 Container docker-compose-restart-loop-troubleshooting-web-1  Recreate
 Container docker-compose-restart-loop-troubleshooting-worker-1  Recreate
 Container docker-compose-restart-loop-troubleshooting-batch-1  Recreate
 Container docker-compose-restart-loop-troubleshooting-worker-1  Started
 Container docker-compose-restart-loop-troubleshooting-web-1  Started
 Container docker-compose-restart-loop-troubleshooting-cron-1  Started
 Container docker-compose-restart-loop-troubleshooting-batch-1  Started
$ docker compose ps -a --format 'table {{.Service}}\t{{.Status}}'
SERVICE   STATUS
batch     Exited (0) 14 seconds ago
cron      Up 14 seconds
web       Up 14 seconds
worker    Up 13 seconds
$ docker inspect --format '{{index .Config.Labels "com.docker.compose.service"}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} restarts={{.RestartCount}}' $(docker compose ps -aq)
batch exit=0 oom=false restarts=0
cron exit=0 oom=false restarts=0
web exit=0 oom=false restarts=0
worker exit=0 oom=false restarts=2
$ docker compose logs batch cron
cron-1   | crond: crond (busybox 1.37.0) started, log level 8
batch-1  | loaded 67108864
$ docker compose logs worker
worker-1  | wget: can't connect to remote host (172.18.0.3): Connection refused
worker-1  | wget: can't connect to remote host (172.18.0.3): Connection refused

batchは64MiB(67108864バイト)を読み込んでExited (0)で終わり、on-failureなので起こされません。残りの3つはUpです。2回再起動したworkerは、webが待ち受けを始める前にwgetが2回失敗していて、ログにConnection refusedが2行残っています。この程度なら再起動ポリシーが吸収しますが、起動に時間がかかるDBが相手だと、待つ間にバックオフが伸びて復帰が遅れます。起動順はdepends_onのcondition: service_healthyで揃えるのが筋で、書き方はDocker Compose入門の3層構成の例にあります。

つまずきポイント

docker compose execが「is restarting, wait until the container is running」で入れない

1つ目のコマンドはwebがまだRestartingを繰り返していたとき(最初の章の状態)、2つ目は修正後にbatchがExited (0)で止まっているときに実行した結果です。

terminal(エラー)
$ docker compose exec web sh
Error response from daemon: Container f52c78aedbfffe20f1f38c8bb2d09c6e870b3b494ae0507996f26dd632176418 is restarting, wait until the container is running
$ docker compose exec batch sh
service "batch" is not running

execはRestartingの間は使えず、止まっているコンテナ(上のbatchのようなExited)にも入れません。docker compose run --rm --entrypoint sh サービス名で、同じ定義の別コンテナを起動して中を調べます。

exec形式のcommandでコマンドが無いと、ループせずCreatedのまま止まる

shを通さない形(exec形式)にworkerのcommandを書き換えると、症状が変わります。

compose.yaml(workerをexec形式にした例)
  worker:
    image: alpine:3
    command: ["curl", "-fsS", "-o", "/dev/null", "http://web:8080/"]
    restart: unless-stopped
terminal(エラー)
$ docker compose up -d worker
 Container docker-compose-restart-loop-troubleshooting-worker-1  Recreate
 Container docker-compose-restart-loop-troubleshooting-worker-1  Recreated
 Container docker-compose-restart-loop-troubleshooting-worker-1  Starting
Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: exec: "curl": executable file not found in $PATH
$ docker compose ps -a worker --format 'table {{.Service}}\t{{.Status}}'
SERVICE   STATUS
worker    Created
$ docker compose logs worker
$ docker inspect --format 'status={{.State.Status}} exit={{.State.ExitCode}} restarts={{.RestartCount}} error={{.State.Error}}' $(docker compose ps -aq worker)
status=created exit=127 restarts=0 error=failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: exec: "curl": executable file not found in $PATH

コマンドを探すのがシェルではなくコンテナランタイム(runc)になるため、プロセスが1つも起動しないまま作成の段階で失敗します。STATUSはCreated、ログは空で、原因は.State.Errorにしか残りません。再起動ポリシーは一度も動かず、restarts=0です。試した後はcompose.yaml(修正後)のworkerに戻してdocker compose up -d workerを実行します。

restartやstopのたびに10秒待たされ、終了コードが137になる

次の出力は、.envを直した後、compose.yamlにinit: trueを足す前のwebで測ったものです。

terminal
$ time docker compose restart web
 Container docker-compose-restart-loop-troubleshooting-web-1  Restarting
 Container docker-compose-restart-loop-troubleshooting-web-1  Started

real	0m10.509s
user	0m0.069s
sys	0m0.056s

コンテナの中でPID 1になったプロセスは、自分でシグナルハンドラを登録していないとSIGTERMを受けても終了しません。python -m http.serverはSIGTERMを処理しないため、Dockerは猶予時間(stop_grace_period、既定10秒)を待ってからSIGKILLを送っていました。init: trueを付けると、PID 1のdocker-initがSIGTERMを子プロセスのpythonに転送します。修正後のcompose.yamlで測り直した結果です。

terminal
$ time docker compose restart web
 Container docker-compose-restart-loop-troubleshooting-web-1  Restarting
 Container docker-compose-restart-loop-troubleshooting-web-1  Started

real	0m0.434s
user	0m0.076s
sys	0m0.055s
$ docker compose stop web
 Container docker-compose-restart-loop-troubleshooting-web-1  Stopping
 Container docker-compose-restart-loop-troubleshooting-web-1  Stopped
$ docker inspect --format 'exit={{.State.ExitCode}} oom={{.State.OOMKilled}}' $(docker compose ps -aq web)
exit=143 oom=false
$ docker compose start web
 Container docker-compose-restart-loop-troubleshooting-web-1  Starting
 Container docker-compose-restart-loop-troubleshooting-web-1  Started

所要時間は10.509秒から0.434秒に縮み、停止時の終了コードは143(128+15、SIGTERM)になりました。stop_grace_periodを延ばしても待ち時間が伸びるだけなので、まずinit: trueを試します。

動作確認

修正後のrestart-labで、状態・再起動回数・HTTPの応答を確かめます。

terminal
$ docker compose ps -a --format 'table {{.Service}}\t{{.Status}}'
SERVICE   STATUS
batch     Exited (0) 3 minutes ago
cron      Up 3 minutes
web       Up 2 minutes
worker    Up 3 minutes
$ docker inspect --format '{{index .Config.Labels "com.docker.compose.service"}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} restarts={{.RestartCount}}' $(docker compose ps -aq)
batch exit=0 oom=false restarts=0
cron exit=0 oom=false restarts=0
web exit=0 oom=false restarts=0
worker exit=0 oom=false restarts=2
$ curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:18080/
200

batchがExited (0)、ほかの3つがUp、HTTPが200なら直っています。数分おいてinspectをもう一度実行し、restartsの値が増えていないことも見ます。2回というworkerの値は起動順の分で、修正から3分たっても増えていません。つまずきポイントのexec形式を試してからworkerを戻した場合は、その時点でworkerが作り直されるので、restartsは0から数え直しになります。

片付けは、restart-lab・policy-lab・health-labの各ディレクトリでdocker compose downを実行します。コンテナとネットワークが消え、run --rmで作ったコンテナは終了時に消えています。image-labとDinDは、それぞれの章に書いたコマンドで片付けます。

terminal
$ docker compose down

よくある質問

Docker Composeでリスタートするには?

docker compose restartで全サービス、docker compose restart webのようにサービス名を付ければそのサービスだけを止めて起動し直します。-tで停止を待つ秒数を変えられます。ただしcompose.yamlや.envの変更は入らないので、設定を変えた後はdocker compose up -dを使います。

Docker Composeの更新方法は?

docker compose pullで新しいイメージを取得してからdocker compose up -dを実行します。up -dはイメージや設定が変わったサービスだけを作り直し、ほかはそのまま動かします。restartでは新しいイメージに切り替わりません。ビルドして使うサービス(build:)はdocker compose up -d --buildです。

Docker Composeで自動起動するにはどうすればいいですか?

各サービスにrestart: unless-stoppedを書き、ホスト側でdocker.serviceが起動時に有効かsystemctl is-enabled docker.serviceで確かめます。Ubuntu/Debianは既定で有効です。unless-stoppedなら、手でstopしたサービスはデーモンが再起動しても止まったままです。

Docker Composeでコンテナを停止するにはどうすればいいですか?

一時的に止めるならdocker compose stopで、コンテナは残りdocker compose startで再開できます。片付けるならdocker compose downで、コンテナとネットワークが消えます。名前付きボリュームも消すときだけ-vを付けます。stopしたコンテナでも、alwaysはデーモン再起動で起動します。

まとめ

Restartingの括弧の数字が1ならログ、127ならイメージの中身、137ならOOMKilledと直前の停止操作の有無、0ならメインプロセスの起動オプションを見ます。

コンテナIDも設定も変えないのがrestartです。compose.yaml・.env・イメージを変えたらup -dで作り直し、マウントしたファイルの変更だけならrestartで足ります。

常駐サービスはunless-stoppedにし、止めたままにしたいものはstopではなくdownで消します。

まずは手元のプロジェクトでdocker inspect --format '{{index .Config.Labels "com.docker.compose.service"}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} restarts={{.RestartCount}}' $(docker compose ps -aq)を実行し、restartsが0でないサービスから原因を読んでいきます。

次に読む

$ subscribe --weekly
毎週月曜、企業ITとホームラボの要点を5分で。