Compose Files: Writing docker-compose.yml
docker-compose.yml is the blueprint for your application. It defines everything. Services. Networks. Volumes. Configuration. Understanding how to write it makes Compose powerful.
🎯 The Big Picture​
Think of docker-compose.yml like a recipe card. It lists all ingredients (services). How to prepare them (configuration). How they work together (networking). One card. Complete meal.
Writing a good Compose file is like writing a good recipe. Clear. Complete. Works every time.
File Structure (Modern Compose - No Version Field!)​
Basic structure (modern Compose - no version needed):
services: # Your containers
service-name:
# Service configuration
networks: # Custom networks (optional)
network-name:
# Network configuration
volumes: # Named volumes (optional)
volume-name:
# Volume configuration
configs: # Config files (optional)
config-name:
# Config configuration
secrets: # Secrets (optional)
secret-name:
# Secret configuration
Think of it as: Multiple sections. Services (what to run). Networks (how to connect). Volumes (where to store data). Configs and secrets (modern features for better security).
Important: The version field is deprecated in modern Docker Compose. Docker Compose automatically detects the format. Your files are cleaner without it!
Service Configuration​
A service is a container. Configure it:
services:
web:
image: nginx # Docker image
container_name: my-web # Container name (optional)
ports: # Port mappings
- "8080:80"
environment: # Environment variables
- NODE_ENV=production
volumes: # Volume mounts
- ./data:/app/data
networks: # Networks to join
- app-network
depends_on: # Dependencies
- db
restart: always # Restart policy
Each option configures the container. All in one place.
Image vs Build​
Two ways to get an image:
Using Existing Image​
services:
web:
image: nginx:latest
Use when: Image exists in registry. Simple. Fast.
Building from Dockerfile​
services:
app:
build:
context: .
dockerfile: Dockerfile
Or shorter:
services:
app:
build: .
Use when: You have a Dockerfile. Need to build custom image.
Think of it as: Using a pre-made dish (image) vs cooking from recipe (build).
Port Mappings​
Map container ports to host:
services:
web:
image: nginx
ports:
- "8080:80" # host:container
- "8443:443" # Multiple ports
- "3000" # Random host port
Format: "host-port:container-port"
Examples:
"8080:80"- Host 8080 → Container 80"3000"- Random host port → Container 3000"127.0.0.1:8080:80"- Only localhost
Environment Variables​
Set environment variables:
services:
app:
image: my-app
environment:
- NODE_ENV=production
- DB_HOST=db
- DB_PORT=5432
Or using a file:
services:
app:
image: my-app
env_file:
- .env
Or both:
services:
app:
image: my-app
env_file:
- .env
environment:
- DEBUG=true # Overrides .env
Volumes​
Mount volumes:
services:
db:
image: postgres:14
volumes:
# Named volume
- postgres-data:/var/lib/postgresql/data
# Bind mount
- ./config:/etc/postgresql
# Anonymous volume
- /tmp
Define volumes:
volumes:
postgres-data:
Think of it as: Where to store data. Named volumes for persistence. Bind mounts for development.
Networks​
Configure networking:
services:
web:
image: nginx
networks:
- frontend
- backend
networks:
frontend:
backend:
driver: bridge
Default network:
- All services on same Compose file share default network
- Can reach each other by service name
- No configuration needed
Custom networks:
- Isolate services
- Better organization
- Custom configuration
Dependencies​
Define service dependencies:
services:
db:
image: postgres:14
app:
image: my-app
depends_on:
- db
What it does:
- Starts
dbbeforeapp - Waits for
dbto start (not ready) - Ensures order
Health checks:
services:
db:
image: postgres:14
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
interval: 10s
timeout: 5s
retries: 5
app:
image: my-app
depends_on:
db:
condition: service_healthy
Now waits for db to be healthy, not just started.
The Recipe Card Analogy​
Think of docker-compose.yml like a recipe card:
Services: Ingredients
- What you need
- How much
- How to prepare
Networks: How ingredients combine
- Which go together
- How they connect
Volumes: Storage
- Where to keep things
- What to preserve
Dependencies: Order of operations
- What to do first
- What depends on what
Once you see it this way, Compose files make perfect sense.
Real-World Example: Complete Application​
Let me show you a complete example with modern best practices:
services:
# PostgreSQL Database
postgres:
image: postgres:16-alpine
container_name: app-postgres
environment:
POSTGRES_USER: ${DB_USER:-appuser}
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
POSTGRES_DB: ${DB_NAME:-myapp}
POSTGRES_INITDB_ARGS: "-E UTF8 --locale=C"
volumes:
- postgres-data:/var/lib/postgresql/data
- ./init-scripts:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-appuser} -d ${DB_NAME:-myapp}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
restart: unless-stopped
deploy:
resources:
limits:
cpus: '2.0'
memory: 2G
reservations:
cpus: '1.0'
memory: 1G
networks:
- backend
secrets:
- db_password
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# Redis Cache
redis:
image: redis:7-alpine
container_name: app-redis
command: >
redis-server
--requirepass ${REDIS_PASSWORD}
--appendonly yes
--maxmemory 256mb
--maxmemory-policy allkeys-lru
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
networks:
- backend
# Node.js Application
app:
build:
context: .
dockerfile: Dockerfile
args:
NODE_ENV: production
container_name: app-backend
environment:
NODE_ENV: ${NODE_ENV:-production}
DB_HOST: postgres
DB_PORT: 5432
DB_USER: ${DB_USER:-appuser}
DB_NAME: ${DB_NAME:-myapp}
DB_PASSWORD_FILE: /run/secrets/db_password
REDIS_HOST: redis
REDIS_PORT: 6379
REDIS_PASSWORD: ${REDIS_PASSWORD}
PORT: 3000
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
restart: unless-stopped
deploy:
replicas: 2
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.5'
memory: 256M
networks:
- backend
- frontend
secrets:
- db_password
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# Nginx Web Server
nginx:
image: nginx:alpine
container_name: app-nginx
ports:
- "${HTTP_PORT:-80}:80"
- "${HTTPS_PORT:-443}:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./ssl:/etc/nginx/ssl:ro
- nginx-cache:/var/cache/nginx
- nginx-logs:/var/log/nginx
depends_on:
app:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 30s
timeout: 10s
retries: 3
restart: unless-stopped
deploy:
resources:
limits:
cpus: '0.5'
memory: 256M
networks:
- frontend
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
volumes:
postgres-data:
driver: local
driver_opts:
type: none
o: bind
device: /var/lib/docker/volumes/postgres-data
redis-data:
driver: local
nginx-cache:
driver: local
nginx-logs:
driver: local
networks:
frontend:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
backend:
driver: bridge
internal: false
ipam:
config:
- subnet: 172.21.0.0/16
secrets:
db_password:
file: ./secrets/db_password.txt
What this example includes:
- ✅ No version field (modern standard)
- ✅ Specific image tags (alpine, version numbers)
- ✅ Health checks for all services
- ✅ Proper dependency conditions
- ✅ Resource limits and reservations
- ✅ Secrets management (modern feature)
- ✅ Logging configuration
- ✅ Network isolation with custom subnets
- ✅ Container names for easier management
- ✅ Restart policies
- ✅ Environment variable defaults
- ✅ Volume driver options
One file. Complete application. Production-ready. Modern best practices.
Best Practices​
1. Use Specific Image Tags​
Don't do this:
services:
web:
image: nginx:latest
Do this:
services:
web:
image: nginx:1.21-alpine
Why: Predictable. Reproducible. No surprises.
2. Use Health Checks​
Add health checks:
services:
db:
image: postgres:14
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
interval: 10s
timeout: 5s
retries: 5
Why: Know when service is ready. Better dependencies.
3. Use Named Volumes​
Don't do this:
services:
db:
volumes:
- /data
Do this:
services:
db:
volumes:
- db-data:/var/lib/postgresql/data
volumes:
db-data:
Why: Manageable. Portable. Better practice.
4. Document with Comments​
Add comments:
services:
# PostgreSQL database
postgres:
image: postgres:14
# Database credentials
environment:
POSTGRES_PASSWORD: secret
Why: Clear. Understandable. Maintainable.
My Take: Writing Compose Files​
I've written hundreds of Compose files. Here's what I learned:
Start simple:
- Get it working first
- Add complexity later
- Test as you go
Use best practices:
- Specific image tags
- Health checks
- Named volumes
- Proper dependencies
Document:
- Add comments
- Explain why
- Make it clear
The key: Start simple. Add features. Test. Iterate.
Memory Tip: The Recipe Card Analogy​
docker-compose.yml = Recipe card
Services: Ingredients Networks: How to combine Volumes: Storage Dependencies: Order
Once you see it this way, Compose files make perfect sense.
Common Mistakes​
- Using
latesttags: Unpredictable - Not using health checks: Dependencies don't work properly
- Anonymous volumes: Hard to manage
- Wrong indentation: YAML is sensitive
- Not testing: Assumes it works
Hands-On Exercise: Write a Compose File​
1. Create docker-compose.yml (modern style - no version needed):
services:
web:
image: nginx:alpine
ports:
- "8080:80"
volumes:
- ./html:/usr/share/nginx/html:ro
2. Create html directory:
mkdir html
echo "<h1>Hello from Compose!</h1>" > html/index.html
3. Run it:
docker compose up -d
4. Test it:
curl http://localhost:8080
# Should see: Hello from Compose!
5. Check logs:
docker compose logs web
6. Stop it:
docker compose down
Key Takeaways​
- docker-compose.yml defines your application - Services, networks, volumes
- Use specific image tags - Predictable and reproducible
- Add health checks - Know when services are ready
- Use named volumes - Manageable and portable
- Document with comments - Clear and maintainable
What's Next?​
Now that you can write Compose files, let's learn about multi-container applications. Next: Multi-Container Apps.
Remember: docker-compose.yml is like a recipe card. Clear. Complete. Works every time. Write it well, and your application runs smoothly.