Docker Compose is a tool for defining and running a multi-container Docker application from a YAML file. Instead of typing several long docker run commands, you describe the application once in compose.yaml, then manage it with docker compose.

Compose is most useful when an application has more than one container: an API, database, cache, worker, queue, or reverse proxy.

Why Compose Exists

Without Compose, a small app quickly becomes several commands:

docker network create app-net
docker volume create db-data
docker run -d --name db --network app-net -e POSTGRES_PASSWORD=local_demo_password -v db-data:/var/lib/postgresql/data postgres:16
docker run -d --name api --network app-net -p 8080:3000 my-api:local

With Compose, those runtime choices live in one file:

docker compose up -d

Compose helps with:

ProblemHow Compose Helps
Many long commandsStore service config in compose.yaml
Multiple containersStart/stop the app as one project
Local networkingServices can reach each other by service name
Persistent dataDeclare named volumes in the file
Repeatable setupNew team members run the same app definition
DebuggingLogs, exec, ps, rebuild, and restart are service-aware

Key Insight: Compose is not a replacement for images, containers, networks, or volumes. It is a higher-level way to declare and manage them together.

Mental Model

flowchart TD
    File["compose.yaml"] --> CLI["docker compose"]
    CLI --> Project["Compose project"]
    Project --> Services["Services"]
    Project --> Networks["Networks"]
    Project --> Volumes["Volumes"]
    Services --> Containers["Containers"]
    Services --> Images["Images or builds"]

Read it like this:

  1. You write a compose.yaml.
  2. docker compose reads that file.
  3. Compose creates a project.
  4. The project contains services, networks, and volumes.
  5. Each service runs one or more containers from an image or build definition.

File Name and Location

The preferred file name is:

compose.yaml

Docker also supports compose.yml, docker-compose.yaml, and docker-compose.yml for compatibility. For new notes and projects, use compose.yaml.

Typical project:

my-app/
  compose.yaml
  Dockerfile
  .dockerignore
  .env
  src/

Use the modern command:

docker compose up

You may still see the old standalone command:

docker-compose up

For current Docker installs, prefer docker compose with a space.

Compose File Structure

A beginner-friendly Compose file usually has these top-level sections:

name: my-app
 
services:
  api:
    build: .
    ports:
      - "8080:3000"
    environment:
      NODE_ENV: development
      DB_HOST: db
    depends_on:
      - db
 
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: local_demo_password
      POSTGRES_DB: app_db
    volumes:
      - db-data:/var/lib/postgresql/data
 
volumes:
  db-data:

Read the shape first:

SectionMeaning
nameProject name used to group resources
servicesContainers your app needs
volumesNamed persistent storage
networksOptional explicit networks
configsNon-sensitive config mounted into services
secretsSensitive config mounted into services where supported

Note: Older Compose examples often include a top-level version field. For new Compose files, leave it out. Current Compose uses the Compose Specification and treats version as obsolete.

The password value above is only a local demo value. Do not commit real passwords into Compose files. Use .env, local secret files, Docker secrets, or your platform’s secret mechanism.

Services

A service describes how to run one type of container.

services:
  api:
    build: .
    ports:
      - "8080:3000"

The service name is api. Compose uses that name for:

  • the service identity
  • DNS inside the Compose network
  • command targeting, such as docker compose logs api
  • scaling, such as docker compose up --scale api=3

Common service fields:

FieldUse
imageUse an existing image
buildBuild an image from a Dockerfile
portsPublish container ports to the host
environmentSet runtime environment variables
env_fileLoad environment variables from a file
volumesMount volumes or host paths
networksAttach service to specific networks
depends_onExpress startup dependency between services
commandOverride the image’s default command
restartConfigure restart behavior for local/runtime use

image vs build

Use image when the service should run an image that already exists locally or in a registry:

services:
  db:
    image: postgres:16

Use build when Compose should build the image from your project:

services:
  api:
    build: .

You can also tag the built image:

services:
  api:
    build: .
    image: my-api:local

This is useful when you want both:

  • local rebuilds from a Dockerfile
  • a predictable image name for inspection or push workflows

Ports

ports publishes a container port to the host.

services:
  api:
    ports:
      - "8080:3000"

Read it like this:

host port 8080 -> container port 3000

This is the Compose version of:

docker run -p 8080:3000 ...

Use ports when you need to reach a service from your browser, host machine, or another external client.

Environment Variables

Use environment for simple runtime settings:

services:
  api:
    environment:
      NODE_ENV: development
      DB_HOST: db
      DB_PORT: "5432"

Use env_file when the list grows:

services:
  api:
    env_file:
      - .env

Example .env for local development:

NODE_ENV=development
DB_HOST=db
DB_PORT=5432

Warning: Treat .env carefully. It is convenient for local development, but real secrets should not be committed to source control.

Volumes

Use a named volume for persistent application data:

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: local_demo_password
    volumes:
      - db-data:/var/lib/postgresql/data
 
volumes:
  db-data:

This means:

db-data volume -> /var/lib/postgresql/data inside db container

Use a bind mount for local source code during development:

services:
  api:
    build: .
    volumes:
      - ./src:/app/src

The rule stays the same as normal Docker:

NeedUse
Container-owned persistent dataNamed volume
Host project files inside containerBind mount
Temporary in-memory datatmpfs

See Persist Data in Docker for the storage model.

Networks

Compose creates a default network for the project. Services on that network can reach each other by service name.

services:
  api:
    build: .
    environment:
      DB_HOST: db
 
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: local_demo_password

In this example, the api container can connect to the database using the hostname:

db

You do not need to publish the database port to the host for api to reach it. ports is only needed when something outside the Compose network needs access.

You can define explicit networks when you want clearer separation:

services:
  api:
    build: .
    networks:
      - app-net
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: local_demo_password
    networks:
      - app-net
 
networks:
  app-net:

See Docker Networking for the bridge-network mental model.

depends_on

depends_on tells Compose that one service should be started before another.

services:
  api:
    build: .
    depends_on:
      - db
 
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: local_demo_password

This is useful, but do not overread it. Starting a database container first does not always mean the database is ready to accept connections. Real applications should still handle retries, or use health checks when startup readiness matters.

Project Name

Compose groups resources under a project name.

name: my-app

Resource names are derived from the project name, service name, and instance number. For example:

my-app-api-1
my-app-db-1
my-app_default
my-app_db-data

You can override the project name from the CLI:

docker compose -p my-test up -d

Project names let you run separate copies of the same Compose app without changing the file.

Common Commands

Run these from the folder that contains compose.yaml.

CommandUse
docker compose upCreate and start services; attach logs
docker compose up -dStart services in the background
docker compose downStop and remove service containers and networks
docker compose psShow service container status
docker compose logsShow logs for all services
docker compose logs -f apiFollow logs for one service
docker compose exec api shRun a shell inside a running service container
docker compose run --rm api shRun a one-off container for a service
docker compose buildBuild service images
docker compose pullPull service images
docker compose restart apiRestart one service
docker compose configRender and validate the final Compose config

up

docker compose up
docker compose up -d
docker compose up --build

up creates and starts the application. If service configuration changes, Compose may recreate affected containers while preserving mounted volumes.

Use:

CommandUse
docker compose upForeground mode; useful when watching logs
docker compose up -dDetached mode; normal background run
docker compose up --buildRebuild images before starting

down

docker compose down
docker compose down -v

down stops the project and removes service containers and networks created by up.

By default, down does not remove named volumes. With -v, it removes named volumes declared in the Compose file and anonymous volumes attached to containers.

Warning: Be careful with docker compose down -v. It can remove database data stored in Compose volumes.

exec vs run

Use exec to run a command inside an already running service container:

docker compose exec api sh

Use run for a one-off service container:

docker compose run --rm api npm test
CommandTarget
docker compose exec api shExisting running api container
docker compose run --rm api npm testNew one-off api container

Compose vs Docker Run

Taskdocker runCompose
One temporary containerExcellentUsually unnecessary
Multi-container appVerboseStrong fit
Repeatable local setupManual scripts or docscompose.yaml
Service-to-service DNSNeed user-defined networkBuilt into project network
Named volumesCommand flagsDeclared in file
Team workflowEasy to driftShared file

Use docker run while learning a single container. Use Compose when the runtime setup becomes part of the application.

Common Mistakes

MistakeWhy It HurtsBetter Habit
Adding version: "3" to new filesCurrent Compose treats version as obsoleteOmit version
Publishing every internal portExposes services unnecessarilyPublish only host-facing services
Hardcoding real secretsSecrets leak through files/historyUse secret handling for real apps
Assuming depends_on means readyService may be started but not readyAdd retries or health checks
Running down -v casuallyCan delete named-volume dataInspect volumes before deletion
Mixing too much into one fileLocal, test, and prod needs differUse override files or profiles deliberately

TL;DR

  • Docker Compose defines a multi-container application in compose.yaml.
  • Use services for containers, volumes for persistent storage, and networks for communication.
  • Prefer the modern docker compose command, not old docker-compose.
  • Omit the old top-level version field in new Compose files.
  • docker compose up -d starts the app in the background.
  • docker compose down stops/removes containers and networks, but not named volumes by default.
  • Use docker compose config to validate the final resolved file.

Resources

Docker Compose overview Official Docker Compose documentation hub.

How Compose works Official explanation of the Compose file, services, networks, volumes, projects, and key commands.

Compose file reference Official reference for Compose file structure.

Version and name top-level elements Official note that version is obsolete and name defines the project name.

docker compose CLI reference Official reference for Compose commands and global options.