KAHIBARO
Discord Login Register

21.10. Docker Compose

Why Docker Compose Exists

Running a single container with docker run is simple. Running a real backend system is not.

A typical backend needs:

Starting all these with plain docker run commands quickly becomes:

Docker Compose solves this by letting you define all services in one file and start them with a single command.

Key idea: Docker Compose is a tool to define and run multi-container Docker applications using a single configuration file, usually docker-compose.yml.

You still use Docker images and containers. Compose simply orchestrates them for you.

The `docker-compose.yml` File

Docker Compose uses a YAML file, usually named docker-compose.yml, to describe:

Minimal `docker-compose.yml` Example

Here is a very simple example with a FastAPI app and PostgreSQL:

yaml
version: "3.9"
services:
  app:
    image: my-fastapi-app:latest
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql://postgres:postgres@db:5432/app_db
    depends_on:
      - db
  db:
    image: postgres:16
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=app_db
    ports:
      - "5432:5432"

You can start everything with:

bash
docker compose up

(Typically the newer command is docker compose, older installs use docker-compose.)

Understanding Services

In Compose, each service represents one type of container.

In the example above, we have 2 services:

Docker Compose will create containers with names based on:

For example, in a project folder named myproject, Compose might create:

You can see running containers with:

bash
docker ps

Building Images with Compose

Instead of using an existing image with image:, you can ask Compose to build an image using a Dockerfile.

Example directory:

text
backend/
  Dockerfile
  app/
    main.py
docker-compose.yml

docker-compose.yml:

yaml
version: "3.9"
services:
  app:
    build: ./backend
    ports:
      - "8000:8000"

This tells Compose:

You can also combine build with image to give the built image a name:

yaml
services:
  app:
    build: ./backend
    image: my-fastapi-app:dev

This is useful if you want to push the image to a registry later.

Defining Service Configuration

Each service has its own configuration. Here are common options you will use for backend development.

Ports

ports maps a host port to a container port:

yaml
services:
  app:
    ports:
      - "8000:8000"

This means:

You can also map different ports:

yaml
services:
  app:
    ports:
      - "8080:8000"

Now you access the app at http://localhost:8080, but the app inside the container still listens on port 8000.

Environment Variables

Compose supports two formats for environment variables.

List format:

yaml
services:
  app:
    environment:
      - ENV=development
      - DATABASE_URL=postgresql://postgres:postgres@db:5432/app_db

Map format (often easier to read):

yaml
services:
  app:
    environment:
      ENV: development
      DATABASE_URL: postgresql://postgres:postgres@db:5432/app_db

You can also load variables from an .env file:

.env:

env
POSTGRES_PASSWORD=supersecret
POSTGRES_DB=app_db

docker-compose.yml:

yaml
services:
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}

Compose will substitute ${POSTGRES_PASSWORD} and ${POSTGRES_DB} from .env.

Volumes

Volumes allow you to:

Example for PostgreSQL data persistence:

yaml
services:
  db:
    image: postgres:16
    volumes:
      - db_data:/var/lib/postgresql/data
volumes:
  db_data:

Here:

Example for mounting source code into a FastAPI container:

yaml
services:
  app:
    build: ./backend
    volumes:
      - ./backend:/app
    command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
    ports:
      - "8000:8000"

This is very useful in development:

Command and Entrypoint

You can override the default command defined in the Docker image.

Example:

yaml
services:
  app:
    build: ./backend
    command: uvicorn app.main:app --host 0.0.0.0 --port 8000

This is common if you want different commands for development and production, for example:

Service Dependencies and Networks

depends_on

depends_on is used to declare that one service depends on another.

Example:

yaml
services:
  app:
    build: ./backend
    depends_on:
      - db
  db:
    image: postgres:16

This means:

For real production readiness checks, you usually add your own health checks or retry logic in the app.

Service-to-Service Networking

Compose automatically creates a network for your project. Each service can reach other services by their service name.

In this configuration:

yaml
services:
  app:
    build: ./backend
    environment:
      DATABASE_URL: postgresql://postgres:postgres@db:5432/app_db
  db:
    image: postgres:16

The connection string uses db as the host, not localhost:

text
postgresql://postgres:postgres@db:5432/app_db

This works because inside the app container:

Common Docker Compose Commands

Here are the commands you will use most often in development.

CommandDescription
docker compose upStart all services, attach logs
docker compose up -dStart in detached mode (run in background)
docker compose downStop and remove containers, default network
docker compose buildBuild or rebuild service images
docker compose psList containers in the current project
docker compose logsShow logs from all services
docker compose logs appShow logs only from the app service
docker compose restart appRestart only the app service
docker compose exec app bashOpen a shell inside the app container

Example workflow for local development:

bash
# Start services
docker compose up -d
# View logs
docker compose logs -f app
# Execute a command inside app container
docker compose exec app python -m pytest
# Stop and clean up
docker compose down

Example: FastAPI + PostgreSQL + Redis with Compose

Here is a more realistic example for a backend application that uses:

Directory structure:

text
project/
  backend/
    Dockerfile
    app/
      main.py
  docker-compose.yml

backend/Dockerfile (simplified):

dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

docker-compose.yml:

yaml
version: "3.9"
services:
  app:
    build: ./backend
    image: fastapi-backend:dev
    ports:
      - "8000:8000"
    environment:
      ENV: development
      DATABASE_URL: postgresql://postgres:postgres@db:5432/app_db
      REDIS_URL: redis://redis:6379/0
    depends_on:
      - db
      - redis
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: app_db
    ports:
      - "5432:5432"
    volumes:
      - db_data:/var/lib/postgresql/data
  redis:
    image: redis:7
    ports:
      - "6379:6379"
volumes:
  db_data:

How this works:

bash
docker compose up

Development vs Production Use

You can use Docker Compose both for:

Typical patterns:

Example development-specific settings:

yaml
services:
  app:
    build: ./backend
    volumes:
      - ./backend:/app
    command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
    ports:
      - "8000:8000"

In production you might:

Summary

As you build more complex backends, Compose will be one of your primary tools for local development and small deployments.

Views: 9

Comments

Please login to add a comment.

Don't have an account? Register now!