Scaling Services with Compose
Scaling a service with Docker Compose means running more than one container from the same service definition. It is useful for local load testing, parallel background workers, and understanding how an application behaves when one role has multiple replicas. Compose scaling is simple, but it has important limits: replicas share the same image and configuration, host ports cannot be reused, and stateful services need extra care.
Overview: How Compose Scaling Works
A Compose service is a template for one or more containers. When a file contains a service named worker, Compose normally creates one container such as demo-worker-1. When you run docker compose up -d --scale worker=3, Compose creates three containers from that same service definition: demo-worker-1, demo-worker-2, and demo-worker-3. Each container is still a normal Docker container: it is created from read-only image layers, receives its own thin writable layer, joins the project network, and runs the service command as its main process.
Compose scaling is not the same thing as Kubernetes, Swarm, or a cloud autoscaler. Compose does not watch CPU usage and automatically add containers. It does not reschedule failed replicas across a cluster. It does not automatically create a production load balancer. It asks one Docker Engine to run a requested number of containers for a service in the current project. That makes it excellent for single-machine development and testing, but not a complete production orchestration system.
Networking is the part that surprises many beginners. All replicas of a service join the same Compose network, and the service name is registered in Docker’s embedded DNS. Other services should connect to the service name, such as worker, api, or redis, instead of container names. However, if a scaled service publishes a fixed host port like 8080:80, only one replica can bind that host port. The second replica cannot also own 8080 on the host. For scaled web containers, either put a reverse proxy in front, let Docker assign random host ports, or scale an internal-only service that has no ports entry.
State also matters. Scaling a stateless API container is usually safe because any replica can handle a request. Scaling queue workers is also common because each worker can pull a different job from Redis, RabbitMQ, or another queue. Scaling a database by setting --scale db=3 is not the same as creating a database cluster. You would create three independent database containers, often fighting over the same volume if configured badly. Replication, leader election, and storage consistency are application-level or database-level features, not automatic Compose behavior.
Syntax
docker compose up -d --scale SERVICE=REPLICAS
docker compose scale SERVICE=REPLICAS
docker compose ps SERVICE
docker compose logs SERVICE
| Part | Meaning |
|---|---|
up -d |
Creates or updates the project and leaves containers running in the background. |
--scale SERVICE=REPLICAS |
Sets how many containers Compose should run for one service during up. |
docker compose scale |
Changes the replica count for already-defined services. up --scale is often clearer because it also applies the current project definition. |
SERVICE |
The service key from compose.yml, such as web or worker. |
REPLICAS |
The desired number of containers. Use 0 to stop running any containers for that service. |
ps SERVICE |
Lists containers for a specific service, including replica indexes in their generated names. |
logs SERVICE |
Shows logs from every replica of the service, prefixed by container name. |
You can also set replicas in a Compose file with deploy.replicas, but plain local docker compose up is commonly driven with --scale. The command line is explicit, easy to change for tests, and avoids hiding a local-only replica count inside the file.
Examples
Example 1: Scale a Stateless Worker
This service has no published ports and no persistent volume. It is a good shape for scaling because every replica can do the same kind of background work independently.
services:
worker:
image: alpine:3.20
command: sh -c "while true; do echo worker=$$HOSTNAME; sleep 30; done"
Start three replicas and list them:
docker compose up -d --scale worker=3
docker compose ps worker
Output:
[+] Running 3/3
✔ Container demo-worker-1 Started
✔ Container demo-worker-2 Started
✔ Container demo-worker-3 Started
NAME IMAGE COMMAND SERVICE STATUS
demo-worker-1 alpine:3.20 "sh -c 'while true;…" worker Up 3 seconds
demo-worker-2 alpine:3.20 "sh -c 'while true;…" worker Up 3 seconds
demo-worker-3 alpine:3.20 "sh -c 'while true;…" worker Up 3 seconds
Compose created three containers from one service definition. The doubled dollar sign in $$HOSTNAME tells Compose to pass a literal $HOSTNAME into the container command instead of trying to substitute a host environment variable while reading the file.
Example 2: Scale a Web Service with Random Host Ports
If you publish a fixed host port, scaling fails because two containers cannot bind the same host address and port. This version publishes container port 80 without choosing the host port, so Docker assigns an available host port to each replica.
services:
web:
image: nginx:1.27-alpine
ports:
- "80"
Start three web replicas and inspect the assigned ports:
docker compose up -d --scale web=3
docker compose ps web
docker compose port --index 2 web 80
Output:
[+] Running 3/3
✔ Container demo-web-1 Started
✔ Container demo-web-2 Started
✔ Container demo-web-3 Started
NAME IMAGE SERVICE STATUS PORTS
demo-web-1 nginx:1.27-alpine web Up 4 seconds 0.0.0.0:32770->80/tcp
demo-web-2 nginx:1.27-alpine web Up 4 seconds 0.0.0.0:32771->80/tcp
demo-web-3 nginx:1.27-alpine web Up 4 seconds 0.0.0.0:32772->80/tcp
0.0.0.0:32771
This is useful for testing that multiple web replicas start cleanly, but it is not a friendly public endpoint because each replica gets a different host port. A realistic stack puts a reverse proxy or load balancer in front of the replicas and publishes only the proxy’s port.
Example 3: Scale Queue Workers Beside Redis
Queue workers are one of the best local Compose scaling examples. Redis is the shared service, and several workers connect to it by service name.
services:
redis:
image: redis:7.4-alpine
worker:
image: redis:7.4-alpine
command: sh -c "while true; do redis-cli -h redis ping; sleep 5; done"
depends_on:
- redis
Scale the workers, then look at their logs:
docker compose up -d --scale worker=4
docker compose logs --tail=8 worker
Output:
worker-1 | PONG
worker-2 | PONG
worker-3 | PONG
worker-4 | PONG
worker-1 | PONG
worker-2 | PONG
worker-3 | PONG
worker-4 | PONG
Each worker is a separate container, but all of them can resolve the Redis hostname redis on the Compose network. In a real app, the command would run a job processor instead of redis-cli ping. The important design is the same: keep the workers stateless and put shared state in a real backing service.
How It Works Step by Step
- Compose reads
compose.yml, expands variables, and builds an in-memory project model containing services, networks, volumes, and requested scale overrides. - Compose chooses the project name, usually from the directory. This prefix becomes part of generated container names such as
demo-worker-2. - For each scaled service, Compose compares the desired replica count with existing containers that have matching project and service labels.
- If too few containers exist, Docker creates more containers from the same image and service configuration. The image layers are shared read-only; each replica receives its own writable layer.
- Docker attaches each replica to the project network and registers it in embedded DNS. Other containers should use the service name, not a generated container name.
- If too many replicas exist, Compose stops and removes the extra containers for that service.
- If the service definition changes, Compose may recreate replicas so the running containers match the file. Data in a container’s writable layer disappears when that container is removed, while named volumes survive unless explicitly removed.
Common Mistakes
Scaling a Service with a Fixed Host Port
Wrong:
services:
web:
image: nginx:1.27-alpine
ports:
- "8080:80"
docker compose up -d --scale web=3
Only one container can publish host port 8080. The fix is to publish random host ports for local testing, or put a proxy in front and scale the internal service behind it:
services:
web:
image: nginx:1.27-alpine
ports:
- "80"
Using container_name on a Scaled Service
Wrong:
services:
worker:
image: alpine:3.20
container_name: worker
command: sh -c "while true; do sleep 60; done"
A fixed container_name prevents Compose from creating worker-1, worker-2, and so on. Let Compose generate names for scalable services.
Scaling a Stateful Database by Guessing
Wrong:
docker compose up -d --scale db=3
This does not create PostgreSQL replication, MySQL clustering, or durable failover. It creates three containers from one database service definition. Use one database container for local Compose, or follow that database’s official clustering and replication documentation when you truly need multiple database nodes.
Expecting depends_on to Mean Ready
depends_on controls start order, not application readiness. Four workers may start before Redis is ready to answer. Real workers should retry connections, and Compose files can add health checks when startup sequencing needs to be visible.
Best Practices
- Scale stateless services: APIs, web frontends behind a proxy, and background workers are good candidates.
- Keep persistent state in named volumes or external backing services, not in a replica’s writable container layer.
- Avoid fixed host ports on scaled services. Publish a proxy port, use random host ports for local tests, or keep the service internal.
- Do not set
container_nameon services you may scale. Compose needs generated names with replica indexes. - Use service names for networking. Generated container names are implementation details and can change.
- Pin image tags such as
nginx:1.27-alpine,alpine:3.20, andredis:7.4-alpineinstead of usinglatest. - Use
docker compose logs SERVICEanddocker compose ps SERVICEto inspect all replicas of a scaled service. - Treat Compose scaling as a development and single-host testing tool unless you have deliberately designed production operations around it.
Practice Exercises
- Create a Compose file with an
alpine:3.20worker that prints its hostname every ten seconds. Start it with five replicas, then scale it back down to two. Expected end state: only two worker containers remain indocker compose ps worker. - Modify a web service that currently uses
"8080:80"so it can run three replicas locally. Hint: do not bind every replica to the same fixed host port. - Add Redis and a worker service to a Compose project. Scale the worker to four replicas and confirm each one can reach Redis by service name. Expected end state: logs from multiple worker replicas show successful Redis responses.
Summary
docker compose up -d --scale service=nruns multiple containers from one service definition.- Each replica is a normal Docker container with its own writable layer, generated name, network attachment, and main process.
- Compose scaling is manual and single-host oriented; it is not automatic cluster orchestration.
- Fixed host ports conflict when a service is scaled. Use a proxy, random host ports, or internal-only services.
- Stateless web services and queue workers scale well in Compose; databases require real database clustering, not just more containers.
- Use
docker compose ps,docker compose logs, anddocker compose portto inspect scaled services.
