A basic docker-compose.yml is just the starting point. Production workloads need healthchecks, profiles, override files, and resource management.
healthcheck for depends_on
By default depends_on only guarantees that a container has started, not that the service inside it is ready. Postgres may be starting up but not yet accepting connections. The solution is healthcheck.
services:
db:
image: postgres:15
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 5s # how often to check
timeout: 3s # timeout for a single check
retries: 5 # number of retries before marking unhealthy
start_period: 10s # grace period before the first check
web:
build: .
depends_on:
db:
condition: service_healthy # wait for the healthcheck to pass
Available condition values:
- service_started — container is running (default)
- service_healthy — healthcheck passed
- service_completed_successfully — exited with code 0 (for init containers)
Healthcheck examples for common services:
# Redis
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 3
# HTTP service
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 10s
timeout: 5s
retries: 3
start_period: 30s
profiles: Dev and Prod Configurations
Profiles let you define services that only start in specific scenarios.
services:
web:
build: .
ports:
- "8000:8000"
# No profile — always starts
db:
image: postgres:15
# No profile — always starts
pgadmin:
image: dpage/pgadmin4
profiles: [dev] # dev only
ports:
- "5050:80"
mailhog:
image: mailhog/mailhog
profiles: [dev] # dev only
ports:
- "8025:8025"
nginx:
image: nginx:alpine
profiles: [prod] # prod only
ports:
- "80:80"
- "443:443"
# Start only the base services
docker compose up -d
# Start with the dev profile
docker compose --profile dev up -d
# Multiple profiles
docker compose --profile dev --profile monitoring up -d
Override: docker-compose.override.yml
Docker Compose automatically loads docker-compose.override.yml and merges it with the main file. This is great for per-developer local settings.
# docker-compose.yml — shared, committed to git
services:
web:
build: .
ports:
- "8000:8000"
environment:
LOG_LEVEL: info
db:
image: postgres:15
# docker-compose.override.yml — local, in .gitignore
services:
web:
volumes:
- .:/app # live reload: mount source code into the container
environment:
LOG_LEVEL: debug
DEBUG: "true"
command: python manage.py runserver 0.0.0.0:8000
db:
ports:
- "5432:5432" # in dev: postgres reachable from the host
Specifying files explicitly:
# Use specific files
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
# Main file only (skip the automatic override)
docker compose -f docker-compose.yml up -d
Variables and ${VAR} Interpolation
# docker-compose.yml
services:
web:
image: ${REGISTRY:-docker.io}/${IMAGE_NAME}:${IMAGE_TAG:-latest}
ports:
- "${APP_PORT:-8000}:8000"
deploy:
replicas: ${WEB_REPLICAS:-1}
Substitution syntax:
- ${VAR} — variable value; error if unset
- ${VAR:-default} — value or default if unset
- ${VAR:?error message} — error with a message if unset
# Preview the final configuration after substitution
docker compose config
deploy: Replicas and Resources
The deploy section controls resource allocation (relevant for Docker Swarm and Compose v3+; partially applies in local Compose as well).
services:
web:
build: .
deploy:
replicas: 3 # number of container copies
update_config:
parallelism: 1 # update one replica at a time
delay: 10s # pause between replicas
restart_policy:
condition: on-failure
max_attempts: 3
resources:
limits:
cpus: "0.5" # at most 50% of one CPU core
memory: 512M # at most 512 MB of RAM
reservations:
cpus: "0.1"
memory: 128M
db:
image: postgres:15
deploy:
resources:
limits:
memory: 1G
reservations:
memory: 256M
Resource limits are important protection against a single container consuming all available host memory.
Management Commands
# Starting
docker compose up -d # start in the background
docker compose up -d --build # rebuild images first
docker compose up -d web db # specific services only
# Stopping
docker compose down # stop and remove containers
docker compose down -v # also remove volumes
docker compose down --rmi local # also remove local images
docker compose stop web # stop a service without removing it
# Logs
docker compose logs -f # all logs in real time
docker compose logs -f web # logs for a specific service
docker compose logs --tail=100 web # last 100 lines
# Running commands
docker compose exec web bash # open a shell in the container
docker compose exec web python manage.py migrate
docker compose run --rm web pytest # run a one-off container
# Scaling
docker compose up -d --scale web=3 # run 3 copies of web
# Information
docker compose ps # status of all services
docker compose top # processes inside containers
docker compose images # service images
docker compose port web 8000 # find the mapped port
Full Example: Production-like Stack
version: "3.8"
services:
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
web:
condition: service_healthy
profiles: [prod]
web:
build: .
image: ${IMAGE_NAME:-myapp}:${IMAGE_TAG:-latest}
environment:
DATABASE_URL: postgresql://${DB_USER}:${DB_PASS}@db:5432/${DB_NAME}
SECRET_KEY: ${SECRET_KEY}
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 10s
timeout: 5s
retries: 3
start_period: 20s
deploy:
replicas: ${WEB_REPLICAS:-2}
resources:
limits:
cpus: "1.0"
memory: 512M
db:
image: postgres:15
environment:
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASS}
POSTGRES_DB: ${DB_NAME}
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
interval: 5s
timeout: 3s
retries: 5
deploy:
resources:
limits:
memory: 1G
volumes:
pgdata:
💬 Comments (0)
No comments yet
Be the first to share your opinion about this article!