8. Introduction to Docker Compose | The Complete Docker Handbook.

7 mins read
1 Like
24 Views

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?

  1. Infrastructure as Code: Your setup is version-controlled in a file.
  2. Simplicity: One command to start/stop the whole project.
  3. Isolation: Compose creates a unique project name for your containers, preventing conflicts with other projects.
  4. 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:

Bash
docker 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.

Yaml
1version: '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:

  1. version: Specifies the Compose file format (usually '3.8' or latest).
  2. services: Defines the containers. Each key (e.g., web, db) becomes a container name.
  3. volumes: Defines named volumes to be created.
  4. 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 Text
1compose-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.

Python
1import 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 Text
1FROM 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 Text
1DB_HOST=db
2DB_NAME=mydb
3DB_USER=postgres
4DB_PASSWORD=supersecret

Step 5: The Compose File (docker-compose.yml)

Yaml
1version: '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 api service connects to the database using the hostname db. Compose automatically creates a network and assigns DNS names based on service keys.
  • Volumes: The pg-data volume ensures your database survives even if you delete the db container.
  • Build vs Image: The api service builds from your local code, while db pulls a pre-made image.

Essential Docker Compose Commands

Now that you have the file, managing the stack is easy.

1. Start the Application

Bash
docker compose up
  • Runs in the foreground. You see logs from all containers.
  • Press Ctrl+C to stop.

2. Start in Detached Mode (Background)

Bash
docker compose up -d
  • Runs in the background. Recommended for production/dev work.

3. View Logs

Bash
docker compose logs
  • Add -f to follow logs in real-time (docker compose logs -f).
  • View specific service: docker compose logs api.

4. View Status

Bash
docker compose ps
  • Shows running containers, ports, and state.

5. Stop and Remove

Bash
docker 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:

Bash
docker compose up -d --build

Best Practices for Compose

  1. Use .env for Secrets: As shown above, keep passwords out of the YAML file.
  2. Pin Versions: Don't use postgres:latest. Use postgres:13-alpine.
  3. Healthchecks: In production, use depends_on with condition: service_healthy to ensure the DB is ready before the API starts (covered in Article 9).
  4. Resource Limits: You can limit CPU/Memory per service in the YAML to prevent one container from hogging resources.
  5. 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_on with health checks (Article 9).
  • Fix: Ensure DB_HOST matches the service name (db), not localhost. Inside the network, localhost refers 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.yml file.
  • Use up, down, logs, and ps commands.
  • Connect services using service names as hostnames.
  • Manage secrets using .env files.
  • 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

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!