9. Advanced Compose & Scaling | The Complete Docker Handbook.
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.
Yaml1depends_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
Yaml1version: '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_isreadyis 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 thedbreports "healthy" before startingapi.
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:
Yaml1services:
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:
Bashdocker 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
apimaps port5000:5000, Docker cannot start 3 containers because only one can bind to host port 5000. - Solution: Remove the
portsmapping 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
Yaml1services:
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
debuggerservice or aui-monitorthat you only want to run during development, not in production.
Compose Profiles allow you to tag services and opt-in to them.
Example:
Yaml1services:
2 api:
3 build: ./backend
4
5 debugger:
6 image: busybox
7 command: top
8 profiles:
9 - debug
Usage:
- Normal Run:
docker compose up(Thedebuggerservice is ignored). - Debug Run:
docker compose --profile debug up(Thedebuggerservice 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:
docker-compose.yml(Base configuration)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.gitignoreso it doesn't get committed.
Example Override
docker-compose.yml (Base)
Yaml1services:
2 api:
3 build: ./backend
4 # No ports exposed in base
docker-compose.override.yml (Local Dev)
Yaml1services:
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 uplocally, 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
- Always use Health Checks for databases and critical services.
- Use
unless-stoppedrestart policy for production services. - Avoid Port Mapping for services you intend to scale.
- Use Override Files to separate Dev and Prod configurations cleanly.
- 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.ymlfor 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