====== Reliable Docker Compose Startup After a Reboot ======
A common and confusing failure mode: a Docker Compose stack that runs perfectly when you start it by hand starts misbehaving **after the server reboots**. One container pins a CPU core, the machine runs hot and the fans spin up — even though the overall system load is near zero. This page explains **why it happens** and shows a clean, general way to prevent it.
===== The trap: ''depends_on'' is not honored at reboot =====
Compose lets you declare start-up order and readiness gating:
services:
aggregator:
depends_on:
backend:
condition: service_healthy
This tells Compose: //"do not start ''aggregator'' until ''backend'' reports healthy."// It works exactly as expected — **but only when you run ''docker compose up''.**
The catch: after a **host reboot**, your containers are usually brought back not by Compose, but by the **Docker daemon's restart policy** (''restart: always'' or ''restart: unless-stopped''). The daemon restarts containers in an **arbitrary order and ignores ''depends_on'' entirely.**
> **Key fact:** ''depends_on: condition: service_healthy'' is respected by ''docker compose up'' — **never** by the Docker daemon when it restarts containers at boot.
===== Why this causes a runaway (hot fans, pinned core) =====
Many "aggregator", "proxy" or "gateway" style services discover their backends **once at start-up** and never retry. If such a service starts **before** its backend is ready — exactly what the reboot race produces — it can end up:
* holding a dead or half-open connection,
* retrying in a tight loop with no back-off,
* pinning a full CPU core at ~100%.
The result: the box is essentially idle (load near zero) yet **one core runs flat out**, temperatures climb and the fans ramp up — with no obvious "busy" process at a casual glance.
**Analogy:** it is like a courier who memorises the delivery list once, at the door, //before// the addresses are posted — then drives in circles forever because the list came up blank.
===== The clean fix: let systemd own start-up =====
The reliable pattern is to stop relying on the daemon's restart policy for boot ordering, and hand that job to **systemd** — which //does// respect Compose's health gating, because it runs ''docker compose up'' itself.
=== 1. Stop the daemon from race-starting containers ===
Change the restart policy in your ''docker-compose.yml'' from ''unless-stopped'' to ''on-failure'':
services:
backend:
restart: on-failure
aggregator:
restart: on-failure
**Why:** the daemon auto-starts ''always'' and ''unless-stopped'' containers at boot (that is the race). It does **not** auto-start ''on-failure'' containers at boot — yet ''on-failure'' still restarts a container that crashes while running. So you keep crash recovery but remove the boot race.
=== 2. Make systemd start the whole stack, in order ===
Create ''/etc/systemd/system/mystack.service'':
[Unit]
Description=My application stack (docker compose)
Requires=docker.service
After=docker.service network-online.target
Wants=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/mystack
ExecStart=/usr/bin/docker compose up -d --wait
ExecStop=/usr/bin/docker compose stop
[Install]
WantedBy=multi-user.target
Enable it once:
sudo systemctl daemon-reload
sudo systemctl enable --now mystack.service
Now systemd is the **single** thing that starts the stack at boot, and ''--wait'' blocks until every service is healthy — so the aggregator always comes up **after** its backends.
=== 3. Handle cold-boot transients with a retry ===
On a cold boot, a backend that needs the network (DNS, a remote API) may crash-loop for a few seconds until networking is fully up. ''docker compose up --wait'' is **intolerant** of this: the moment any container reports //unhealthy//, it aborts — it will not wait for the container to recover on its own.
Make the unit retry, so the stack converges by itself:
[Unit]
# ...
StartLimitIntervalSec=900
StartLimitBurst=6
[Service]
# ...
Restart=on-failure
RestartSec=20
If the first attempt fails because a backend was still settling, systemd waits and re-runs ''compose up --wait''. By the retry the backend is healthy, the aggregator starts, and the unit goes ''active'' — with **zero manual intervention**. Seeing a handful of restarts in the boot log is normal, not a fault:
systemctl show mystack.service -p NRestarts --value
===== How to diagnose the runaway =====
If a box runs hot after a reboot but overall load is low, look for a single pinned container:
# per-container CPU — the culprit sits near a full core
docker stats --no-stream
# confirm it is a spin, not real work, then read its logs
# for a tight retry / error loop (e.g. the same request repeating)
docker logs --tail 50
A quick manual recovery (until the boot fix is in place) is simply to restart the affected container //once its dependencies are up//:
docker restart
===== Optional: a runtime watchdog =====
The steps above fix **boot** ordering. A separate, rarer problem is a long-running proxy that holds a **persistent session** to a backend and has no working auto-reconnect: if the backend restarts //during normal operation//, the session dies and the proxy may spin again. If your software has this limitation, a small periodic health-check script (driven by a systemd timer or cron) that restarts the proxy when it detects the fault is a reasonable backstop. Guard it with a **minimum interval** between restarts so it can never storm.
===== Summary =====
* ''depends_on: service_healthy'' orders **''compose up''**, not the daemon at reboot.
* Use ''restart: on-failure'' so the daemon does not race-start containers at boot.
* Let a **systemd oneshot unit** run ''docker compose up -d --wait'' — it respects health ordering.
* Add ''Restart=on-failure'' + ''RestartSec'' so cold-boot transients self-heal.
* Result: the stack comes up in the correct order every time, and a single misbehaving service can no longer pin a core and roast your fans.