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.

Unlock Full Tutorial

This chapter is paid content. Join the project to unlock over 5000 words of deep analysis, including 10+ god-tier Prompts and real Source Code examples!