Docker Compose can run several copies (replicas) of a service on one host: useful to use more CPU cores for a single-threaded app, to survive one container crashing, or to update without downtime. It takes a couple of settings, plus a load balancer in front.

Scale a service

docker compose up -d --scale web=3
docker compose ps

Or set the default in the file:

services:
  web:
    image: myapp:1.8.3
    deploy:
      replicas: 3

Avoid fixed host ports

This fails with more than one replica, because each container would claim host port 8080:

ports:
  - "8080:8080"      # ❌ port conflict with replicas

Instead, don’t publish the app’s port. Put a reverse proxy on the same Compose network, which reaches the replicas by service name.

Load balancing with Nginx

Docker’s embedded DNS returns all replica IPs for the service name web:

services:
  web:
    image: myapp:1.8.3
    deploy:
      replicas: 3
    expose:
      - "8080"
  proxy:
    image: nginx:1.27
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on: [web]
# nginx.conf
resolver 127.0.0.11 valid=10s;          # Docker's embedded DNS
upstream app {
    zone app 64k;
    server web:8080 resolve;             # Nginx 1.27.3+ (open source): re-resolves replicas
}
server {
    listen 80;
    location / { proxy_pass http://app; }
}

With older Nginx versions, server web:8080; resolves the replica IPs once, at startup, so reload Nginx after scaling (docker compose exec proxy nginx -s reload). See Nginx load balancing.

Load balancing with Traefik (automatic)

Traefik watches Docker, and adds or removes replicas automatically:

services:
  traefik:
    image: traefik:v3.1
    command: ["--providers.docker=true", "--providers.docker.exposedbydefault=false", "--entrypoints.web.address=:80"]
    ports: ["80:80"]
    volumes: ["/var/run/docker.sock:/var/run/docker.sock:ro"]
  web:
    image: myapp:1.8.3
    deploy:
      replicas: 3
    labels:
      - traefik.enable=true
      - traefik.http.routers.web.rule=Host(`app.example.com`)
      - traefik.http.services.web.loadbalancer.server.port=8080

See Traefik reverse proxy.

Make the app scale-friendly

Replicas only help if the app is stateless:

  • sessions in Redis or the database, not in memory or local files;
  • uploads in a shared volume or object storage;
  • scheduled jobs and queue consumers run once (a separate worker service, scaled independently);
  • database migrations run once (a one-off docker compose run --rm web migrate), not in every replica’s startup.

See VPS scaling: vertical vs horizontal.

Updating with less downtime

Compose recreates all replicas at once by default. A simple rolling-style approach:

docker compose pull web
docker compose up -d --no-deps --scale web=6 --no-recreate web   # start new replicas alongside old
# then remove old containers, and scale back to 3

This gets fiddly. Tools like docker-rollout automate it. For real rolling updates and multi-host scaling, use Docker Swarm (docker stack deploy understands deploy.update_config) or Kubernetes. See Docker Swarm and Kubernetes vs Docker Compose.

Sizing

Replicas share the host’s CPU and RAM. Set limits per replica, so they can’t starve the database:

deploy:
  replicas: 3
  resources:
    limits: { cpus: "1", memory: 512M }

A good starting point is one replica per spare CPU core for CPU-bound apps. See Docker CPU and memory limits.

Quick reference

Task How
Scale now docker compose up -d --scale web=3
Default replicas deploy.replicas: 3
No port conflicts expose instead of ports
Load balancer Nginx upstream on service name, or Traefik labels
Rolling updates docker-rollout, Swarm, or Kubernetes

Related: Docker Compose in production and Docker networking.

  • Docker
  • Scaling
  • DevOps