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:localWith Compose, those runtime choices live in one file:
docker compose up -dCompose helps with:
| Problem | How Compose Helps |
|---|---|
| Many long commands | Store service config in compose.yaml |
| Multiple containers | Start/stop the app as one project |
| Local networking | Services can reach each other by service name |
| Persistent data | Declare named volumes in the file |
| Repeatable setup | New team members run the same app definition |
| Debugging | Logs, 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:
- You write a
compose.yaml. docker composereads that file.- Compose creates a project.
- The project contains services, networks, and volumes.
- Each service runs one or more containers from an image or build definition.
File Name and Location
The preferred file name is:
compose.yamlDocker 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 upYou may still see the old standalone command:
docker-compose upFor 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:
| Section | Meaning |
|---|---|
name | Project name used to group resources |
services | Containers your app needs |
volumes | Named persistent storage |
networks | Optional explicit networks |
configs | Non-sensitive config mounted into services |
secrets | Sensitive config mounted into services where supported |
Note: Older Compose examples often include a top-level
versionfield. For new Compose files, leave it out. Current Compose uses the Compose Specification and treatsversionas 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:
| Field | Use |
|---|---|
image | Use an existing image |
build | Build an image from a Dockerfile |
ports | Publish container ports to the host |
environment | Set runtime environment variables |
env_file | Load environment variables from a file |
volumes | Mount volumes or host paths |
networks | Attach service to specific networks |
depends_on | Express startup dependency between services |
command | Override the image’s default command |
restart | Configure 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:16Use 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:localThis 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 3000This 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:
- .envExample .env for local development:
NODE_ENV=development
DB_HOST=db
DB_PORT=5432Warning: Treat
.envcarefully. 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 containerUse a bind mount for local source code during development:
services:
api:
build: .
volumes:
- ./src:/app/srcThe rule stays the same as normal Docker:
| Need | Use |
|---|---|
| Container-owned persistent data | Named volume |
| Host project files inside container | Bind mount |
| Temporary in-memory data | tmpfs |
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_passwordIn this example, the api container can connect to the database using the hostname:
dbYou 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_passwordThis 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-appResource 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-dataYou can override the project name from the CLI:
docker compose -p my-test up -dProject 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.
| Command | Use |
|---|---|
docker compose up | Create and start services; attach logs |
docker compose up -d | Start services in the background |
docker compose down | Stop and remove service containers and networks |
docker compose ps | Show service container status |
docker compose logs | Show logs for all services |
docker compose logs -f api | Follow logs for one service |
docker compose exec api sh | Run a shell inside a running service container |
docker compose run --rm api sh | Run a one-off container for a service |
docker compose build | Build service images |
docker compose pull | Pull service images |
docker compose restart api | Restart one service |
docker compose config | Render and validate the final Compose config |
up
docker compose up
docker compose up -d
docker compose up --buildup creates and starts the application. If service configuration changes, Compose may recreate affected containers while preserving mounted volumes.
Use:
| Command | Use |
|---|---|
docker compose up | Foreground mode; useful when watching logs |
docker compose up -d | Detached mode; normal background run |
docker compose up --build | Rebuild images before starting |
down
docker compose down
docker compose down -vdown 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 shUse run for a one-off service container:
docker compose run --rm api npm test| Command | Target |
|---|---|
docker compose exec api sh | Existing running api container |
docker compose run --rm api npm test | New one-off api container |
Compose vs Docker Run
| Task | docker run | Compose |
|---|---|---|
| One temporary container | Excellent | Usually unnecessary |
| Multi-container app | Verbose | Strong fit |
| Repeatable local setup | Manual scripts or docs | compose.yaml |
| Service-to-service DNS | Need user-defined network | Built into project network |
| Named volumes | Command flags | Declared in file |
| Team workflow | Easy to drift | Shared file |
Use docker run while learning a single container. Use Compose when the runtime setup becomes part of the application.
Common Mistakes
| Mistake | Why It Hurts | Better Habit |
|---|---|---|
Adding version: "3" to new files | Current Compose treats version as obsolete | Omit version |
| Publishing every internal port | Exposes services unnecessarily | Publish only host-facing services |
| Hardcoding real secrets | Secrets leak through files/history | Use secret handling for real apps |
Assuming depends_on means ready | Service may be started but not ready | Add retries or health checks |
Running down -v casually | Can delete named-volume data | Inspect volumes before deletion |
| Mixing too much into one file | Local, test, and prod needs differ | Use override files or profiles deliberately |
TL;DR
- Docker Compose defines a multi-container application in
compose.yaml. - Use
servicesfor containers,volumesfor persistent storage, andnetworksfor communication. - Prefer the modern
docker composecommand, not olddocker-compose. - Omit the old top-level
versionfield in new Compose files. docker compose up -dstarts the app in the background.docker compose downstops/removes containers and networks, but not named volumes by default.- Use
docker compose configto 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
versionis obsolete andnamedefines the project name.docker compose CLI reference Official reference for Compose commands and global options.