Docker Compose lets you define and run multi-container applications with a single YAML file. Instead of running multiple docker run commands with dozens of flags, you describe your entire stack declaratively and bring it up with one command.
Your First Compose File
A compose.yaml file defines services (containers), networks, and volumes:
services:
web:
image: nginx:alpine
ports:
- "8080:80"
volumes:
- ./html:/usr/share/nginx/html
api:
build: ./api
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgres://user:pass@db:5432/myapp
depends_on:
- db
db:
image: postgres:16
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: myapp
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:Run it with docker compose up and you have an Nginx web server, a Node.js API, and a PostgreSQL database — all networked together automatically.
Essential Commands
# Start all services (foreground)
docker compose up
# Start in background (detached)
docker compose up -d
# Stop all services
docker compose down
# Stop and remove volumes (clean slate)
docker compose down -v
# Rebuild images before starting
docker compose up --build
# View logs
docker compose logs
docker compose logs api # specific service
docker compose logs -f # follow (live)
# Run a one-off command in a service
docker compose exec db psql -U user -d myapp
docker compose run api npm test
# Scale a service
docker compose up -d --scale worker=3
# Show running services
docker compose psKey Concepts
Services
Each service becomes a container. You can use a pre-built image or build from a Dockerfile:
services:
# Pre-built image from Docker Hub
redis:
image: redis:7-alpine
# Build from a Dockerfile
api:
build:
context: ./api
dockerfile: Dockerfile
args:
NODE_ENV: productionPorts
Map host ports to container ports with HOST:CONTAINER:
ports:
- "3000:3000" # same port
- "8080:80" # host 8080 → container 80
- "127.0.0.1:9090:80" # bind to localhost onlyVolumes
Persist data and share files between host and container:
services:
api:
volumes:
# Bind mount: host path → container path
- ./src:/app/src
# Named volume: managed by Docker
- node_modules:/app/node_modules
db:
volumes:
# Named volume for database persistence
- pgdata:/var/lib/postgresql/data
# Declare named volumes
volumes:
pgdata:
node_modules:Named volumes survive docker compose down. Bind mounts are great for development (live code reloading) but shouldn't be used in production.
Environment Variables
services:
api:
# Inline
environment:
- NODE_ENV=production
- API_KEY=secret123
# From a file
env_file:
- .env
- .env.localUse env_file for sensitive values — don't commit .env files to git. See our .gitignore guide for what to exclude.
depends_on
Control startup order. The basic form waits for the container to start, but not for it to be "ready":
services:
api:
depends_on:
# Basic: just wait for container to start
- redis
# With health check: wait until healthy
db:
condition: service_healthy
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user"]
interval: 5s
timeout: 5s
retries: 5Always use condition: service_healthy for databases. A started PostgreSQL container isn't ready to accept connections for several seconds.
Networks
By default, all services in a Compose file share a network and can reach each other by service name. You can define separate networks for isolation:
services:
web:
networks:
- frontend
api:
networks:
- frontend
- backend
db:
networks:
- backend # not reachable from web
networks:
frontend:
backend:Common Stack Templates
Node.js + PostgreSQL + Redis
services:
api:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://user:pass@db:5432/app
REDIS_URL: redis://cache:6379
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
volumes:
- ./src:/app/src # hot reload in dev
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: app
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user"]
interval: 5s
timeout: 5s
retries: 5
cache:
image: redis:7-alpine
command: redis-server --maxmemory 64mb --maxmemory-policy allkeys-lru
volumes:
pgdata:WordPress + MySQL
services:
wordpress:
image: wordpress:latest
ports:
- "8080:80"
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: wp
WORDPRESS_DB_PASSWORD: secret
WORDPRESS_DB_NAME: wordpress
volumes:
- wp_data:/var/www/html
depends_on:
db:
condition: service_healthy
db:
image: mysql:8
environment:
MYSQL_ROOT_PASSWORD: rootsecret
MYSQL_DATABASE: wordpress
MYSQL_USER: wp
MYSQL_PASSWORD: secret
volumes:
- db_data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
timeout: 5s
retries: 5
volumes:
wp_data:
db_data:Development vs Production
Use override files to separate dev and prod configuration:
services:
api:
build: .
environment:
- NODE_ENV=productionservices:
api:
build:
target: development
volumes:
- ./src:/app/src
environment:
- NODE_ENV=development
- DEBUG=true
ports:
- "9229:9229" # Node.js debuggerDocker Compose automatically merges compose.override.yaml on top of compose.yaml. For production, use an explicit file: docker compose -f compose.yaml -f compose.prod.yaml up.
Resource Limits
Prevent any service from consuming all host resources:
services:
api:
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
reservations:
cpus: "0.25"
memory: 128M
worker:
deploy:
replicas: 3 # run 3 instances
restart_policy:
condition: on-failure
max_attempts: 3Common Mistakes
- Assuming depends_on means "ready." Without a health check,
depends_ononly waits for the container to start. Your database might not be accepting connections yet. Always addhealthcheckandcondition: service_healthy. - Bind-mounting node_modules. Mounting
./:/appoverwrites the container'snode_moduleswith your host's (or empty) directory. Use a named volume fornode_modulesto prevent this. - Hardcoding secrets in compose files. Use
env_fileand add.envto.gitignore. For production, use Docker secrets or external secret managers. - Using
latesttag in production. Always pin image versions (postgres:16notpostgres:latest). Thelatesttag can change at any time and break your stack. - Forgetting to declare volumes. Named volumes must be declared in the top-level
volumes:section, not just referenced in services. Docker Compose may create anonymous volumes instead, which are easily lost.
Skip the server management
Need managed PostgreSQL, MySQL, or Redis alongside your containers? DigitalOcean Managed Databases handle backups, failover, and scaling so you can focus on your app. Pair with App Platform for zero-ops container deployment.
docker-compose.yml vs compose.yaml
The modern filename is compose.yaml (recommended by Docker). The old docker-compose.yml still works but is considered legacy. Similarly, use docker compose (with a space) instead of docker-compose (with a hyphen) — the latter is the standalone v1 binary which is no longer maintained.
Try It Yourself
Use our Docker Compose Validator to validate your compose files for syntax errors, missing dependencies, and best practices. Building a Dockerfile? The Dockerfile Validator checks for security issues, layer optimization, and common mistakes. And for validating your environment variables, try the .env File Validator.