Docker Compose — Development vs. Production Environments
Why Environment Separation Matters
Running the same configuration in development and production is a recipe for disaster. Development needs hot-reload, verbose logging, and debug tools. Production needs minimal images, security hardening, and health checks. Docker Compose supports multiple configurations via multiple compose files and variable substitution.
Why this matters for your career:
- Environment separation is a fundamental DevOps practice
- Misconfigured production environments cause outages
- Understanding overlay files and variable substitution makes you more productive
- Multi-environment setup is commonly requested in freelance infrastructure projects
Development Configuration
# docker-compose.dev.yml
docker-compose.dev.yml
version: "3.9"
services:
app:
build:
context: .
dockerfile: Dockerfile.dev
volumes:
- .:/app # Mount source code for hot-reload
- /app/node_modules # Exclude node_modules from mount
ports:
- "3000:3000"
- "9229:9229" # Debug port (Node.js inspector)
environment:
- NODE_ENV=development
- DEBUG=true
- LOG_LEVEL=debug
command: npm run dev # Hot-reload dev server
db:
image: postgres:16-alpine
ports:
- "5432:5432"
environment:
POSTGRES_USER: dev_user
POSTGRES_PASSWORD: dev_password
POSTGRES_DB: myapp_dev
volumes:
- pgdata_dev:/var/lib/postgresql/data
- ./scripts/seed.sql:/docker-entrypoint-initdb.d/seed.sql
volumes:
pgdata_dev:
Dockerfile.dev
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
# No COPY of source code — it's mounted as a volume
# Hot-reload with nodemon
RUN npm install -g nodemon
EXPOSE 3000
CMD ["nodemon", "--inspect=0.0.0.0:9229", "src/server.js"]
Production Configuration
# docker-compose.prod.yml
version: "3.9"
services:
app:
build:
context: .
dockerfile: Dockerfile.prod
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DEBUG=false
- LOG_LEVEL=info
restart: always
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_NAME}
volumes:
- pgdata_prod:/var/lib/postgresql/data
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
interval: 10s
timeout: 5s
retries: 5
volumes:
pgdata_prod:
Dockerfile.prod
# Multi-stage build for production
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
# Copy only what's needed
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
# Run as non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "dist/server.js"]
Running Different Environments
# Development (override base compose file)
docker compose -f docker-compose.yml -f docker-compose.dev.yml up
# Production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
# Build for production
docker compose -f docker-compose.yml -f docker-compose.prod.yml build
Overlay File Pattern
The base docker-compose.yml contains shared configuration:
# docker-compose.yml (base)
version: "3.9"
services:
app:
image: myapp:latest
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/mydb
depends_on:
- db
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: mydb
Environment-specific files override only the settings that differ. This keeps configuration DRY.
Environment Variables (.env)
# .env.dev
NODE_ENV=development
LOG_LEVEL=debug
DB_HOST=localhost
DB_PORT=5432
# .env.prod
NODE_ENV=production
LOG_LEVEL=info
DB_HOST=db.internal
DB_PORT=5432
# docker-compose.yml using variables
services:
app:
environment:
- NODE_ENV=${NODE_ENV}
- LOG_LEVEL=${LOG_LEVEL}
# Specify env file
docker compose --env-file .env.prod up -d
Key Differences Summary
| Aspect | Development | Production | |--------|-------------|------------| | Image type | Dev image (includes build tools) | Minimal (distroless, alpine) | | Code mounting | Volume mount (hot-reload) | Built into image | | Debugging | Enabled (debug port, verbose logs) | Disabled (info/warn only) | | Restart policy | no (manual) | always (automatic) | | Health checks | Optional | Required | | Logging | Console (docker logs) | Structured (json-file, Loki) | | User | root (convenience) | Non-root (security) | | Ports | Exposed (localhost access) | Internal (reverse proxy) | | Database | Ephemeral or seeded | Persistent with backups |
Common Mistakes
| Mistake | Consequence | Fix | |---------|-------------|-----| | Using dev config in production | Security vulnerabilities, poor performance | Always use production config | | Hardcoding secrets in compose file | Secrets in git history | Use .env files or Docker secrets | | Running as root in production | Security risk | Add USER directive | | No health checks | Traffic to unhealthy containers | Add healthcheck to every service | | No restart policy | Container stays down after crash | Add restart: always | | Mounting source code in production | Live code changes (bad for stability) | Only mount in development | | Unbounded log growth | Disk full from logs | Configure log rotation |
Summary
Separating development and production configurations is essential for security, performance, and developer productivity. Use overlay compose files and environment variables to keep configuration DRY and maintainable.
Key takeaways:
- Development: hot-reload, debug tools, verbose logs, root user
- Production: minimal images, health checks, non-root user, restart policy
- Use multiple compose files: base + dev/prod overrides
- Use .env files for secrets and environment-specific values
- Multi-stage builds keep production images small
- Never mount source code in production
- Always add health checks to production services
- Configure log rotation to prevent disk-full issues
What's Next: Networks & Volumes
The next chapter covers Docker Compose networks and volumes — custom networks, named volumes, bind mounts, and data persistence strategies.