
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:の値をコンテナ作成時に設定として渡すだけです。
$ 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を作ります。
mkdir -p ~/restart-lab && cd ~/restart-lab
printf 'WEB_BIND=192.168.1.10\n' > .env && echo v1 > index.html次のcompose.yamlを置きます。コメントに書いた終了コードで落ちるよう、サービスごとに1か所ずつ間違えてあります。
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-stoppedname:はプロジェクト名で、省略すると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で戻ります。
$ 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でサービス名と状態だけに絞ると、長いコンテナ名に邪魔されずに読めます。
$ 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 agoRestarting (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を返します。
$ 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は何も出力しません。
$ 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 cronwebは、Pythonのトレースバックの最後がOSError: [Errno 99] Cannot assign requested addressです。指定したアドレスがコンテナの中に無いのでbindできていません。コンテナは自分専用のネットワーク名前空間を持ち、見えるのはComposeが作ったブリッジネットワーク上のアドレスだけです。同じ設定の使い捨てコンテナをdocker compose runで起動して確かめると、ブリッジ側のアドレスしかありません。
$ docker compose run --rm --entrypoint sh web -c 'hostname -I'
172.18.0.2workerはsh: curl: not foundと終了コード127です。127はシェルがコマンドを見つけられなかったときの値で、alpine:3にcurlが入っていないのが原因です。set -eがあるので、シェルはその終了コードのまま抜けています。イメージに何が入っているかはwhichで確かめます。
$ docker compose run --rm worker which curl wget
/usr/bin/wgetbatchは137でOOMKilledがtrue、ログは空です。printに届く前に、カーネルがメモリ上限を超えたプロセスを止めています。128を超える終了コードは「シグナルで終了した」という印で、128を引いた番号がシグナル番号です。kill -lで名前に戻せます。
$ kill -l 137; kill -l 143
KILL
TERMcronは終了コード0でログも空、つまりエラーではありません。BusyBoxのcrondはオプションを付けないとバックグラウンドに回る作りで(-bが既定)、最初に起動したプロセスはすぐ終わります。コンテナは最初のプロセス(PID 1)が終わった時点で止まるので、「正常終了→unless-stoppedが起動→正常終了」を繰り返していました。
$ 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 | ログ・状況 | 疑う原因 | 次に見るもの |
|---|---|---|---|---|
| 1 | false | アプリのエラー(トレースバックなど) | 設定値・接続先・権限 | ログの末尾、.envとcompose.yamlの値 |
| 127 | false | sh: コマンド名: not found | イメージに無いコマンド | docker compose run --rm サービス名 which コマンド名 |
| 127(STATUSがCreated) | false | 空 | exec形式のcommandでコマンドが無い | .State.Error(つまずきポイント参照) |
| 137 | true | 空のことが多い | メモリ上限(mem_limit)超え | 上限値と、処理が本当にそのメモリを必要とするか |
| 137 | false | 停止操作の約10秒後 | SIGTERMを受け付けずSIGKILLされた | init: true、stop_grace_period |
| 137 | false | 停止操作をしていないのに終わった | メモリ上限超え(OOMの記録が取りこぼされた回) | ほかの回のOOMKilledとoomイベント、上限を上げて再現するか |
| 143 | false | 停止操作の直後 | SIGTERMで終了した(正常な停止) | 対応不要 |
| 0 | false | 空、または起動メッセージだけ | メインプロセスがすぐ終わる(デーモン化・一度きりの処理) | フォアグラウンドで動かすオプション、restartの値 |
| 255 | false | ― | dockerdが強制終了されたときに動いていた | 後述の再起動ポリシー表 |
時系列で追いたいときはdocker eventsを使います。--sinceと--until 0sを組み合わせると、過去のイベントを出力して終了します。--untilを付けなければ、新しいイベントを待ち受け続けます。
$ 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=137dieには終了コードが付き、メモリ上限で止められた回はその直前に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)を比べます。
$ 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はコンテナを作ったときの値です。
$ 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/
v12つのハッシュが違うので、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を置いて、その中でコマンドを実行します。
name: docker-compose-restart-loop-troubleshooting-image
services:
app:
image: docker-compose-restart-loop-troubleshooting-app:latest
command: ["sleep", "infinity"]
init: true$ 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=2f17fc044b57alpine: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つだけをマウントしている場合は書き換え方に注意が要ります。
$ 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/
v2sed -iは別のファイルに書き出してから元の名前に置き換えるので、inode番号が206063から206845に変わっています。ファイル単位のバインドマウントはコンテナ起動時のファイルを指し続けるため、稼働中のコンテナからは古いv1が見え、restartでマウントし直した時点でv2になりました。ディレクトリごとマウントしておけば、置き換えた新しいファイルも稼働中のまま見えます(-v "$PWD/site:/srv:ro"の形で同じsed -iを試し、restartなしでv2が読めることを確認しました)。
何を変えたときにどのコマンドを使うか、手元の結果をまとめます。
| 変えたもの | 使うコマンド | 根拠 |
|---|---|---|
| compose.yaml、.env | docker compose up -d | restart後もコンテナIDとConfig.Cmdが変わらなかった |
| イメージ(pull後) | docker compose pullの後にdocker compose up -d | restart後もイメージIDが変わらなかった |
| Dockerfile(build:を使うサービス) | docker compose up -d --build | 起動前にイメージをビルドし直す(公式リファレンスの説明。この記事では未検証) |
| マウントしたファイルの中身 | docker compose restart サービス名 | 起動し直した時点で新しい内容が読まれた |
| 何も変えずに作り直したい | docker compose up -d --force-recreate | docker updateで変えたポリシーもcompose.yamlの値に戻った(ループを止める章で確認) |
再起動ポリシーの挙動比較:no・on-failure・always・unless-stopped
4つのポリシーの違いは、「どんな終わり方をしたときに起こすか」と「dockerdが起動し直したときに起こすか」の2点です。まず同じホスト上で、異常終了・正常終了・手動停止を比べます。確認用のpolicy-labでは、同じ中身のコンテナをポリシーだけ変えて4つ並べました。
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と解釈されるのを避けるためです。
$ 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)です。
$ 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=7on-failureは終了コード0では一度も再起動しません。cronのRestarting (0)は、ポリシーがunless-stoppedだったから起きていたわけです。最後に、長く動くコンテナを手で止めます。
$ 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を返して抜けます。
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ソケットだけにするためです。
$ 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でした。
$ 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がコンテナを止め終えるまで待つための猶予です。
$ 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でした。起動し直した後の状態です。
$ 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 secondsalwaysとunless-stoppedが起動したのは想定どおりですが、on-failureは2つで結果が分かれました。SIGTERMで0を返したon-failureは起こされず、SIGKILLで137になったon-failure-killは起こされています。公式ドキュメントでは、on-failureはデーモンの再起動では再起動しないと説明されていますが、手元の計測では停止時の終了コードが0以外なら起動しました。次は、全部を手で止めてからデーモンを再起動します。
$ 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$ 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$ 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し、停電に近い止まり方を再現しました。
$ docker compose up -d$ 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すれば起動します。
$ 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 secondsDinDコンテナを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が起動した時点で、上の表の「デーモン再起動」または「強制終了」の列と同じ判定になる想定ですが、確かめてはいません。本番で頼る前に、検証機で一度試してください。
$ 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分以上待った結果です。
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$ 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を一度消します。
$ docker compose rm -sf worker別のターミナルで、workerのstartとdieを記録し始めます。--since 0sで過去分を出さず、--untilに5分後の時刻を渡しているので、記録は5分で自動的に終わります。
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までの間隔を計算します。
$ 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を同時に動かしました。
$ docker compose downdocker 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$ 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分動かすと、再起動回数は逆転しました。
$ 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.553518621Z10秒以上動いてから落ちるアプリには、バックオフが効きません。起動のたびに外部の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で試しました。
$ 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 agoupdateを実行したのは02:27:09(UTC)で、StartedAtはその後の02:27:57です。update時点ですでに次の起動を待っていた分は取り消されず、1回起動してからExited (0)で止まりました。60秒待ちに入っているコンテナなら、止まるまで最大でその分かかります。すぐ止めたいならstopを使います。
調査が終わったら元のポリシーに戻します。ここでdocker compose up -dだけを実行しても戻りません。
$ 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で作ったコンテナには再起動ポリシーが付かないため、調べている途中で落としてもループしません。
$ 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-debugrestart: unless-stoppedはrunのコンテナに引き継がれず、終了コード3で終わった後もrestarts=0のままです。調べる流れは「stopで止める→runで中を見る→compose.yamlを直す→up -d」を基本にし、docker updateは「動かし続けたいが、落ちても起こさないでほしい」ときだけに使います。
4つのループを直す
原因に合わせて直したcompose.yamlです。webの.envは前の章で修正済みです。
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で読めます。
$ 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 refusedbatchは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)で止まっているときに実行した結果です。
$ 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 runningexecはRestartingの間は使えず、止まっているコンテナ(上のbatchのようなExited)にも入れません。docker compose run --rm --entrypoint sh サービス名で、同じ定義の別コンテナを起動して中を調べます。
exec形式のcommandでコマンドが無いと、ループせずCreatedのまま止まる
shを通さない形(exec形式)にworkerのcommandを書き換えると、症状が変わります。
worker:
image: alpine:3
command: ["curl", "-fsS", "-o", "/dev/null", "http://web:8080/"]
restart: unless-stopped$ 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で測ったものです。
$ 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で測り直した結果です。
$ 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の応答を確かめます。
$ 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/
200batchが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は、それぞれの章に書いたコマンドで片付けます。
$ 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でないサービスから原因を読んでいきます。
次に読む
- Docker Compose入門:自宅サーバーでWeb・DB・リバースプロキシをまとめて動かす(healthcheckとdepends_onで起動順を揃える)
- Dockerとは?仮想マシンとの違い・仕組み・情シスでの使いどころを整理(コンテナとプロセスの関係)
- PrometheusとGrafanaで自宅サーバーを監視する(異常に人より先に気付くための監視基盤)