Docker Compose & Networking

Difficulty: Intermediate

Question

How does Docker Compose orchestrate multi-container applications? Explain Docker networking and service communication.

Answer

Docker Compose defines and runs multi-container applications using a YAML file. Each service runs in its own container but they share a network, making inter-service communication seamless.

Docker networking: Compose automatically creates a bridge network for each project. Containers on the same network can reach each other using service names as hostnames. For example, a Node.js container connects to PostgreSQL using db:5432 where db is the service name.

Network types: - Bridge (default): Containers on same host communicate via service names. Isolated from the host network. - Host: Container shares the host network stack. No port mapping needed but no network isolation. - None: No networking. Complete isolation. - Overlay: Multi-host networking for Docker Swarm/Kubernetes.

Volumes persist data beyond container lifecycle. Named volumes are managed by Docker and survive container recreation. Bind mounts map host paths into containers, useful for development hot-reloading.

Health checks let Compose and orchestrators know if a service is actually ready, not just running. depends_on with health checks ensures proper startup order.

Code examples

Full-Stack Docker Compose

# docker-compose.yml
services:
  app:
    build:
      context: ./server
      dockerfile: Dockerfile
    ports:
      - '3000:3000'
    environment:
      DATABASE_URL: postgresql://user:pass@db:5432/mydb
      REDIS_URL: redis://cache:6379
      NODE_ENV: production
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    networks:
      - backend
    restart: unless-stopped

  client:
    build:
      context: ./client
      dockerfile: Dockerfile
    ports:
      - '80:80'
    depends_on:
      - app
    networks:
      - backend

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: mydb
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - backend

  cache:
    image: redis:7-alpine
    volumes:
      - redisdata:/data
    networks:
      - backend

volumes:
  pgdata:
  redisdata:

networks:
  backend:
    driver: bridge

Services communicate using service names as hostnames (db:5432, cache:6379). Health checks on db ensure the app does not start until PostgreSQL is actually ready to accept connections.

Docker Networking Deep Dive

# List networks
docker network ls
# NETWORK ID   NAME                DRIVER
# abc123       bridge              bridge
# def456       myapp_backend       bridge

# Inspect a network
docker network inspect myapp_backend
# Shows all containers on the network with their IPs

# Create a custom network
docker network create --driver bridge my-network

# Connect a running container to a network
docker network connect my-network container-name

# DNS resolution between containers
# Inside the app container:
ping db          # resolves to db container's IP
ping cache       # resolves to cache container's IP

# Port mapping:
# -p host:container
# -p 3000:3000    maps host 3000 to container 3000
# -p 8080:3000    maps host 8080 to container 3000
# -p 127.0.0.1:3000:3000  only accessible from localhost

# Internal-only services (no port mapping)
# db and cache don't need ports mapped to host
# They're only accessible from other containers on the network

# Expose ports between containers (no host mapping)
expose:
  - "5432"

Docker's built-in DNS resolves service names to container IPs automatically on bridge networks. Only map ports to the host for services that need external access.

Development vs Production Compose

# docker-compose.yml (base)
services:
  app:
    image: myapp
    environment:
      NODE_ENV: production

# docker-compose.override.yml (dev - auto-loaded)
services:
  app:
    build: .
    volumes:
      - ./src:/app/src        # hot reload
      - /app/node_modules     # don't override node_modules
    environment:
      NODE_ENV: development
    command: npm run dev

# docker-compose.prod.yml (production)
services:
  app:
    image: registry.example.com/myapp:latest
    deploy:
      replicas: 3
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
    restart: always

# Run development (uses override automatically)
docker compose up

# Run production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

# Useful commands
docker compose ps                    # list running services
docker compose logs -f app           # follow app logs
docker compose exec app sh           # shell into app container
docker compose down --volumes        # stop and remove volumes
docker compose build --no-cache      # rebuild without cache

Use override files for dev/prod differences. The override file auto-loads for development. Production compose adds replicas, resource limits, and uses pre-built images.

Key points

Concepts covered

Docker Compose, Docker Networking, Service Discovery, Volumes, Health Checks