Docker Compose Patterns
Docker Compose Patterns
When running several containers on a single host (web, DB, cache, queue) Compose is the lightest path. One YAML file ties together dependencies, networks, volumes, and healthchecks. Not being a full orchestrator is both its strength and its limit. This piece covers the v1 vs v2 difference, the core keys of compose.yaml, healthcheck, env_file, profiles, and override patterns.
1. About Compose
Compose started as an external tool called Fig (2014); Docker acquired and absorbed it. v1 was a separate Python CLI (docker-compose), and from v2 (2021) it was rewritten in Go and merged into the docker compose subcommand. v2 differs in BuildKit support, profiles, and faster execution.
The recommended file name is compose.yaml or compose.yml. The older convention docker-compose.yml is still recognized.
2. Basic Structure
services:
web:
build: ./web
ports:
- "127.0.0.1:8080:8080"
environment:
DATABASE_URL: postgres://app:${DB_PASS}@db:5432/app
depends_on:
db:
condition: service_healthy
restart: unless-stopped
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: ${DB_PASS}
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
volumes:
db-data:
3. Core Keys
| Key | Role |
|---|---|
services |
Container-level definitions. |
volumes |
Named volumes. |
networks |
User-defined networks (a default is auto-created if omitted). |
secrets |
Secret value mount (file or external secret). |
configs |
Config file mount. |
profiles |
Optional service grouping. |
4. Dependencies and Healthcheck
depends_on alone is a weak guarantee that "another service is alive." Even when the network is up, the DB may not be ready to take queries. In v2, combine condition: service_healthy with healthcheck to verify true readiness.
5. env_file and ${VAR} Substitution
services:
web:
env_file: .env
environment:
LOG_LEVEL: ${LOG_LEVEL:-info}
env_file injects environment variables into the container. ${VAR} substitution is read by Compose from the .env file and the shell environment, filling in the YAML itself. The two have different meanings — one is the container environment, the other is text substitution in the Compose file.
6. profiles
Attach a profile to services that should only come up in certain environments:
services:
app: { ... }
pgadmin:
image: dpage/pgadmin4
profiles: ["dev"]
docker compose --profile dev up brings up the dev-profile services as well. Useful for naturally excluding tooling that's not needed in production.
7. Override File
The compose.override.yaml next to compose.yaml is auto-merged. For environment-specific branches it's clearer to use explicit files:
docker compose -f compose.yaml -f compose.prod.yaml up -d
Later files override earlier ones. Splitting dev / staging / prod differences into small patch files is a common pattern.
8. extends
Inherit and compose service definitions from another file. The right place to define a shared base in a large monorepo:
services:
app:
extends:
file: common.yaml
service: app-base
9. Other Paths
The comparisons to Compose are limited to single-host setups.
- systemd unit + Docker — service definitions managed by systemd. The dependency graph is systemd's
After=andRequires=. - Podman + quadlet — Podman's systemd integration. Container definitions in
*.containerfiles. - Nomad (HashiCorp) — a lightweight single-binary orchestrator. Single host up to hundreds of nodes.
- Kubernetes — a different weight class. Often considered overkill on a single host.
The moment you spread across multiple hosts, Compose isn't enough. Move on to Swarm, Nomad, or k8s.
10. Single-host Suitability
Strengths — small learning cost, every definition in one file, fast start and stop.
Limits — single host (no horizontal scaling). Zero-downtime deploys need an external tool or a manual procedure. Secret management and rolling upgrades are weak.
For most side projects and small-to-mid services, a single host plus Compose is enough.
11. Common Patterns
Build and image side by side:
services:
app:
image: ghcr.io/my/app:1.4.2
build: ./app
docker compose build then push in CI. The production host runs pull. Putting image and build together lets the two flows merge naturally.
Logging and restart — keep the default log driver from filling the disk:
services:
app:
restart: unless-stopped
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
12. Common Pitfalls
Missing bind address on a port — "8080:8080" binds to 0.0.0.0 and is reachable from outside. In production security models, "127.0.0.1:8080:8080" is often the right call.
Believing depends_on alone is enough — without condition, only the start order is guaranteed.
Volume vs bind mount permissions — when the host user (uid) and container user differ, write failures show up frequently.
Security of .env — stored in plaintext for Compose's ${VAR} substitution. Keep secrets in a separate tool (Docker secret, age, sops).
Name collisions — running multiple Compose projects on one host with the same project name mixes containers and networks. Use --project-name or separate directories.
Closing thoughts
Compose is the most intuitive tool for tying every concern of a single host into one YAML. With healthcheck plus condition: service_healthy, 127.0.0.1 binding, and override-file branching all in place, operational stability improves significantly.
Next
- caddy-not-nginx
- loopback-ssh-tunnel
Refer to the Compose docs, the Compose Specification, the Compose file reference, Healthcheck, Profiles, and Podman + quadlet.