Docker Compose Patterns I Actually Use
Six compose patterns that survive production: healthchecks, depends_on with conditions, profiles, and restart semantics.
Docker Compose is the lingua franca of self-hosting. After running a homelab for three years, six patterns have survived every refactor.
1. Healthchecks Are Not Optional
Without a healthcheck, Compose has no way to know whether a container is
actually serving traffic — only that the process hasn’t exited. A database
that’s stuck in crash-loop recovery will look “healthy” to depends_on.
services:
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 10s
timeout: 3s
retries: 5
2. depends_on With Conditions
The bare depends_on: [db] only waits for the container to start. Pair it
with a service condition to wait until the dependency is genuinely ready:
api:
depends_on:
db:
condition: service_healthy
This single change eliminates an entire class of “first boot fails, retry works” bugs.
3. Compose Profiles
A single compose.yaml can serve multiple environments with profiles:
services:
app: { ... }
debug-tools:
profiles: [debug]
image: nicolaka/netshoot
docker compose up starts the app; docker compose --profile debug up adds
the troubleshooting sidecar.
4. Restart Policies
restart: unless-stopped is almost always what you want. always will
re-launch containers after a reboot even if you deliberately stopped them,
which is rarely the intent on a development machine.
5. Named Volumes Over Bind Mounts for State
Bind mounts are convenient for source code but treacherous for databases — filesystem permission and ownership semantics will eventually bite. Use named volumes for anything you can’t recreate from a config file.
6. One Compose File, Many Overlays
The -f flag composes multiple files. Keep a base compose.yaml for the
service definitions and a compose.production.yaml that overrides labels,
resource limits, and networks. This keeps environment-specific noise out of
the main file.
Wrapping Up
These patterns aren’t exotic — they’re the boring defaults I keep coming back to. The unifying principle: make the system’s actual state match what Compose thinks it is.