8. Introduction to Docker Compose | The Complete Docker Handbook.
Welcome to Article 8 of The Complete Docker Handbook.
In Article 7, we mastered Docker Networking. You learned how to connect containers using custom networks and DNS. But imagine you are deploying a real-world application. You might need:
- 1 Frontend Container
- 1 Backend API Container
- 1 Database Container
- 1 Redis Cache Container
- Custom Networks for security
- Volumes for data persistence
To start this stack manually, you would need to run 4+ long docker run commands with countless flags (-v, -p, --network, -e). One typo, and the whole system breaks.
There is a better way. Docker Compose.
In this article, you will learn how to define your entire application stack in a single file and launch it with one command.
What is Docker Compose?
Docker Compose is a tool for defining and running multi-container Docker applications.
- Configuration: You describe your services (containers), networks, and volumes in a YAML file (
docker-compose.yml). - Execution: You use a single command (
docker compose up) to create and start all services from your configuration.
Why use it?
- Infrastructure as Code: Your setup is version-controlled in a file.
- Simplicity: One command to start/stop the whole project.
- Isolation: Compose creates a unique project name for your containers, preventing conflicts with other projects.
- Automatic Networking: Services can communicate with each other using their service names automatically.
Installation Check
If you installed Docker Desktop (Mac/Windows) in Article 2, you already have Docker Compose installed.
Verify Installation:
Bashdocker compose version
Note: In Docker Compose V2, the command is docker compose (with a space), not docker-compose (with a hyphen). Both work, but the space version is the modern standard.
The docker-compose.yml File
The heart of Compose is the YAML file. Let's break down the structure.
Yaml1version: '3.8'
2
3services:
4 web:
5 image: nginx
6 ports:
7 - "8080:80"
8
9 db:
10 image: postgres
11 environment:
12 POSTGRES_PASSWORD: example
13
14volumes:
15 db-data:
16
17networks:
18 default:
19 driver: bridge
Key Sections:
version: Specifies the Compose file format (usually'3.8'or latest).services: Defines the containers. Each key (e.g.,web,db) becomes a container name.volumes: Defines named volumes to be created.networks: Defines custom networks (optional, as Compose creates a default one).
Common Service Directives:
| Directive | Description | Example |
|---|---|---|
image | Pull from Docker Hub. | image: postgres:13 |
build | Build from a Dockerfile. | build: ./backend |
ports | Map host to container ports. | ports: - "3000:3000" |
volumes | Mount storage. | volumes: - db-data:/var/lib/postgresql/data |
environment | Set env variables. | environment: - DB_PASS=secret |
depends_on | Start order dependency. | depends_on: - db |
restart | Restart policy. | restart: always |
Practical Example: A Full Stack App
Let's create a simple stack: A Python Flask API connected to a PostgreSQL Database.
Step 1: Project Structure
Create a folder compose-project and set up this structure:
Plain Text1compose-project/ 2├── docker-compose.yml 3├── backend/ 4│ ├── Dockerfile 5│ └── app.py 6└── .env
Step 2: The Backend Code (backend/app.py)
A simple script that connects to a DB.
Python1import os
2from flask import Flask
3import psycopg2
4
5app = Flask(__name__)
6
7def get_db_connection():
8 conn = psycopg2.connect(
9 host=os.environ.get('DB_HOST'),
10 database=os.environ.get('DB_NAME'),
11 user=os.environ.get('DB_USER'),
12 password=os.environ.get('DB_PASSWORD')
13 )
14 return conn
15
16@app.route('/')
17def hello():
18 return "Hello from Docker Compose!"
19
20if __name__ == '__main__':
21 app.run(host='0.0.0.0', port=5000)
Step 3: The Dockerfile (backend/Dockerfile)
Plain Text1FROM python:3.9-slim 2WORKDIR /app 3COPY app.py . 4RUN pip install flask psycopg2-binary 5CMD ["python", "app.py"]
Step 4: The Environment File (.env)
Never hardcode secrets in the YAML file.
Plain Text1DB_HOST=db 2DB_NAME=mydb 3DB_USER=postgres 4DB_PASSWORD=supersecret
Step 5: The Compose File (docker-compose.yml)
Yaml1version: '3.8'
2
3services:
4 api:
5 build: ./backend
6 ports:
7 - "5000:5000"
8 environment:
9 - DB_HOST=db
10 - DB_NAME=mydb
11 - DB_USER=postgres
12 - DB_PASSWORD=supersecret
13 depends_on:
14 - db
15
16 db:
17 image: postgres:13-alpine
18 volumes:
19 - pg-data:/var/lib/postgresql/data
20 environment:
21 - POSTGRES_DB=mydb
22 - POSTGRES_USER=postgres
23 - POSTGRES_PASSWORD=supersecret
24
25volumes:
26 pg-data:
Notice the Magic:
- Networking: The
apiservice connects to the database using the hostnamedb. Compose automatically creates a network and assigns DNS names based on service keys. - Volumes: The
pg-datavolume ensures your database survives even if you delete thedbcontainer. - Build vs Image: The
apiservice builds from your local code, whiledbpulls a pre-made image.
Essential Docker Compose Commands
Now that you have the file, managing the stack is easy.
1. Start the Application
Bashdocker compose up
- Runs in the foreground. You see logs from all containers.
- Press
Ctrl+Cto stop.
2. Start in Detached Mode (Background)
Bashdocker compose up -d
- Runs in the background. Recommended for production/dev work.
3. View Logs
Bashdocker compose logs
- Add
-fto follow logs in real-time (docker compose logs -f). - View specific service:
docker compose logs api.
4. View Status
Bashdocker compose ps
- Shows running containers, ports, and state.
5. Stop and Remove
Bashdocker compose down
- Stops containers and removes them, along with the default network.
- Warning: This does not delete named volumes (your DB data is safe).
- To delete volumes too:
docker compose down -v.
6. Rebuild If you change your Dockerfile or code:
Bashdocker compose up -d --build
Best Practices for Compose
- Use
.envfor Secrets: As shown above, keep passwords out of the YAML file. - Pin Versions: Don't use
postgres:latest. Usepostgres:13-alpine. - Healthchecks: In production, use
depends_onwithcondition: service_healthyto ensure the DB is ready before the API starts (covered in Article 9). - Resource Limits: You can limit CPU/Memory per service in the YAML to prevent one container from hogging resources.
- Ignore
docker-compose.override.yml: If you have dev-specific settings (like binding ports only for local dev), put them in an override file. Compose merges them automatically.
Troubleshooting Compose
1. "Service 'X' could not be found"
- Cause: Typo in service name or running command in wrong directory.
- Fix: Ensure you are in the folder containing
docker-compose.yml.
2. "Connection Refused" to Database
- Cause: The API started before the DB was ready.
- Fix: Add a retry logic in your code or use
depends_onwith health checks (Article 9). - Fix: Ensure
DB_HOSTmatches the service name (db), notlocalhost. Inside the network,localhostrefers to the container itself, not the host.
3. Orphaned Containers
- Cause: You changed service names in YAML.
- Fix: Run
docker compose up --remove-orphans.
Summary Checklist
By the end of this article, you should be able to:
- Explain the purpose of Docker Compose.
- Write a basic
docker-compose.ymlfile. - Use
up,down,logs, andpscommands. - Connect services using service names as hostnames.
- Manage secrets using
.envfiles. - Persist data using volumes within Compose.
What's Next?
You now have the power to define complex stacks easily. But production environments demand more robustness.
- What if a container crashes?
- How do you scale the API to handle more traffic?
- How do you manage different configurations for Dev vs. Prod?
In Article 9, we will cover Advanced Compose & Scaling.
- Health checks and restart policies.
- Scaling services (
--scale). - Profiles and override files.
Link: Read Article 9: Advanced Compose & Scaling
Challenge: Take the example above. Add a Redis service to the stack. Update your Python app to connect to Redis. Verify all three services (API, DB, Redis) can talk to each other using docker compose logs.
Next Up: Advanced Compose & Scaling