Started is not ready
A Compose file that lists the database under depends_on guarantees only that Compose creates and starts the database container first. Docker documents that by default it waits until the dependency is running, not until the service inside it can accept connections. A database engine can take many seconds to initialise or replay its journal after a restart, so an app that connects on its first instruction fails, exits, and may be restarted in a loop. The symptom is an app that works on the second or third try, or only when someone starts the stack in two steps.
- depends_on without a condition orders container creation only.
- A fresh database volume takes longer than an existing one, so the failure shows on first deploy.
- The app's own retry logic is the other defence, but should not be the only one.
Give the dependency a health check that tests the real path, and wait for it
Define a healthcheck on the database service with a command that succeeds only when it can serve, with an interval, timeout, retries and a start period that suit the engine. Then change the app's dependency to the long form with the service_healthy condition. Compose creates the app only after the database reports healthy. The Compose reference says the healthcheck attribute has the same defaults as the Dockerfile instruction, so set the values you rely on explicitly rather than assuming them. A health check that is too lenient reports healthy too early, and one that is too strict keeps the stack from ever starting. The first deploy on an empty volume is where a lenient check bites. The official PostgreSQL image starts a temporary server to run its initialisation, and that server listens only on the Unix socket; pg_isready with no host connects over that socket, so it can report ready while other containers still cannot connect over the network. Ask for a TCP connection with -h 127.0.0.1 so the check goes through the same kind of connection the app will use, and test the first deploy on an empty volume, not only a restart on an existing one.
- Use a readiness command that exercises the service, not just a running process.
- For PostgreSQL, give pg_isready a host (-h 127.0.0.1) so it checks the TCP path.
- Test the first deploy on an empty volume, because that is when initialisation runs.
- Set a start period long enough for first initialisation, and keep the check cheap; it runs repeatedly.
If the matrix is wider than the box, scroll horizontally to read every column. Keyboard: focus the matrix and use Left/Right.
services:
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U app -d app"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
app:
image: registry.yourbusiness.co.uk/app:1.4.2
depends_on:
db:
condition: service_healthy
restart: unless-stoppedRestart behaviour after a reboot
The restart attribute decides what happens when a container exits or the host reboots. The default is no, so a stack with no restart setting stays down after a reboot. The documented values are always, on-failure with an optional retry limit, and unless-stopped, which restarts regardless of the exit code but not after you deliberately stopped the service. For a long-running server stack, unless-stopped or always is normal; on-failure suits a job that should not restart after a clean exit. A restart policy brings containers back after a reboot only if the Docker service itself starts at boot, which you should confirm on the host.
- Default is no: nothing returns after a reboot.
- unless-stopped respects a deliberate stop; always does not.
- Test it with a real reboot, not by reading the file.
Production habits for a single VPS
Docker's production guidance is short and worth following. Remove bind mounts that mount application code from the host, so the code is part of the image and cannot be edited in place. Map ports deliberately. Put the differences from development in a second file layered over the base one. To deploy a change, rebuild the image and recreate only the service that changed with the no-dependencies option, so the database is not restarted by a web deploy. Keep the previous image tag so a bad release can go back by changing one tag and recreating one service. Render the final configuration with the config command before you deploy and read it; its quiet option only validates the file and prints nothing.
- Use docker compose config -q to validate before a deploy; avoid pasting the full config output anywhere, because it resolves variables and can include their values.
- Pin image tags to a version, not latest, so a rollback is a tag change.
- Never publish a database port unless a client outside the host needs it.
What a health check does not do
A Compose health check reports a state; it does not repair anything. The Compose pages cited here do not say that an unhealthy container is restarted for you, so do not rely on that, and the dependency condition applies when a stack starts, not continuously. If you need a stuck service restarted automatically, that needs a supervisor you have configured and tested. The fixed Compose deployment job sets up the health checks, the startup order, the restart policy and the redeploy steps for one stack of up to four services, tests them with a real reboot, and does not promise uptime.
Sources and limits
- Docker: control startup order in Compose Checked 2026-10-11.
- By default Compose waits only until a dependency container is running, not until the service inside is ready.
- depends_on with condition service_healthy makes Compose wait until the dependency reports healthy; it needs a healthcheck on that service.
- Docker: Compose services reference Checked 2026-10-11.
- The restart values are no, always, on-failure and unless-stopped, with no as the default.
- The healthcheck attribute has the same defaults as the Dockerfile HEALTHCHECK instruction.
- Docker: use Compose in production Checked 2026-10-11.
- For production, remove bind mounts of application code, use a restart policy, and recreate only the changed service with --no-deps.
- Docker Official Image: postgres Checked 2026-10-11.
- On a fresh data directory the temporary server started for initialisation scripts listens only on the Unix socket, and the image accepts no incoming connections while it initialises a new database.
- Docker: docker compose config Checked 2026-10-11.
- docker compose config renders the Compose model after resolving variables, and its --quiet option only validates the configuration without printing anything.