Tutorial
May 30, 2026
18 min read

Docker Compose for Development 2026: Multi-Container Setups

Master Docker Compose for development environments. Learn multi-container setups, development workflows, service orchestration, and best practices.

Rehman Farouq

Full Stack Developer | DevOps Expert

Introduction to Docker Compose

Docker Compose simplifies multi-container Docker applications. It's perfect for development environments, allowing you to define and run multi-container applications with a single command.

This guide covers everything from basic setups to advanced multi-container orchestration for development workflows.

Basic Docker Compose Setup

Get started with a simple Docker Compose configuration for a web application.

# docker-compose.yml
version: '3.8'

services:
  web:
    build: .
    ports:
      - "3000:3000"
    volumes:
      - .:/app
      - /app/node_modules
    environment:
      - NODE_ENV=development
    command: npm run dev

# Dockerfile
FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .

EXPOSE 3000

CMD ["npm", "start"]

# Run with
docker-compose up
docker-compose up -d  # detached mode
docker-compose down   # stop and remove

Multi-Container Application

Set up a complete application with web server, database, and cache.

# docker-compose.yml
version: '3.8'

services:
  web:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgresql://user:password@db:5432/myapp
      - REDIS_URL=redis://redis:6379
    depends_on:
      - db
      - redis
    volumes:
      - .:/app
      - /app/node_modules

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
      POSTGRES_DB: myapp
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data

volumes:
  postgres_data:
  redis_data:

# Network configuration
networks:
  default:
    driver: bridge

Development Workflow

Optimize your development workflow with Docker Compose commands and patterns.

# Development commands
docker-compose up              # Start all services
docker-compose up -d           # Start in detached mode
docker-compose down            # Stop and remove containers
docker-compose logs            # View logs
docker-compose logs -f web     # Follow logs for specific service
docker-compose ps              # List running containers
docker-compose exec web bash   # Execute command in container
docker-compose restart web     # Restart specific service
docker-compose build           # Rebuild images
docker-compose pull            # Pull latest images

# Development override
# docker-compose.dev.yml
version: '3.8'

services:
  web:
    build:
      context: .
      target: development
    volumes:
      - .:/app
      - /app/node_modules
    environment:
      - NODE_ENV=development
      - CHOKIDAR_USEPOLLING=true
    command: npm run dev

# Run with override
docker-compose -f docker-compose.yml -f docker-compose.dev.yml up

# Hot reload configuration
# Dockerfile
FROM node:18-alpine AS base
WORKDIR /app

FROM base AS development
COPY package*.json ./
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]

FROM base AS production
COPY package*.json ./
RUN npm ci --only=production
COPY . .
CMD ["npm", "start"]

Health Checks and Dependencies

Implement health checks to ensure services are ready before dependent services start.

# docker-compose.yml with health checks
version: '3.8'

services:
  web:
    build: .
    ports:
      - "3000:3000"
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
      POSTGRES_DB: myapp
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

# Wait script for dependencies
# wait-for-it.sh
#!/bin/sh
TIMEOUT=15
QUIET=0
HOST=$1
PORT=$2
shift 2
COMMAND=$@

if [ "$QUIET" -ne 1 ]; then
  echo "Waiting for $HOST:$PORT..."
end

for i in $(seq 1 $TIMEOUT); do
  nc -z "$HOST" "$PORT" >/dev/null 2>&1
  
  if [ $? -eq 0 ]; then
    if [ "$QUIET" -ne 1 ]; then
      echo "$HOST:$PORT is available"
    fi
    exec $COMMAND
  fi
  
  sleep 1
done

echo "Timeout reached for $HOST:$PORT"
exit 1}

Environment Configuration

Manage environment variables across different environments with Docker Compose.

# .env file
DATABASE_URL=postgresql://user:password@db:5432/myapp
REDIS_URL=redis://redis:6379
API_KEY=your_api_key
NODE_ENV=development

# .env.production
DATABASE_URL=postgresql://prod_user:prod_pass@prod_db:5432/myapp
REDIS_URL=redis://prod_redis:6379
API_KEY=prod_api_key
NODE_ENV=production

# docker-compose.yml
version: '3.8'

services:
  web:
    build: .
    ports:
      - "3000:3000"
    env_file:
      - .env
    environment:
      - NODE_ENV=${NODE_ENV:-development}

# Using different env files
docker-compose --env-file .env.production up

# Environment substitution
version: '3.8'

services:
  web:
    image: nginx:${NGINX_VERSION:-latest}
    ports:
      - "${WEB_PORT:-80}:80"
    environment:
      - APP_NAME=${APP_NAME}
      - DEBUG=${DEBUG:-false}

Volumes and Networking

Configure volumes for data persistence and custom networks for service communication.

# docker-compose.yml with volumes and networks
version: '3.8'

services:
  web:
    build: .
    volumes:
      - ./src:/app/src              # Bind mount for development
      - node_modules:/app/node_modules  # Named volume for dependencies
      - ./logs:/app/logs            # Log directory
    networks:
      - frontend
      - backend

  db:
    image: postgres:15-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    networks:
      - backend

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data
    networks:
      - backend

volumes:
  postgres_data:
    driver: local
  redis_data:
    driver: local
  node_modules:

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true  # No external access

# Volume backup and restore
# Backup
docker run --rm -v myapp_postgres_data:/data -v $(pwd):/backup   alpine tar czf /backup/postgres_backup.tar.gz /data

# Restore
docker run --rm -v myapp_postgres_data:/data -v $(pwd):/backup   alpine tar xzf /backup/postgres_backup.tar.gz -C /

Production Considerations

Best practices for using Docker Compose in production environments.

# docker-compose.prod.yml
version: '3.8'

services:
  web:
    image: myapp:latest
    deploy:
      replicas: 3
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 256M
    restart: unless-stopped
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

  db:
    image: postgres:15-alpine
    deploy:
      resources:
        limits:
          memory: 1G
    restart: unless-stopped
    volumes:
      - postgres_data:/var/lib/postgresql/data

# Security best practices
# Use non-root users
# Dockerfile
FROM node:18-alpine
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodejs -u 1001
USER nodejs
WORKDIR /app

# Scan images for vulnerabilities
docker scan myapp:latest

# Use specific image versions
# Instead of latest
image: postgres:15-alpine

# Limit container capabilities
cap_drop:
  - ALL
cap_add:
  - NET_BIND_SERVICE

# Read-only filesystem
read_only: true
tmpfs:
  - /tmp

Docker Compose Best Practices

Development

  • • Use bind mounts for code
  • • Enable hot reload
  • • Separate dev and prod configs
  • • Use environment variables

Performance

  • • Use named volumes for data
  • • Optimize Dockerfile layers
  • • Use .dockerignore
  • • Cache dependencies

Security

  • • Scan images for vulnerabilities
  • • Use specific image versions
  • • Run as non-root user
  • • Limit container capabilities

Maintenance

  • • Version control compose files
  • • Document service dependencies
  • • Use health checks
  • • Monitor resource usage

Conclusion

Docker Compose is an essential tool for development environments, making it easy to manage multi-container applications. By following these patterns and best practices, you can create efficient, reproducible development setups.

Start with simple setups and gradually add complexity as your application grows. Remember to separate development and production configurations for optimal results.

Ready to Containerize Your Development?

Explore more DevOps tutorials and streamline your development workflow!