Hello! Welcome back.
In our last session, we mastered the art of managing environment-specific configurations using .env files and multiple Compose files. This allows us to create flexible and secure setups for development and production. Now that we have a production-ready configuration, the next logical step is to ensure our application is both resilient and performant under load.
Today, we'll focus on a core concept in modern infrastructure: high availability. You'll learn how to scale services to run multiple container instances. We'll explore how Docker Compose enables you to run several copies of a service and, just as importantly, how to distribute traffic between them using a load balancer. This skill is fundamental for building fault-tolerant applications that can handle increased user traffic.
1. The "Why" Behind Scaling: High Availability and Load Balancing
Before we dive into commands, let's clarify the concept. When we talk about "scaling" a service in this context, we mean horizontal scaling—running multiple identical copies (replicas) of that service. This achieves two primary goals:
- High Availability: If one container instance crashes or becomes unresponsive, other instances are still running and can handle requests. This prevents a single point of failure and makes your application more resilient.
- Load Balancing: When traffic increases, a single container might become a bottleneck. By running multiple instances, you can distribute the incoming requests among them, improving performance and response times.
This principle is the bedrock of container orchestrators like Kubernetes and Docker Swarm.

While the image depicts a multi-node Docker Swarm cluster, Docker Compose allows us to apply the same principle on a single machine, which is perfect for development, testing, and smaller-scale production deployments.
2. Scaling Services with the --scale Flag
The most direct way to scale a service with Docker Compose on a single host is by using the --scale flag with the up command. This tells Compose to create a specified number of instances for a particular service, overriding the default of one.
Let's build a practical example. We'll create a simple web application and scale it to three instances.
Hands-On Exercise: Scaling a Web Service
First, set up the following directory structure and files:
scaling-exercise/
├── docker-compose.yml
└── web/
└── index.html
-
web/index.html: Create a simple HTML file. To see the load balancing in action later, we'll use a little JavaScript to display the container's hostname.<!DOCTYPE html> <html> <head> <title>Scaled Service</title> <style> body { font-family: sans-serif; text-align: center; background-color: #282c34; color: white; margin-top: 100px; font-size: 1.5em; } .hostname { color: #61dafb; font-weight: bold; } </style> </head> <body> <h1>Hello from a Scaled Service!</h1> <p>This request was served by container: <b class="hostname" id="hostname">...</b></p> <script> // In a real app, the backend would render this. For our static Nginx example, we'll fetch it. // Note: This is a simplification for demo purposes. document.getElementById('hostname').textContent = window.location.hostname; </script> </body> </html> -
docker-compose.yml: Define a single service namedwebthat uses the standardnginximage and mounts ourindex.htmlfile.version: '3.8' services: web: image: nginx:alpine volumes: - ./web/index.html:/usr/share/nginx/html/index.html networks: - app-net networks: app-net: driver: bridge
Now, from within the scaling-exercise directory, run the following command to start three instances of the web service:
docker compose up --scale web=3 -d
You can verify that three containers for the web service are running:
docker compose ps
You should see output similar to this, showing three running containers:
NAME COMMAND SERVICE STATUS PORTS
scaling-exercise-web-1 "/docker-entrypoint.…" web running 80/tcp
scaling-exercise-web-2 "/docker-entrypoint.…" web running 80/tcp
scaling-exercise-web-3 "/docker-entrypoint.…" web running 80/tcp
We now have three running web servers, but we have a problem: none of them have published ports. How do we access them? And how do we distribute traffic among them? This is where a load balancer comes in.
3. Adding a Load Balancer with Nginx
To distribute traffic to our scaled web instances, we need a single entry point: a reverse proxy that acts as a load balancer. We'll add another Nginx service to our Compose file to perform this role.
A key feature of Docker's networking is its built-in DNS service. When a service is scaled, any other container on the same network can resolve the service name (e.g., web) and Docker's DNS will return the IP addresses of all healthy containers for that service in a round-robin fashion. Our load balancer can leverage this to automatically discover and distribute traffic to all replicas.
Let's update our project.
Hands-On Exercise: Adding the Load Balancer
-
Create an Nginx configuration file: In the root of your
scaling-exercisedirectory, createnginx.conf.# nginx.conf events {} http { upstream web_backend { # Docker's DNS will resolve 'web' to all container IPs for the service. # Nginx will then round-robin requests between them. server web:80; } server { listen 80; location / { proxy_pass http://web_backend; } } }This configuration tells Nginx to listen for requests and forward (
proxy_pass) them to anupstreamgroup namedweb_backend. That group contains a single entry:server web:80;. Because of Docker's DNS, this is all we need to load balance across allwebcontainers. -
Update
docker-compose.yml: Add theload_balancerservice.version: '3.8' services: web: image: nginx:alpine # We don't need ports here, the load balancer will handle access. volumes: - ./web/index.html:/usr/share/nginx/html/index.html networks: - app-net load_balancer: image: nginx:alpine ports: - "8080:80" # Expose the load balancer on host port 8080 volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro # Mount our config networks: - app-net depends_on: # Optional but good practice - web networks: app-net: driver: bridge
Now, bring down the old containers and run the full stack:
# Stop and remove the previous containers
docker compose down
# Start the new stack with 3 web instances
docker compose up --scale web=3 -d
Open your browser to http://localhost:8080. Refresh the page several times. You will see that the container hostname changes (e.g., scaling-exercise-web-1, scaling-exercise-web-2, scaling-exercise-web-3), confirming that our load balancer is distributing requests across all three replicas!
Scale Services with Docker Compose: A Practical Guide
The article 'Scale Services with Docker Compose: A Practical Guide' provides a complete overview of this pattern. It walks through setting up the project structure and explains the role of each component.
Skim through this article to reinforce your understanding of the example we just built. Notice its nginx.conf lists the server multiple times; our approach using Docker's DNS is more dynamic and scalable, but both achieve a similar result.
Test your understanding!
You want to scale your web service to 5 instances and your background worker service to 2 instances. What command would you use?
Show answer
You can provide multiple --scale flags in a single command:
docker compose up --scale web=5 --scale worker=2 -d
4. A Note on the deploy Key
When you browse examples online (including the one we just reviewed), you'll often see a deploy key in docker-compose.yml files, like this:
services:
web:
image: my-app
deploy:
replicas: 3
It is critical to understand that the deploy key and its sub-options (like replicas, resources, restart_policy) are ignored by the docker compose up command. This key is exclusively used by Docker Swarm when you deploy a stack using docker stack deploy.
docker compose up --scale: For scaling on a single host. Ideal for local development or simple, single-node deployments.deploykey: For scaling across a multi-node cluster using Docker Swarm.
This distinction is a common point of confusion. Given your background with Kubernetes, you can think of it this way: docker compose up is for your local development machine, while docker stack deploy (using the deploy key) is the first step towards a true orchestrated production environment, similar in spirit to kubectl apply.
Conclusion
You've now added a powerful technique to your Docker Compose toolkit. By running multiple instances of a service behind a load balancer, you can build applications that are more robust and can handle significantly more traffic.
Key Takeaways:
- Horizontal scaling improves high availability and allows for load balancing.
- Use
docker compose up --scale <service>=<N>to run multiple instances of a service on a single host. - A reverse proxy (like Nginx) is essential to act as a load balancer and distribute traffic to the scaled replicas.
- Docker's internal DNS service allows the load balancer to automatically discover all running instances of a scaled service.
- The
deploy: replicas:key in a Compose file is for Docker Swarm only and is ignored bydocker compose up.
In our next lesson, we will address a consequence of scaling: resource consumption. We'll learn how to configure resource limits (CPU and memory) for services to ensure that no single service can monopolize the host's resources, which is crucial for stable multi-service applications.