Docker Compose
Article.
Compose defines a multi-container application in one file (compose.yaml):
services, networks, volumes, and how they start. It runs on one host: the
right tool for local development, integration tests in CI, and small
single-machine deployments, not an orchestrator for many machines.
The API crash-loops because Postgres is not ready yet. Fix it in
Compose. Everything below was run with Docker Compose v5.5.1 and
postgres:18.
The tool and the file today
docker compose up -d # the plugin; `docker-compose` (V1) is end of lifeThe CLI went from v2 straight to v5, skipping 3 and 4 to avoid confusion with the old file-format versions. The file itself has no version any more:
WARN: the attribute `version` is obsolete, it will be ignored,
please remove it to avoid potential confusionDelete version: "3.9"; the Compose Specification replaced the 2.x/3.x
formats, and the canonical file name is compose.yaml.
Start order is not readiness
services:
db:
image: postgres:18
app:
image: myapp
depends_on: [db] # waits for the db container to STARTthree runs, the app connecting as soon as it starts:
app: OperationalError: connection failed: ... connection refused (3 of 3)Short-form depends_on only orders container start. Postgres takes a few
seconds after starting before it accepts connections, and the app tried
during that window every time.
Healthchecks and conditions
Readiness needs a healthcheck and a condition:
services:
db:
image: postgres:18
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 1s
retries: 30
migrate:
image: myapp
command: ["alembic", "upgrade", "head"]
depends_on:
db: {condition: service_healthy}
app:
image: myapp
depends_on:
db: {condition: service_healthy}
migrate: {condition: service_completed_successfully}migrations applied
app: connected after 0.0s
db running Up 19 seconds (healthy)service_completed_successfully turns a one-shot container into a gate:
migrations finish before the app starts, and a failed migration stops the app
from starting at all.
And an hour later?
The conditions apply at startup only. When Postgres restarts later, Compose
does not stop the app or wait; the app sees broken connections. So the app
still needs its own resilience: connection retries with backoff, a pool that
validates connections (pool_pre_ping), and a restart policy
(restart: unless-stopped). The healthcheck fixes the race at startup; only
the application fixes it at run time.
Override files merge, they do not replace
# compose.yaml # compose.override.yaml
services: services:
app: app:
ports: ["8000:8000"] ports: ["8001:8000"]docker compose config -> published: "8000" and published: "8001"
with ports: !override ["8001:8000"] -> published: "8001"compose.override.yaml is merged automatically on top of compose.yaml.
Single values are replaced, but sequences such as ports are appended,
so both ports were published. The !override tag replaces the list and
!reset removes it. docker compose config prints the merged model: check
it before assuming what runs.
Postgres 18 moved its data directory
docker run -v pgdata:/var/lib/postgresql/data postgres:18
Error: in 18+, these Docker images are configured to store database data in a
format which is compatible with "pg_ctlcluster" ...From 18 the image keeps data under /var/lib/postgresql/18/docker and declares
the volume at /var/lib/postgresql; mounting the old path stops it from
starting. Mount /var/lib/postgresql, which also makes major-version upgrades
with pg_upgrade --link possible inside one volume.
db:
image: postgres:18
volumes: ["pgdata:/var/lib/postgresql"]
volumes:
pgdata:Secrets and configuration
services:
app:
env_file: .env # not committed
secrets: [db_password] # mounted at /run/secrets/db_password
secrets:
db_password:
file: ./secrets/db_password.txtPlaintext passwords inline in compose.yaml end up in git. Variables can come
from .env (interpolated as ${VAR}) and secrets from files mounted under
/run/secrets/, which keeps them out of docker inspect output and out of
the image.
Where it breaks in production
Compose as the production orchestrator
One host, no rescheduling when the machine dies, no rolling updates across machines. For a small internal tool that is fine and simpler than Kubernetes; for anything that must survive a host failure, it is not. Know which one you are running.
Scaling a service with a published port
docker compose up --scale web=3 with ports: ["5000:5000"] fails with
“port is already allocated”: three containers cannot bind one host port. Put
a reverse proxy in front, or publish a range.
Data in an anonymous volume
A database without a named volume gets an anonymous one; docker compose down -v, or a project rename, and the data is gone. Name every volume that holds
state.
Bind mounts in CI and production
.:/app is right for development (live code reload) and wrong anywhere else:
the container runs whatever is on the host disk, not the image you tested.
Use the image’s own files outside development.
The trade not taken
Kubernetes, or a managed service such as ECS, is the multi-machine answer: scheduling, self-healing, rolling deploys, service discovery across nodes. It costs a control plane and a much larger configuration surface.
Compose stays the right tool for development and CI even in Kubernetes shops, because the whole stack starts with one command on a laptop; the mistake is only using it where the guarantees of an orchestrator are needed (Scaling containers).
Related
Interview angle 4
- “The API crash-loops because Postgres is not ready. Fix it.” - a
healthcheck on the database (
pg_isready) anddepends_onwithcondition: service_healthy. Plaindepends_ononly orders start: the app failed 3 of 3 times without it.- Follow-up: “Migrations must finish first.” - a one-shot
migrateservice andcondition: service_completed_successfullyon the app. - Follow-up: “Postgres restarts an hour later.” - Compose does nothing; the app needs retries, connection validation and a restart policy.
- Follow-up: “Migrations must finish first.” - a one-shot
- “The override should replace a port, but both are published. Why?” -
lists are appended when files merge.
docker compose configshows the merged result;!overridereplaces the list. - “Why ‘version is obsolete’?” - the Compose Specification dropped the file version; the CLI’s own version (v5) is unrelated. Delete the key.
- “Compose or Kubernetes?” - Compose for one host: development, CI, small deployments. Kubernetes when the service must survive machine failures and deploy across nodes.