Docker Course #6: Docker Compose — Multi-Container Applications
Welcome to the Docker Course - Part 6 of 10. In this article, you will learn how to use Docker Compose to define and manage multi-container applications with a single configuration file.

Source: Wikimedia Commons
Up until now, we have been running individual containers with docker run. But real-world applications often need multiple services working together: a web server, a database, a cache, a message queue, and more. Managing all of these individually with separate docker run commands quickly becomes impractical.
Docker Compose solves this problem by letting you define all your services, networks, and volumes in a single YAML file and manage them with simple commands.
What Is Docker Compose?
Docker Compose is a tool for defining and running multi-container Docker applications. With Compose, you use a YAML file to configure your application's services, networks, and volumes. Then, with a single command, you create and start all the services from your configuration.
Key benefits of Docker Compose:
- Single file configuration: All services defined in one
docker-compose.yml(orcompose.yml) file - One-command deployment: Start everything with
docker compose up - Environment consistency: Everyone on the team uses the same setup
- Isolated environments: Compose creates a dedicated network for your services
- Reproducibility: The same file works on any machine with Docker installed
Docker Compose comes built into Docker Desktop and modern Docker Engine installations. You use it with the docker compose command (note: the older docker-compose with a hyphen is deprecated).
docker compose version. You should be using Compose V2 or later for the best experience.
The docker-compose.yml Structure
The Compose file is a YAML file that defines your entire application stack. Here is the basic structure:
1# docker-compose.yml (or compose.yml)
2
3# Optional: specify the Compose file version (modern Compose V2 does not require this)
4# version: "3.8"
5
6services:
7 # Each service is a container
8 service-name:
9 image: image-name:tag
10 ports:
11 - "host-port:container-port"
12 environment:
13 - KEY=value
14 volumes:
15 - volume-name:/path/in/container
16 networks:
17 - network-name
18
19volumes:
20 volume-name:
21
22networks:
23 network-name:
Let us break down each top-level key:
| Key | Purpose |
|---|---|
services |
Defines the containers that make up your application |
volumes |
Declares named volumes for persistent data |
networks |
Declares custom networks for inter-service communication |
Services, Networks, and Volumes
Let us explore each of these core concepts in detail.
Services
A service is a container definition. Each service specifies what image to use (or how to build it), what ports to expose, what environment variables to set, and more.
1services:
2 web:
3 image: nginx:alpine
4 ports:
5 - "8080:80"
6 volumes:
7 - ./html:/usr/share/nginx/html:ro
8 restart: unless-stopped
9
10 api:
11 build:
12 context: ./api
13 dockerfile: Dockerfile
14 ports:
15 - "3000:3000"
16 environment:
17 - NODE_ENV=production
18 - DB_HOST=database
19 depends_on:
20 - database
21
22 database:
23 image: postgres:16-alpine
24 environment:
25 POSTGRES_DB: myapp
26 POSTGRES_USER: appuser
27 POSTGRES_PASSWORD: secret123
28 volumes:
29 - db-data:/var/lib/postgresql/data
30 ports:
31 - "5432:5432"
32
33volumes:
34 db-data:
Common service configuration options:
image- The Docker image to usebuild- Build from a Dockerfile instead of pulling an imageports- Port mappings (host:container)environment- Environment variables (list or map format)volumes- Volume mounts (named volumes or bind mounts)restart- Restart policy (no,always,unless-stopped,on-failure)command- Override the default commanddepends_on- Service dependencies
Networks
By default, Compose creates a single network for your application. All services can reach each other using the service name as hostname. You can also define custom networks for isolation:
1services:
2 frontend:
3 image: nginx:alpine
4 networks:
5 - frontend-net
6
7 api:
8 image: node:20-alpine
9 networks:
10 - frontend-net
11 - backend-net
12
13 database:
14 image: postgres:16-alpine
15 networks:
16 - backend-net
17
18networks:
19 frontend-net:
20 backend-net:
In this example, the frontend can talk to the api but cannot directly reach the database. The api sits in both networks and acts as the bridge.
Volumes
Named volumes persist data across container restarts and recreations:
1services:
2 database:
3 image: mysql:8
4 volumes:
5 - mysql-data:/var/lib/mysql # Named volume
6 - ./init.sql:/docker-entrypoint-initdb.d/init.sql # Bind mount
7
8volumes:
9 mysql-data:
10 driver: local
Essential Docker Compose Commands
Here are the commands you will use every day with Docker Compose:
1# Start all services (detached mode)
2docker compose up -d
3
4# Start all services (foreground with logs)
5docker compose up
6
7# Stop all services
8docker compose down
9
10# Stop and remove volumes (WARNING: deletes data!)
11docker compose down -v
12
13# View running services
14docker compose ps
15
16# View logs for all services
17docker compose logs
18
19# View logs for a specific service (follow mode)
20docker compose logs -f api
21
22# Restart a specific service
23docker compose restart api
24
25# Rebuild images and start
26docker compose up -d --build
27
28# Execute a command in a running service
29docker compose exec database psql -U appuser -d myapp
30
31# Scale a service (run multiple instances)
32docker compose up -d --scale api=3
33
34# Pull latest images
35docker compose pull
36
37# View resource usage
38docker compose top
docker compose down -v removes named volumes and all their data. Use it with caution, especially with database volumes. Use docker compose down (without -v) to stop services while keeping your data.
Environment Variables in Compose
Docker Compose offers several ways to manage environment variables:
Method 1: Inline in the Compose file
1services:
2 api:
3 image: myapi:latest
4 environment:
5 - DATABASE_URL=postgresql://user:pass@db:5432/myapp
6 - REDIS_URL=redis://cache:6379
7 - NODE_ENV=production
Method 2: Using a .env file
Create a .env file in the same directory as your docker-compose.yml:
1# .env file
2POSTGRES_DB=myapp
3POSTGRES_USER=appuser
4POSTGRES_PASSWORD=supersecret123
5API_PORT=3000
Then reference those variables in your Compose file:
1services:
2 database:
3 image: postgres:16-alpine
4 environment:
5 POSTGRES_DB: ${POSTGRES_DB}
6 POSTGRES_USER: ${POSTGRES_USER}
7 POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
8 ports:
9 - "${API_PORT:-3000}:5432"
Method 3: env_file directive
1services:
2 api:
3 image: myapi:latest
4 env_file:
5 - ./config/api.env
6 - ./config/db.env
.env files with real passwords to version control. Add .env to your .gitignore and provide a .env.example file with placeholder values instead.
depends_on and Service Startup Order
The depends_on directive controls the order in which services start. However, it only waits for the container to start, not for the service inside to be ready.
1services:
2 api:
3 build: ./api
4 depends_on:
5 - database
6 - cache
7
8 database:
9 image: postgres:16-alpine
10
11 cache:
12 image: redis:7-alpine
In this example, Docker Compose will start database and cache before api. But the database might not be ready to accept connections yet when the API tries to connect.
To solve this, use depends_on with health checks (covered in the next section):
1services:
2 api:
3 build: ./api
4 depends_on:
5 database:
6 condition: service_healthy
7 cache:
8 condition: service_started
9
10 database:
11 image: postgres:16-alpine
12 healthcheck:
13 test: ["CMD-SHELL", "pg_isready -U appuser"]
14 interval: 5s
15 timeout: 5s
16 retries: 5
17
18 cache:
19 image: redis:7-alpine
Now the API will wait until the database health check passes before starting.
Health Checks in Compose
Health checks tell Docker how to test whether a service is working correctly. This is essential for reliable service startup ordering and for production deployments.
1services:
2 web:
3 image: nginx:alpine
4 healthcheck:
5 test: ["CMD", "curl", "-f", "http://localhost"]
6 interval: 30s
7 timeout: 10s
8 retries: 3
9 start_period: 10s
10
11 database:
12 image: postgres:16-alpine
13 environment:
14 POSTGRES_PASSWORD: secret123
15 healthcheck:
16 test: ["CMD-SHELL", "pg_isready -U postgres"]
17 interval: 10s
18 timeout: 5s
19 retries: 5
20 start_period: 30s
21
22 cache:
23 image: redis:7-alpine
24 healthcheck:
25 test: ["CMD", "redis-cli", "ping"]
26 interval: 10s
27 timeout: 5s
28 retries: 3
29
30 api:
31 build: ./api
32 depends_on:
33 database:
34 condition: service_healthy
35 cache:
36 condition: service_healthy
37 healthcheck:
38 test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
39 interval: 15s
40 timeout: 5s
41 retries: 3
42 start_period: 15s
Health check parameters explained:
test- The command to run to check healthinterval- Time between checks (default 30s)timeout- Maximum time to wait for a check (default 30s)retries- Number of consecutive failures needed to mark as unhealthy (default 3)start_period- Grace period for the container to initialize (default 0s)
docker compose ps. The status column will show healthy, unhealthy, or starting.
Practical Example: WordPress + MySQL + phpMyAdmin
Let us build a complete stack with WordPress, MySQL, and phpMyAdmin. This is a real-world example that demonstrates all the concepts we have covered.
Create a new directory and the Compose file:
1# Create project directory
2mkdir wordpress-stack && cd wordpress-stack
3
4# Create the compose file
5touch docker-compose.yml
6
7# Create the .env file
8touch .env
First, create the .env file with your configuration:
1# .env
2MYSQL_ROOT_PASSWORD=rootpass123
3MYSQL_DATABASE=wordpress
4MYSQL_USER=wpuser
5MYSQL_PASSWORD=wppass123
6WORDPRESS_PORT=8080
7PMA_PORT=8081
Now create the docker-compose.yml:
1# docker-compose.yml - WordPress + MySQL + phpMyAdmin
2
3services:
4 # MySQL Database
5 database:
6 image: mysql:8.0
7 restart: unless-stopped
8 environment:
9 MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
10 MYSQL_DATABASE: ${MYSQL_DATABASE}
11 MYSQL_USER: ${MYSQL_USER}
12 MYSQL_PASSWORD: ${MYSQL_PASSWORD}
13 volumes:
14 - mysql-data:/var/lib/mysql
15 networks:
16 - wp-network
17 healthcheck:
18 test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"]
19 interval: 10s
20 timeout: 5s
21 retries: 5
22 start_period: 30s
23
24 # WordPress Application
25 wordpress:
26 image: wordpress:6-apache
27 restart: unless-stopped
28 depends_on:
29 database:
30 condition: service_healthy
31 ports:
32 - "${WORDPRESS_PORT:-8080}:80"
33 environment:
34 WORDPRESS_DB_HOST: database:3306
35 WORDPRESS_DB_NAME: ${MYSQL_DATABASE}
36 WORDPRESS_DB_USER: ${MYSQL_USER}
37 WORDPRESS_DB_PASSWORD: ${MYSQL_PASSWORD}
38 volumes:
39 - wp-content:/var/www/html/wp-content
40 networks:
41 - wp-network
42 healthcheck:
43 test: ["CMD", "curl", "-f", "http://localhost:80"]
44 interval: 30s
45 timeout: 10s
46 retries: 3
47 start_period: 30s
48
49 # phpMyAdmin - Database Management UI
50 phpmyadmin:
51 image: phpmyadmin:5
52 restart: unless-stopped
53 depends_on:
54 database:
55 condition: service_healthy
56 ports:
57 - "${PMA_PORT:-8081}:80"
58 environment:
59 PMA_HOST: database
60 PMA_PORT: 3306
61 MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
62 networks:
63 - wp-network
64
65volumes:
66 mysql-data:
67 name: wordpress-mysql-data
68 wp-content:
69 name: wordpress-content
70
71networks:
72 wp-network:
73 name: wordpress-network
74 driver: bridge
Now let us start the entire stack:
1# Start all services in detached mode
2docker compose up -d
3
4# Watch the logs to see startup progress
5docker compose logs -f
6
7# Check the status of all services
8docker compose ps
9
10# Once everything is healthy, access:
11# WordPress: http://localhost:8080
12# phpMyAdmin: http://localhost:8081
13
14# To stop everything (keeping data)
15docker compose down
16
17# To stop everything AND delete all data
18docker compose down -v
database as the hostname (Docker's built-in DNS resolves this automatically). All three services share the wp-network so they can communicate.
Common Patterns and Best Practices
Here are some patterns you will use frequently with Docker Compose:
Override files for different environments
Use a base file and override files for different environments:
1# docker-compose.yml (base)
2services:
3 api:
4 build: ./api
5 environment:
6 - NODE_ENV=production
7
8# docker-compose.override.yml (auto-loaded in development)
9services:
10 api:
11 volumes:
12 - ./api/src:/app/src # Hot reload
13 environment:
14 - NODE_ENV=development
15 - DEBUG=true
16 ports:
17 - "9229:9229" # Debug port
1# Development (auto-loads override)
2docker compose up -d
3
4# Production (only base file)
5docker compose -f docker-compose.yml up -d
6
7# Explicit multiple files
8docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
Wait-for scripts
For applications that need more sophisticated startup checks:
1services:
2 api:
3 build: ./api
4 command: ["./wait-for-it.sh", "database:5432", "--", "node", "server.js"]
5 depends_on:
6 - database
Profiles for optional services
1services:
2 api:
3 image: myapi:latest
4
5 database:
6 image: postgres:16-alpine
7
8 # Only starts when explicitly requested
9 debug-tools:
10 image: busybox
11 profiles:
12 - debug
13
14 monitoring:
15 image: grafana/grafana
16 profiles:
17 - monitoring
1# Start only default services
2docker compose up -d
3
4# Start default services + monitoring
5docker compose --profile monitoring up -d
Summary and Next Steps
In this article, you learned how to use Docker Compose to orchestrate multi-container applications. Here is what we covered:
- Docker Compose basics: What it is and why you need it
- YAML structure: Services, networks, and volumes
- Essential commands: up, down, logs, ps, exec, and more
- Environment variables: Inline, .env files, and env_file
- depends_on: Controlling service startup order
- Health checks: Ensuring services are truly ready
- Practical example: WordPress + MySQL + phpMyAdmin stack
- Best practices: Override files, profiles, and common patterns
For more details, check the official Docker Compose documentation: https://docs.docker.com/compose/
Comments
Sign in to leave a comment
No comments yet. Be the first!