Docker Compose in production, one step per page
The twenty steps between a compose file that works on your laptop and one that survives a server: restart policies, healthchecks, secrets, log caps and the override file that holds them all.
Your laptop file is not a server file.
Docker Compose in production, one step per page
The twenty steps between a compose file that works on your laptop and one that survives a server: restart policies, healthchecks, secrets, log caps and the override file that holds them all.
Set in Space Grotesk, Inter and JetBrains Mono (SIL Open Font License).
Field behaviour verified against the official Docker documentation: docs.docker.com, including the Compose file reference (restart, healthcheck, depends_on, secrets, networks, ports, volumes, logging, mem_limit, cpus, stop_grace_period, pull_policy, env_file, name), the Compose how-tos (Use Compose in production, Networking in Compose, Use Compose Watch) and the Compose CLI reference.
This book is the production companion to 'Docker, one command per page' and stands alone: it assumes you can write a working compose.yaml and teaches what changes on a server.
Your purchase is for personal use only. You do not have redistribution rights: please do not share, resell, or republish this book or its pages.
© 2026 Steve Hodgkiss. All rights reserved. Personal use only; no redistribution rights.
Edition 1.0 · stevehodgkiss.net
Contents
The two-file habit
The base file describes the app; the production file describes the server. Merging them with -f flags, then making containers survive crashes and reboots with a restart policy.
- 01The override file
- 02restart: unless-stopped
- 03healthcheck:
Compose merges a production file over the base file with repeated -f flags, so server-only lines never live in the app's shape file.
The override file
Let's say your compose.yaml went from laptop to VPS untouched, and it's full of things no server should run.
Compose can stack files: -f compose.yaml -f compose.production.yaml, and the second file's lines land on top of the first. Same services, new values. The base keeps describing the app. The override describes the server.
The mistake is growing one file until it's both things at once, then deploying it to CI to find out.
One file for the shape, one file for the server.
Create an empty compose.production.yaml next to compose.yaml. Deploy with docker compose -f compose.yaml -f compose.production.yaml up -d. Every page after this adds lines to it.
The restart policy the platform applies on container termination. The default is no. unless-stopped restarts on any exit but stays down after a manual stop.
restart: unless-stopped
Let's say the VPS rebooted last night for a kernel update. Your app didn't come back.
The default restart policy is no. Crash, reboot, daemon restart: the container stays dead until a human types something. unless-stopped brings it back in all those cases, and still respects a manual stop.
always also restarts the ones you stopped on purpose, which is a confusing 3am surprise of its own.
If you didn't stop it, it should come back by itself.
Add restart: unless-stopped under web and db in the production file. Kill a container with docker kill, then watch it return with docker compose ps.
The healthcheck attribute declares a test command run on a schedule to mark a container healthy or unhealthy. Started is not ready.
healthcheck:
Let's say the app container is Up, the proxy sends it traffic, and every request fails. Up only means the process exists.
A healthcheck is a command Docker runs inside the container on a schedule: test, interval, timeout, retries. Pass and it's marked healthy. Fail three times in a row and it's marked unhealthy, where your scripts and conditions can see it.
A check that hits the homepage but not the database proves nothing about readiness. Check what must be true to serve.
Started is the process. Healthy is the promise.
Give web a healthcheck: test curl -f http://localhost:3000 or your framework's ready endpoint, interval 10s, timeout 3s, retries 3. Watch it flip with docker compose ps.
Compose creates one default bridge network per project and services reach each other by service name. Custom networks control which services can talk to each other.
Networks
Let's say your db container restarted with a new IP, and half your config broke overnight.
Every project gets a default bridge network, and containers find each other by service name: web connects to db:5432. Your own networks decide who talks to whom: proxy and web on one, web and db on another.
Container IPs are dynamic. Hardcoding one is a delayed outage.
The name is the address. The networks are the walls.
Add frontend and backend networks, attach proxy and web to frontend, web and db to backend. Then delete db's ports entry entirely and connect through the network.
The default json-file log driver grows log files without limit. The logging options max-size and max-file cap each service's disk use.
Log rotation
Let's say the server died overnight and nothing on it would start. The disk was full. It was logs.
The default json-file driver never rotates: every service's stdout lands on disk forever. logging options cap it: max-size per file, max-file files kept.
A full disk doesn't just break the chatty service, it breaks every write on the box, including the database you were careful with.
If you didn't set a cap, the disk is a countdown.
Add logging with driver json-file, options max-size 10m, max-file 3 to web and db in the production file. Check usage now with docker system df.
Ten lines to verify in the merged Compose file before any deploy: restart, healthcheck, condition, secrets, networks, loopback ports, named volumes, log caps, memory and CPU limits, pinned images.
The pre-flight checklist
Let's say the file grew all week and deploys happen from memory. This page is the memory.
Read the merged file and check ten lines. docker compose config prints exactly what will run, after all overrides land.
One read, every time, before it meets the internet. Two minutes.
Every outage in this book was a missing line from this list.
Two minutes of reading beats a night of paging.
Run docker compose -f compose.yaml -f compose.production.yaml config and check it against this list. Fix what's missing, then deploy.