Chapter 1.9 - Mastering Docker Compose - A Modern, Best-Practices Guide

Introduction

Docker Compose is an official and powerful tool from Docker for defining and running multi-container applications. It simplifies the management of complex application stacks by allowing you to configure all of your application’s services, networks, and volumes in a single declarative YAML file.

The code is open-source and available on GitHub: https://github.com/docker/compose.

While a Dockerfile is used to build an image for a single container, real-world applications are often composed of multiple, interconnected services. For example, a web application might consist of a frontend web server, a backend API, a database, and a caching layer. Each of these components would run in its own container.

Docker Compose allows you to manage this entire stack as a single unit. You use a YAML file, conventionally named compose.yml or docker-compose.yml, to define the project.

There are two core concepts in Docker Compose:

Originally a Python project called Fig, Compose has been rewritten and is now integrated directly into the Docker CLI as a plugin. This guide focuses on the modern docker compose command (with a space), which is the current best practice.


Installation

As of Docker Engine v20.10, Docker Compose is included as a plugin, docker compose, directly within the Docker CLI. The older, standalone Python version (docker-compose) is now deprecated.

Docker Desktop (macOS, Windows, and Linux)

If you have installed Docker Desktop, you already have Docker Compose. It is bundled by default. You can verify the installation by running:

docker compose version

This should output the version of the Docker Compose plugin, for example: Docker Compose version v2.27.0.

Linux Server

On Linux, if you installed Docker Engine without Docker Desktop, you may need to install the Compose plugin separately.

The easiest way is to install it from Docker’s official package repository.

For Debian/Ubuntu:

sudo apt-get update
sudo apt-get install docker-compose-plugin

For CentOS/RHEL/Fedora:

sudo dnf install docker-compose-plugin

2. Manual Binary Installation

If you cannot use the package manager, you can install the plugin by downloading its binary from the official GitHub Releases page.

  1. Download the appropriate binary for your system architecture. For Linux x86-64:

    # Set the version you want to install
    COMPOSE_VERSION="v2.27.0"
    
    # Create the directory for CLI plugins
    mkdir -p ~/.docker/cli-plugins/
    
    # Download the binary
    curl -SL "https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-linux-x86_64" -o ~/.docker/cli-plugins/docker-compose
    
  2. Make the binary executable:

    chmod +x ~/.docker/cli-plugins/docker-compose
    
  3. Verify the installation:

    docker compose version
    

To make using docker compose easier, you can install command completion scripts.

# For bash on Ubuntu/Debian
sudo apt-get install bash-completion

# For other systems, you may need to download it manually
# sudo curl -L https://raw.githubusercontent.com/docker/compose/v2/contrib/completion/bash/docker-compose -o /etc/bash_completion.d/docker-compose

How to Uninstall


Example Project: A Simple Web Counter

Let’s build a simple web application that uses Flask and Redis to count the number of times a page has been visited.

First, create a new directory for your project and navigate into it.

1. The Python Application (app.py)

Create a file named app.py with the following content. This script sets up a simple web server and connects to a Redis service named redis.

import time
import redis
from flask import Flask

app = Flask(__name__)
# The hostname 'redis' is used to connect to the Redis container
# because Docker's internal networking will resolve it.
cache = redis.Redis(host='redis', port=6379)

def get_hit_count():
    """Connects to Redis and increments the 'hits' counter."""
    retries = 5
    while True:
        try:
            # The 'incr' command is atomic, ensuring thread safety.
            return cache.incr('hits')
        except redis.exceptions.ConnectionError as exc:
            if retries == 0:
                raise exc
            retries -= 1
            time.sleep(0.5)

@app.route('/')
def hello():
    count = get_hit_count()
    return f'Hello from Docker! I have been seen {count} times.\n'

if __name__ == "__main__":
    # Host 0.0.0.0 makes the server accessible from outside the container.
    app.run(host="0.0.0.0", port=5000, debug=True)

2. The Dockerfile (Dockerfile)

Next, create a Dockerfile to containerize the Python application. Using a -slim base image is a good practice as it provides a good balance between size and compatibility.

# Use a specific and recent version of Python for reproducibility.
FROM python:3.11-slim-bookworm

# Set a working directory inside the container.
WORKDIR /code

# Copy dependency file and install dependencies.
# This leverages Docker's layer caching.
COPY requirements.txt requirements.txt
RUN pip install --no-cache-dir -r requirements.txt

# Copy the rest of the application code.
COPY . .

# Command to run the application.
CMD ["python", "app.py"]

Create a requirements.txt file for the Python dependencies:

flask
redis

3. The Compose File (compose.yml)

This is the main file where you define your services. Create a file named compose.yml. This version introduces best practices like named volumes for data persistence and a dedicated network for communication.

# compose.yml
services:
  # The Python web application service
  web:
    build: .
    ports:
      - "8000:5000" # Expose port 8000 on the host and map it to 5000 in the container
    volumes:
      - .:/code # Bind mount for live code changes during development
    networks:
      - counter-net
    depends_on:
      - redis # Ensures redis starts before the web service

  # The Redis service
  redis:
    image: "redis:7-alpine" # Use a specific version of the Redis image
    volumes:
      - redis-data:/data # Use a named volume to persist Redis data
    networks:
      - counter-net

# Define the network
networks:
  counter-net:
    driver: bridge

# Define the named volume
volumes:
  redis-data:

4. Running the Project

Now, from your project directory, run the following command:

docker compose up --build

You will see logs from both the web and redis services in your terminal. Open your web browser and navigate to http://localhost:8000. You should see “Hello from Docker! I have been seen 1 times.” Each time you refresh the page, the counter will increment.

To stop and remove all the project’s containers and networks, press Ctrl-C in the terminal, and then run:

# The --volumes flag also removes the named volume (redis-data)
docker compose down --volumes

Common Docker Compose Commands

The basic syntax is docker compose [OPTIONS] [COMMAND]. Here are the most essential commands:

Project Lifecycle Commands

Service Management Commands

Image and Build Commands

Configuration and Cleanup