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
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
workerservice, 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.