Skip to content

Docker Compose

What Is It?

Docker Compose is a tool for defining and running multi-container Docker applications declaratively. Instead of issuing a chain of docker run commands with carefully-crafted flags, you describe the entire stack — services, networks, volumes, and their relationships — in a single docker-compose.yml file. One command then brings the whole stack up or down.

Compose is the simplest form of container orchestration. It runs on a single host (no clustering, no scheduling across machines), but it removes nearly all the manual bookkeeping of running multiple cooperating containers: dependency ordering, named networks, named volumes, environment variables, restart policies — all expressed in YAML.

For larger production setups you would graduate to Kubernetes (covered in week 11). Compose is the right tool when:

  • A single host is enough.
  • The number of services is small (3–20).
  • You want a checked-in description of "the production deployment" that someone can read without docker inspect-ing every container.

Installation

Modern Docker installations ship Docker Compose as a CLI plugin — invoked as docker compose (with a space, not a hyphen). On the lab VM, where Docker was installed in week 9, no extra packages are needed:

sudo dnf install -y docker-compose-plugin
sudo docker compose version

If docker-compose-plugin is already pulled in by the docker-ce install, dnf will say nothing changed.

docker-compose (v1, hyphenated) is deprecated

The standalone Python tool docker-compose (with a hyphen) was the original implementation. It is end-of-life — use docker compose (the plugin) instead. Most v1 commands still work in v2 with the same flags.

Key Files and Directories

Path Purpose
docker-compose.yml The Compose project definition. Lives at the project root.
compose.yml, compose.yaml Alternative names accepted by docker compose v2.
.env Optional file for environment variables interpolated into docker-compose.yml.
/var/lib/docker/volumes/ Where named volumes Compose manages live on disk.

A directory containing a docker-compose.yml is called a project. The project's name defaults to the directory name (e.g. lab10) and is used to namespace networks, volumes, and container labels Compose creates.

Configuration

docker-compose.yml is a YAML file with three top-level keys you'll use most: services, networks, and volumes. Compose creates and destroys all three together.

Minimal Working Configuration

services:
  app:
    image: registry.hpc.ut.ee/mirror/library/nginx:latest
    ports:
      - "8080:80"
    restart: unless-stopped

Bring up:

sudo docker compose up -d

Take down (stops and removes containers, but keeps named volumes):

sudo docker compose down

Important Directives

services:
The list of containers in the stack. Each key under services: is a service name (it becomes the container's hostname on the Compose network).
image:
The image to run, exactly as you'd pass to docker run. Either pulled from a registry or built locally if you also specify build:.
build:
Path to a directory with a Dockerfile. If both image and build are set, the built image gets tagged with the image name.
ports:
Port mappings, in "host:container" form. Bind to localhost ("*********:5000:5000") when the service should only be reachable through a reverse proxy on the same host.
volumes: (per-service)
Mounts. Bind-mount with /host/path:/container/path, named volume with volumename:/container/path. Named volumes must also be declared at the top-level volumes: block.
environment:
Environment variables passed into the container. Use a list (KEY=value) or a dict (KEY: value).
restart:
Same semantics as docker run --restart. Use unless-stopped for production-like behaviour: restart on crash and on host reboot, but stay stopped if you explicitly docker compose stop.
depends_on:
A service waits for others to start before starting itself. Note: by default this only waits for the dependency's container to start, not for its application to be healthy. Add a healthcheck: and use depends_on: { service: { condition: service_healthy } } for true readiness gating.
labels:
Arbitrary metadata. Reverse-proxies like Traefik consume specific label keys to discover services automatically.
networks: (top-level)
Define networks the stack uses. By default, Compose creates a single user-defined bridge network named <project>_default and attaches every service to it.
volumes: (top-level)
Define named volumes the stack uses. Without further configuration, Compose manages the lifecycle and storage location.

Reusing an Existing Named Volume or Network

A common upgrade path is migrating containers that were previously started with docker run and a manually-created named volume. To make Compose reuse the existing volume rather than create a new (empty) one, declare it as external: true:

volumes:
  minio-data:
    external: true            # use the pre-existing 'minio-data' volume

services:
  minio:
    image: registry.hpc.ut.ee/mirror/minio/minio:latest
    volumes:
      - minio-data:/data      # same name as in the top-level block

Networks work the same way:

networks:
  mynetwork:
    external: true

This is essential when migrating a stateful service (database, object store, …) so you don't lose its data.

Common Commands

# Start the entire stack in the background
sudo docker compose up -d

# View the logs of every service (follow mode)
sudo docker compose logs -f

# Logs of one service only
sudo docker compose logs -f minio

# List the running containers in this project
sudo docker compose ps

# Stop everything but keep containers around
sudo docker compose stop

# Stop and remove containers (named volumes survive by default)
sudo docker compose down

# Stop, remove containers, AND remove named volumes (DESTRUCTIVE)
sudo docker compose down -v

# Rebuild images (after editing Dockerfiles or build context)
sudo docker compose build

# Restart one service after a config change
sudo docker compose up -d --force-recreate inventory

# Check the effective merged config (interpolates env vars)
sudo docker compose config

All commands operate on the project rooted at the current directory. Use -f /path/to/docker-compose.yml to point at a project elsewhere.

Logging and Debugging

  • docker compose logs <service> aggregates the logs of every container in that service.
  • docker compose ps shows the status of each service container — use this to confirm everything actually started.
  • docker compose config parses, validates, and prints the merged configuration. Run this after any edit; it's the fastest way to catch YAML errors and broken variable references.
  • Containers managed by Compose carry these labels (visible in docker inspect):
  • com.docker.compose.project=<project name>
  • com.docker.compose.service=<service name>
  • If a container fails to start, docker compose up (without -d) attaches to the foreground and shows you the start-up output directly.

Security Considerations

  • The same security caveats apply as for raw Docker: containers run as root by default, the Docker socket is root-equivalent, and image provenance still matters. Compose does nothing to mitigate these.
  • Don't store secrets directly in docker-compose.yml if it's checked into git. Use a .env file kept out of version control, or Docker secrets.
  • restart: always will respawn a container indefinitely, including one that crashes immediately. Use unless-stopped for normal services, or on-failure: <count> to bound retries.
  • docker compose down -v deletes named volumes. If your data is in a named volume, this is a foot-gun — re-read the prompt before confirming.

Further Reading