Docker Compose for Real Life: The Stack That Never Breaks
Stop copying Compose files from blog posts and praying. Real-world patterns for healthchecks, volumes, restart policies and logging that keep a homelab stack alive for years.
Every blog-post Compose file looks the same: a few services, a port mapping, a volumes: line, done. It works for a weekend demo. Then your NAS reboots at 3am, a database container comes up before it should, and your “stack” is a pile of crashed containers by breakfast.
After running dozens of stacks in production-ish homelab conditions, here are the four patterns that separate the Compose files that survive from the ones that get rewritten every month.
The restart policy that actually saves you
Tutorials default to restart: always. That’s the wrong default. always restarts a container even after you deliberately stopped it — which means docker compose down fights you, and a crash-looping container keeps hammering your logs forever.
Use this instead:
services:
app:
image: your-app:latest
restart: unless-stopped
unless-stopped gives you the same crash recovery, but respects an intentional docker compose stop. It is the only restart policy you need.
Healthchecks: your stack’s canary
Ports being open doesn’t mean your app is ready. Databases especially — Postgres accepts TCP connections before it’s actually able to serve queries, and the container that depends on it starts, fails, and crash-loops in a race you’ll debug for an hour.
Give every service a real healthcheck:
services:
db:
image: postgres:17
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER}"]
interval: 10s
timeout: 5s
retries: 5
app:
image: your-app:latest
depends_on:
db:
condition: service_healthy
Now app won’t start until Postgres answers pg_isready. Note the $${...} — Compose escapes the variable so the container sees it, not the host.
Volumes: named for data, bind for config
Databases and stateful app data belong in named volumes — Docker manages the permissions, backups are one tar away, and you never have to worry about UID mismatches on a shared NAS mount.
volumes:
pgdata:
services:
db:
volumes:
- pgdata:/var/lib/postgresql/data
Config you actually edit — Caddyfiles, compose overrides, app configs — belongs in bind mounts so you can edit it from the host without entering the container. Mixing the two is the difference between “oh no, my data” and “let me just fix the config”.
Logging: stop letting logs eat your disk
The default JSON-file logging driver grows without limit. On a small homelab disk, a chatty container will fill the drive in a weekend. Cap it:
services:
app:
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
A skeleton that never breaks
Put it together and the core stack of a serious homelab — reverse proxy, database, app — looks like this:
services:
caddy:
image: caddy:2
ports: ["80:80", "443:443"]
volumes: ["./Caddyfile:/etc/caddy/Caddyfile:ro"]
restart: unless-stopped
db:
image: postgres:17
volumes: [pgdata:/var/lib/postgresql/data]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER}"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
app:
build: .
depends_on:
db:
condition: service_healthy
restart: unless-stopped
logging:
driver: json-file
options: {max-size: "10m", max-file: "3"}
volumes:
pgdata:
Four habits — unless-stopped, real healthchecks, named volumes for data, capped logs — and your stack stops being a source of 3am panic. It just runs. That’s the whole point of Compose.
Written by
JellyPuff Media
Editor-in-Chief
Auckland-based publisher covering AI, self-hosting and tech in New Zealand.
Keep reading
Related articles
Building the Perfect NAS Dashboard with Homepage
Turn a wall of bookmarks into a living control room. Build a gethomepage.dev dashboard with live Docker container cards, resource widgets and a status board that actually updates.
The Open Source Stack That Runs My Homelab
A NUC, a NAS, and zero licence fees. The battle-tested open source stack that runs my homelab — from containers and DNS to dashboards and backups — and what each tool actually earns its place.