Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 93

Scaling Services with `docker-compose up --scale`

Recommended resources

Hello! Welcome back to your course on Docker Compose.

In our last lesson, we took a major step towards creating robust applications by learning how to control service startup order with depends_on and healthcheck. This ensures that our services start reliably and don't crash due to race conditions.

Now that we can build a stable multi-service application, let's address another real-world challenge: handling changes in demand. A single container for your API might be fine for a few users, but what happens when traffic surges? This lesson focuses on a core concept in DevOps: scalability. Our goal is to learn how to scale individual services up or down using the docker-compose up --scale command.

By the end of this lesson, you'll be able to dynamically adjust the number of containers running for a service, preparing your application to handle a higher load with ease.

1. Why and How to Scale Services

When an application receives more requests than a single instance can handle, its performance degrades. The solution is often horizontal scaling, which means running multiple instances of the service to distribute the workload.

Docker Compose Service Scaling Illustration
Caption: This image visually represents horizontal scaling. An influx of requests (the blue spheres in the funnel) is distributed across multiple instances of a service (the stacks of containers), allowing the application to handle a greater load.

However, simply running multiple copies of a web service container presents a networking problem. If your docker-compose.yml has a port mapping like ports: - "8080:80", you can only run one instance. The second instance will fail to start because the host port 8080 is already in use by the first one.

The Solution: A Load Balancer

The standard pattern to solve this is to place a load balancer in front of your scaled services.

  • The load balancer is the only service that exposes a port to the host machine (e.g., 8080).
  • The scaled services (e.g., your api service) do not expose any ports to the host. They are all on the same internal Docker network as the load balancer.
  • The load balancer accepts all incoming traffic and distributes it among the available api instances in a round-robin fashion.

This way, you have a single entry point (http://localhost:8080), and Docker Compose handles the internal routing from the load balancer to your fleet of scaled api containers.


2. The docker-compose up --scale Command

Docker Compose makes scaling simple with a dedicated command-line flag. The core command syntax is:

docker compose up -d --scale <service-name>=<number-of-replicas>

Let's break this down:

  • up -d: Starts the services in detached mode, as we've done before.
  • --scale: This is the flag that enables scaling.
  • <service-name>=<number-of-replicas>: You specify which service you want to scale and how many instances (replicas) you want to run.

To get a formal introduction to this command, the following reading is a great starting point.

Scaling Services with Docker Compose

The article 'Scaling Services with Docker Compose' from CodeSignal provides a clear and direct explanation of the main command we'll be using.

Read the sections 'Exploring the Docker Compose Scale Command' and 'Example: Scaling a Web Service'. Focus on the syntax of the command and the example provided.

This command imperatively tells Docker Compose to override the number of containers for a service for this specific run. It's perfect for development or for manually responding to load changes.


3. Hands-On Practice: Scaling an API with a Load Balancer

Let's put this into practice. We'll create a simple two-service application: a Python Flask api and an nginx load balancer.

Step 1: Set Up Your Project

Create a new project directory and create the following files inside it.

  1. api/Dockerfile: This will build our simple API service.

    FROM python:3.9-slim
    WORKDIR /app
    RUN pip install Flask
    COPY app.py .
    CMD ["flask", "run", "--host=0.0.0.0", "--port=5000"]
    
  2. api/app.py: A minimal Flask app that returns its own container hostname. This will allow us to see the load balancer in action.

    import os
    from flask import Flask
    
    app = Flask(__name__)
    
    @app.route('/')
    def hello():
        # The hostname is the container ID in this case
        hostname = os.uname()[1]
        return f"Hello! This request was served by container: {hostname}\n"
    
  3. nginx.conf: The configuration for our Nginx load balancer. This tells Nginx to distribute traffic to the api service on port 5000.

    events {}
    
    http {
        upstream backend {
            # 'api' is the service name from docker-compose.yml
            # Docker's internal DNS will resolve this to the available containers.
            server api:5000;
        }
    
        server {
            listen 80;
            server_name localhost;
    
            location / {
                proxy_pass http://backend;
            }
        }
    }
    
  4. docker-compose.yml: This file ties everything together.

    version: '3.8'
    
    services:
      api:
        build: ./api
        # No 'ports' section here! The load balancer handles external traffic.
        networks:
          - app-network
    
      load_balancer:
        image: nginx:1.21-alpine
        ports:
          - "8080:80" # Expose the load balancer on host port 8080
        volumes:
          - ./nginx.conf:/etc/nginx/nginx.conf:ro # Mount our config
        depends_on:
          - api
        networks:
          - app-network
    
    networks:
      app-network:
        driver: bridge
    

    Notice that the api service does not expose any ports to the host. Only the load_balancer does.

Step 2: Scale and Run the Application

Now, from your terminal in the project directory, run the following command to build the images and start the services, scaling the api service to 3 instances.

docker compose up -d --build --scale api=3

You should see output indicating that it's creating three containers for the api service and one for the load_balancer.

Step 3: Verify the Scaled Services

How do we know it worked? We can use the docker compose ps command to list the running containers for our project.

docker compose ps

The output should look something like this, clearly showing three api containers and one load_balancer container running:

NAME                           COMMAND                  SERVICE         STATUS          PORTS
myproject-api-1                "flask run --host=0.…"   api             running
myproject-api-2                "flask run --host=0.…"   api             running
myproject-api-3                "flask run --host=0.…"   api             running
myproject-load_balancer-1      "/docker-entrypoint.…"   load_balancer   running         0.0.0.0:8080->80/tcp

This verification step is crucial. To reinforce this, take a look at the final section of the CodeSignal article.

Scaling Services with Docker Compose

To see how to check that our scaling command worked, let's refer back to the CodeSignal article.

Read the section 'Monitoring and Verifying Scaled Services'. This shows you how to use docker compose ps to confirm that the correct number of instances are running.

Step 4: Test the Load Balancer

Now for the fun part. Open your terminal and use curl to make several requests to the load balancer's port.

curl http://localhost:8080
curl http://localhost:8080
curl http://localhost:8080

You should see a different container ID in the response for each request, demonstrating that Nginx is distributing the traffic across your three API instances!

Hello! This request was served by container: f2a4b6c8d1e0
Hello! This request was served by container: 9a8c7b6d5e4f
Hello! This request was served by container: 3d2e1f0a9b8c

To scale down, you simply run the command again with the new desired number. For example, to go back to a single instance:

docker compose up -d --scale api=1

Docker Compose will gracefully stop the extra containers. Finally, to clean up all the containers for this project, run:

docker compose down
Test your understanding!

You have a docker-compose.yml file with three services: frontend, api, and cache. You expect a surge in API traffic, but the frontend and cache services have low, stable workloads.

How would you run your application to have 5 instances of the api service while leaving the frontend and cache services with their default single instance?

Show answer

You would use the --scale flag specifically for the api service:

docker compose up -d --scale api=5

Docker Compose will start one instance of frontend, one instance of cache, and five instances of api.


Conclusion

Congratulations! You've just added a powerful and essential tool to your DevOps toolkit. You now understand not just how to scale a service with Docker Compose, but also why it's necessary and how to handle the networking implications with a load balancer.

Key Takeaways:

  • Horizontal scaling (adding more instances) is a primary strategy for handling increased application load.
  • The docker compose up -d --scale <service>=<N> command is used to run a specific number of instances for a service.
  • When scaling services that listen on a port, a load balancer is required to provide a single entry point and distribute traffic, avoiding host port conflicts.
  • You can verify the number of running instances with docker compose ps.

Next Up

We've now covered all the individual building blocks for creating a multi-container application: defining services, managing configuration with .env files, ensuring reliable startup with depends_on, and now, handling load with --scale. In our final lesson for this module, we will bring all these concepts together to launch, manage, and troubleshoot a full multi-tier application, solidifying your mastery of Docker Compose.