Docker Compose - The Orchestra Conductor 🎼
Imagine you’re running a restaurant. You don’t just have a chef—you have a chef, a waiter, a dishwasher, and a host. They all need to work together, start in the right order, and know how to recover when things go wrong.
Docker Compose is like the restaurant manager who coordinates everyone. One command, and the whole team springs into action!
🌍 Environment Configuration
What’s an Environment?
Think of environments like different “modes” for your app:
- Development = Practice mode (okay to make mistakes)
- Production = Show time (everything must be perfect)
Setting Up Environment Variables
Method 1: Inline in docker-compose.yml
services:
web:
image: myapp
environment:
- DATABASE_URL=postgres://db:5432
- DEBUG=true
Method 2: Using an .env file
Create a file called .env:
DATABASE_URL=postgres://db:5432
DEBUG=true
SECRET_KEY=my-super-secret
Then reference it:
services:
web:
image: myapp
env_file:
- .env
Why use .env files?
- Keep secrets out of your code
- Easy to change settings without editing the main file
- Different .env files for different environments
🔗 depends_on and healthcheck
The “Wait for Me!” Problem
Imagine your web app needs the database. But what if the database isn’t ready yet?
graph TD A["Web App Starts"] --> B{Database Ready?} B -->|No| C["❌ App Crashes"] B -->|Yes| D["✅ App Works"]
depends_on: Basic Waiting
services:
web:
image: myapp
depends_on:
- db
db:
image: postgres
But wait! depends_on only waits for the container to start, not for it to be ready.
healthcheck: Smart Waiting
A healthcheck is like asking “Are you actually ready to work?”
services:
db:
image: postgres
healthcheck:
test: ["CMD", "pg_isready"]
interval: 5s
timeout: 3s
retries: 5
What this means:
- Every 5 seconds, ask “Are you ready?”
- Wait up to 3 seconds for an answer
- Try 5 times before giving up
Combining Both Powers
services:
web:
depends_on:
db:
condition: service_healthy
db:
healthcheck:
test: ["CMD", "pg_isready"]
interval: 5s
timeout: 3s
retries: 5
Now the web app waits until the database is truly ready!
🔄 Restart and Deploy Policies
Restart: What Happens When Things Crash?
Your app might crash. It happens! Restart policies decide what to do next.
services:
web:
image: myapp
restart: always
Restart Options:
| Policy | What It Does |
|---|---|
no |
Never restart (default) |
always |
Always restart, no matter what |
on-failure |
Only restart if it crashed |
unless-stopped |
Restart unless you stopped it manually |
Deploy: Production-Grade Settings
For serious production use, the deploy section gives you superpowers:
services:
web:
image: myapp
deploy:
replicas: 3
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
resources:
limits:
cpus: '0.5'
memory: 512M
Translation:
- Run 3 copies of this service
- If one fails, wait 5 seconds, then try again
- Give up after 3 attempts
- Each copy gets max half a CPU and 512MB RAM
🎭 Compose Profiles
Why Profiles?
Sometimes you want different services for different situations:
- Development: Include debugging tools
- Production: Only essential services
- Testing: Include test database
Creating Profiles
services:
web:
image: myapp
# No profile = always runs
db:
image: postgres
# No profile = always runs
debug-tools:
image: debug-toolkit
profiles:
- debug
test-db:
image: postgres
profiles:
- testing
Using Profiles
# Normal start (web + db only)
docker compose up
# Include debug tools
docker compose --profile debug up
# Include testing services
docker compose --profile testing up
# Multiple profiles
docker compose --profile debug --profile testing up
Think of profiles like “expansion packs” you can add when needed!
🛠️ Docker Compose Commands
The Essential Commands
graph TD A["docker compose up"] --> B["Start Everything"] C["docker compose down"] --> D["Stop & Remove"] E["docker compose ps"] --> F["List Running"] G["docker compose logs"] --> H["See Output"]
Quick Reference
Starting:
# Start all services
docker compose up
# Start in background (detached)
docker compose up -d
# Start specific services
docker compose up web db
Stopping:
# Stop everything
docker compose down
# Stop and remove volumes too
docker compose down -v
Monitoring:
# See what's running
docker compose ps
# View logs
docker compose logs
# Follow logs live
docker compose logs -f
# Logs for one service
docker compose logs web
Rebuilding:
# Rebuild and start
docker compose up --build
# Just rebuild
docker compose build
📁 Override Files
The Magic of Overrides
Imagine you have a base recipe, but want to add extra spices for different occasions.
Base file: docker-compose.yml
services:
web:
image: myapp
ports:
- "3000:3000"
Override file: docker-compose.override.yml
services:
web:
environment:
- DEBUG=true
volumes:
- ./src:/app/src
Docker Compose automatically merges them!
Override Loading Order
graph TD A["docker-compose.yml"] --> B["Base Config"] B --> C["docker-compose.override.yml"] C --> D["Final Merged Config"]
Custom Override Files
# Use a specific override file
docker compose -f docker-compose.yml \
-f docker-compose.prod.yml up
Common pattern:
docker-compose.yml→ Base configdocker-compose.override.yml→ Development extrasdocker-compose.prod.yml→ Production settingsdocker-compose.test.yml→ Testing settings
🔀 Variable Substitution
Making Your Compose Files Dynamic
Instead of hardcoding values, use variables!
In your .env file:
APP_PORT=3000
APP_VERSION=2.1.0
DB_PASSWORD=supersecret
In docker-compose.yml:
services:
web:
image: myapp:${APP_VERSION}
ports:
- "${APP_PORT}:3000"
db:
image: postgres
environment:
- POSTGRES_PASSWORD=${DB_PASSWORD}
Default Values
What if a variable isn’t set? Use defaults!
services:
web:
# Use 8080 if APP_PORT isn't set
ports:
- "${APP_PORT:-8080}:3000"
# Require this variable (error if missing)
environment:
- SECRET_KEY=${SECRET_KEY:?Secret key required!}
Variable Substitution Syntax
| Syntax | Meaning |
|---|---|
${VAR} |
Use the value of VAR |
${VAR:-default} |
Use default if VAR is unset |
${VAR:?error} |
Show error if VAR is unset |
${VAR:+replacement} |
Use replacement if VAR is set |
🎯 Putting It All Together
Here’s a complete example using everything we learned:
# docker-compose.yml
services:
web:
image: myapp:${APP_VERSION:-latest}
ports:
- "${WEB_PORT:-3000}:3000"
environment:
- DATABASE_URL=postgres://db:5432/${DB_NAME}
depends_on:
db:
condition: service_healthy
restart: unless-stopped
db:
image: postgres:15
environment:
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_DB=${DB_NAME:-myapp}
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
interval: 5s
timeout: 3s
retries: 5
volumes:
- db-data:/var/lib/postgresql/data
redis:
image: redis:alpine
profiles:
- cache
volumes:
db-data:
What this does:
- ✅ Web app waits for healthy database
- ✅ Uses environment variables for flexibility
- ✅ Has sensible defaults
- ✅ Redis only starts when needed (profile)
- ✅ Database data persists in a volume
- ✅ Auto-restarts on crash
🏆 Key Takeaways
- Environment Configuration → Use .env files to keep secrets safe
- depends_on + healthcheck → Make services wait properly
- restart + deploy → Handle failures gracefully
- Profiles → Add optional services when needed
- Commands → up, down, ps, logs are your daily tools
- Override files → Layer configs for different environments
- Variable substitution → Make configs flexible and reusable
You’re now ready to orchestrate containers like a pro! 🎼🐳
