9. Advanced Compose & Scaling | The Complete Docker Handbook.

7 mins read
0 Like
17 Views

Welcome to Article 9 of The Complete Docker Handbook.

In Article 8, you mastered the basics of Docker Compose. You learned how to define multiple services in a YAML file and launch them with a single command. This is a huge leap forward from running individual docker run commands.

However, the basic setup we covered is primarily suitable for local development. In production, applications need to be resilient, scalable, and environment-aware.

  • What happens if your database takes 10 seconds to start, but your API tries to connect immediately?
  • What if your API container crashes at 3 AM?
  • How do you handle different configurations for Development vs. Production without maintaining two separate YAML files?

In this article, we will level up your Compose skills. We will cover health checks, restart policies, scaling services, and managing complex configurations using profiles and override files.


1. Health Checks: Ensuring Readiness

In Article 8, we used depends_on to ensure the database started before the API.

Yaml
1depends_on:
2  - db

The Problem: depends_on only waits for the container to be running, not for the application inside to be ready. If your database takes 15 seconds to initialize, your API might crash because it tried to connect too early.

The Solution: Use Health Checks.

Docker can run a command inside your container periodically to check if the app is healthy. Compose can wait for this health status before starting dependent services.

Example: Adding Health Checks

Yaml
1version: '3.8'
2
3services:
4  db:
5    image: postgres:13
6    healthcheck:
7      test: ["CMD-SHELL", "pg_isready -U postgres"]
8      interval: 5s
9      timeout: 5s
10      retries: 5
11      start_period: 10s
12
13  api:
14    build: ./backend
15    depends_on:
16      db:
17        condition: service_healthy

Breakdown:

  • test: The command to run. pg_isready is a PostgreSQL utility that returns success if the DB is accepting connections.
  • interval: How often to run the check.
  • condition: service_healthy: Tells Compose to wait until the db reports "healthy" before starting api.

2. Restart Policies: Handling Crashes

Containers can crash due to errors, out-of-memory issues, or host reboots. You don't want to manually restart them every time.

You can define a restart policy in your Compose file.

Policy Behavior Use Case
no Never restart. Default. Good for one-off tasks.
always Always restart if stopped. Production services.
on-failure Restart only if exit code is non-zero. When you want to debug crashes manually.
unless-stopped Restart unless explicitly stopped by user. Recommended for most services.

Example:

Yaml
1services:
2  api:
3    build: ./backend
4    restart: unless-stopped

If the API crashes, Docker will automatically bring it back up.


3. Scaling Services

Sometimes one container isn't enough to handle the traffic. You might want to run multiple instances of your API service behind a load balancer.

The Command

You can scale services directly from the command line:

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

This will start 3 instances of the api service (e.g., project-api-1, project-api-2, project-api-3).

⚠️ The Port Mapping Limitation

You cannot scale services that have host port mappings defined in the YAML file.

  • Error: If api maps port 5000:5000, Docker cannot start 3 containers because only one can bind to host port 5000.
  • Solution: Remove the ports mapping from the YAML for scaled services. Instead, put them behind a reverse proxy (like Nginx or Traefik) that balances traffic to the internal container ports.

Example Scaling Setup

Yaml
1services:
2  api:
3    build: ./backend
4    # No ports mapping here for scaling
5    expose:
6      - "5000"
7  
8  nginx:
9    image: nginx
10    ports:
11      - "80:80"
12    # Configure nginx to load balance between api instances

4. Profiles: Managing Different Environments

Sometimes you want to run extra services only in specific scenarios.

  • Example: You have a debugger service or a ui-monitor that you only want to run during development, not in production.

Compose Profiles allow you to tag services and opt-in to them.

Example:

Yaml
1services:
2  api:
3    build: ./backend
4  
5  debugger:
6    image: busybox
7    command: top
8    profiles:
9      - debug

Usage:

  • Normal Run: docker compose up (The debugger service is ignored).
  • Debug Run: docker compose --profile debug up (The debugger service starts).

This keeps your main file clean while allowing flexibility.


5. Override Files: Dev vs. Prod

Maintaining two separate files (docker-compose.dev.yml and docker-compose.prod.yml) often leads to duplication and errors. Docker Compose supports multiple files that merge together.

By default, Compose looks for:

  1. docker-compose.yml (Base configuration)
  2. docker-compose.override.yml (Local overrides)

Strategy

  • docker-compose.yml: Contains production-ready settings (no port maps, secure env vars, restart policies).
  • docker-compose.override.yml: Contains development settings (port maps, volume mounts for code, debug env vars). Add this file to .gitignore so it doesn't get committed.

Example Override

docker-compose.yml (Base)

Yaml
1services:
2  api:
3    build: ./backend
4    # No ports exposed in base

docker-compose.override.yml (Local Dev)

Yaml
1services:
2  api:
3    ports:
4      - "5000:5000"  # Expose for local testing
5    volumes:
6      - ./backend:/app # Live code reloading

Result:

  • When you run docker compose up locally, it merges both files.
  • When you deploy to production, you only use the base file (or specify -f docker-compose.yml).

6. Summary of Advanced Commands

Command Description
docker compose up --scale = Run multiple instances of a service.
docker compose --profile up Enable services with specific profiles.
docker compose ps View status of all scaled instances.
docker compose logs -f Follow logs for a specific service.
docker compose exec Run a command inside a running service.

Best Practices Recap

  1. Always use Health Checks for databases and critical services.
  2. Use unless-stopped restart policy for production services.
  3. Avoid Port Mapping for services you intend to scale.
  4. Use Override Files to separate Dev and Prod configurations cleanly.
  5. Use Profiles for optional tools (debuggers, monitors).

Summary Checklist

By the end of this article, you should be able to:

  • Configure health checks to ensure service readiness.
  • Set restart policies for automatic recovery.
  • Scale services using --scale.
  • Understand the limitation of port mapping with scaling.
  • Use Profiles to enable optional services.
  • Implement docker-compose.override.yml for local development.

What's Next?

You now have a robust, scalable, and manageable container environment. But as you move closer to production, Security becomes the top priority.

Running containers as root, exposing unnecessary ports, or hardcoding secrets can lead to severe vulnerabilities.

In Article 10, we will dive into Docker Security Best Practices.

  • Scanning images for vulnerabilities.
  • Running containers as non-root users.
  • Managing secrets securely.
  • Resource limits to prevent Denial of Service.

Link: Read Article 10: Docker Security Best Practices


Challenge: Take your Compose file from Article 8. Add a health check to the database. Create an override file that maps ports for local development. Try scaling the API service to 3 instances (remember to remove port mappings first!).

Next Up: Docker Security Best Practices

Share:

Comments

0
Join the conversation

Sign in to share your thoughts and connect with other readers

No comments yet

Be the first to share your thoughts!