← All 45 books Docker Compose in production, one step per page Get the full edition · £10
One step per page

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.


Steve Hodgkiss 6 steps

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

Contents


Part 1 · The two-file habit4
The override file5
restart: unless-stopped6
healthcheck:7
Part 2 · The blast radius
Networks8
Part 3 · The host survives
Log rotation9
Part 4 · Staying current
The pre-flight checklist10
Part 1 of 4
Shape and server, separated
1

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.


In this part
  1. 01The override file
  2. 02restart: unless-stopped
  3. 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.

Docker Compose · No. 01
Deploy

The override file

Two files: the shape, and the server

ONE FILE FOR THE SHAPE. ONE FOR THE SERVER. -f STAMPS THEM.compose.yamlservices: web: build: . ports: - 3000:3000 db: image: postgrescompose.production.yamlservices: web: restart: unless-stopped mem_limit: 512m logging: max-size: 10mmerged configweb: ports: 3000 restart: unless-stopped mem_limit: 512m logging: 10mdocker compose -f compose.yaml -f compose.production.yaml up -d

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.

DO IT THIS WEEK

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.

Docker Compose · No. 02
Survive

restart: unless-stopped

Dead containers stay dead by default

THE DEFAULT POLICY IS NO. A DEAD CONTAINER STAYS DEAD.restart: nothe default. crash = deadunless-stoppedcomes back, respects a stopserver reboots at 04:00stays downrebootpolicy: back up within secondsalways also restarts stopped-by-hand services

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.

DO IT THIS WEEK

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.

Docker Compose · No. 03
Survive

healthcheck:

Started isn't ready

STARTED ISN'T READY. THE TEST COMMAND DECIDES.runninghealthyprocess is upthe app answershealthcheck: test: curl -f localhost interval: 10s timeout: 3s retries: 3checkshealthyhealthyhealthythree fails in a row: the container is marked unhealthy

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.

DO IT THIS WEEK

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.

Docker Compose · No. 04
Protect

Networks

Service names are the addresses

EVERY SERVICE ON ONE DEFAULT BRIDGE, REACHABLE BY NAME.frontend netproxywebhttp://webbackend netwebdbdb:5432web joins both. proxy and db share nothing.hardcode an IP? it changes on recreatedns by service name, always current

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.

DO IT THIS WEEK

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.

Docker Compose · No. 05
Protect

Log rotation

Unbounded logs fill the disk at 3am

JSON-FILE LOGS GROW WITHOUT LIMIT. UNLESS YOU CAP THEM.defaultweeks pass, the disk fillsmax-size: 10mrolls over, max-file: 3logging: driver: json-file options: max-size: 10m max-file: 3a full disk takes every service down, not just the chatty one

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.

DO IT THIS WEEK

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.

Docker Compose · No. 06
Deploy

The pre-flight checklist

Ten lines before every deploy

TEN LINES BEFORE EVERY DEPLOY. ONE PAGE.restart: unless-stoppedhealthcheck: test, interval, retriesdepends_on: condition: service_healthysecrets: files, not environmentnetworks: split who talks to whomports: 127.0.0.1 or none, behind proxyvolumes: named, for statelogging: max-size, max-filemem_limit / cpus per serviceimages pinned, no :latestrun it againstthe merged filedocker compose configsees the final shapeone read of the file, every time, before it meets the internet

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.

DO IT THIS WEEK

Run docker compose -f compose.yaml -f compose.production.yaml config and check it against this list. Fix what's missing, then deploy.

Index

Index


healthcheck:7
Log rotation9
Networks8
restart: unless-stopped6
The override file5
The pre-flight checklist10