Docker Compose Patterns Every Developer Should Know
import Post from ’../../layouts/Post.astro’; export const prerender = true;
Docker Compose is one of those tools people use for years without exploring what it can really do. Here are the patterns that will make your development workflow significantly better.
1. Health Checks — Because Starting Fast ≠ Ready Fast
services:
postgres:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
start_period: 10s
api:
depends_on:
postgres:
condition: service_healthy
command: sh -c "npm run migrate && npm start"
condition: service_healthy blocks the api container until Postgres is genuinely ready — not just started.
2. Named Volumes for Persistent Dev Data
volumes:
postgres_data:
redis_data:
services:
postgres:
image: postgres:16-alpine
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
Data survives docker compose down -v if you remove the volume definition. Your DB doesn’t get wiped every time you restart.
3. Multiple Dockerfiles for Dev vs Prod
services:
api:
build:
context: .
dockerfile: Dockerfile
profiles: ["dev", "default"]
api:build:
build:
context: .
dockerfile: Dockerfile.optimized
profiles: ["prod"]
Run docker compose --profile prod up for production builds, docker compose up for dev.
4. Resource Limits — Because One Container Shouldn’t Kill Your Machine
services:
postgres:
image: postgres:16-alpine
deploy:
resources:
limits:
cpus: '1'
memory: 1G
reservations:
cpus: '0.5'
memory: 256M
api:
build: .
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
5. env_file — Separate Secrets from Config
# .env (committed, with defaults)
DATABASE_HOST=localhost
LOG_LEVEL=info
# .env.local (gitignored, secrets)
DATABASE_PASSWORD=super_secret_change_me
JWT_SECRET=change_me_to_random_string
services:
api:
env_file:
- .env
- .env.local
Secrets in .env.local, defaults in .env. Nobody accidentally commits the database password.
6. Networks — Isolated Service Communication
services:
frontend:
networks:
- web
api:
networks:
- web
- backend
postgres:
networks:
- backend
# Postgres is NOT reachable from frontend — only from api
networks:
web:
driver: bridge
backend:
driver: bridge
internal: true # No internet access — truly isolated
Internal networks are great for databases: services can reach the DB, but the DB can’t reach the internet.
7. The Override Pattern — Don’t Repeat Yourself
# docker-compose.yml — base config (committed)
version: '3.9'
services:
api:
build: .
ports:
- "3000:3000"
postgres:
image: postgres:16-alpine
# docker-compose.override.yml — dev overrides (gitignored)
services:
api:
environment:
NODE_ENV: development
volumes:
- .:/app
command: npm run dev
postgres:
ports:
- "5432:5432" # Expose DB port only in dev
docker compose up automatically reads override.yml. Prod uses only docker-compose.yml.
8. Wait-for Script — The Universal Health Check Pattern
#!/bin/bash
# wait-for.sh — put in ./scripts/wait-for.sh
host="$1"
shift
cmd="$@"
until nc -z "$host"; do
echo "Waiting for $host..."
sleep 1
done
exec $cmd
services:
api:
depends_on:
postgres:
condition: service_started
command: ["./scripts/wait-for.sh", "postgres:5432", "npm", "start"]
Works for any service, any language, no healthcheck configuration needed.
These eight patterns cover the most common dev workflow headaches. Start with health checks and depends_on: condition: service_healthy — you’ll immediately stop seeing “connection refused” errors on startup.
Enjoying the content? Here are tools I personally use and recommend:
- 🌐 Hosting: Bluehost — what this blog runs on
- 🛒 Tech Gear: My Amazon Store — keyboards, monitors, dev tools I use
Purchases through my links help keep this blog ad-free 💙
Enjoyed this post?
Subscribe to the newsletter or follow on YouTube for more dev content.
🎬 Watch Shorts