Docker Compose · guide
Docker Compose Guide
Compose defines and runs multi-container applications from a YAML file. One command builds, creates and starts every service. See the official Compose docs.
Last updated
On this page · 13 sections
1. What Is Docker Compose
Docker Compose lets you declare your entire application stack — services, networks, and volumes — in a single docker-compose.yml file. Instead of running multiple docker run commands, you run docker compose up.
Compose V2/V5 vs V1: Current Compose is invoked as docker compose (a Docker CLI plugin). Docker's supported Compose CLIs are v2 and v5 — v5, released in 2025, is functionally identical to v2, so docker compose version may report either. The old V1 was a standalone Python binary called docker-compose (with a hyphen) and is no longer supported. All examples in this guide use the docker compose syntax.
2. Install / Verify
Compose V2 is included with Docker Desktop. On Linux, it's installed as a plugin with the Docker Engine packages.
# Check Compose is available
docker compose version
# If missing on Linux, install the plugin
sudo apt-get install -y docker-compose-plugin
3. Your First Compose File
Create a file named docker-compose.yml in your project directory. (compose.yaml is now the preferred name; Compose still reads docker-compose.yml for backwards compatibility and uses compose.yaml if both exist.) This example runs a Node.js app with a PostgreSQL database.
services:
app:
image: node:24-alpine
working_dir: /app
volumes:
- .:/app
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://app:secret@db:5432/appdb
command: npm run dev
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: appdb
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 5s
timeout: 3s
retries: 5
volumes:
pgdata:
# Start everything
docker compose up -d
# Check it's running
docker compose ps
4. Services, Ports, and Environment Variables
Each top-level key under services: is a service. A service defines a container's image, ports, environment, and behavior.
services:
web:
image: nginx:alpine
ports:
- "8080:80" # host:container
- "443:443"
environment:
- NGINX_HOST=example.com
- NGINX_PORT=80
api:
image: my-api:latest
ports:
- "4000:4000"
environment:
NODE_ENV: production
SECRET_KEY: my-secret
Port syntax is "HOST_PORT:CONTAINER_PORT". If you only specify one number ("3000"), Docker picks a random host port. Without a host IP the port is published on every interface (0.0.0.0), and Docker's rules bypass host firewalls such as ufw — use "127.0.0.1:3000:3000" for anything that should only be reachable from the host itself.
5. Volumes in Compose
Volumes persist data and share files between the host and containers. Compose supports both named volumes (managed by Docker) and bind mounts (direct host paths).
services:
db:
image: postgres:16-alpine
volumes:
# Named volume — Docker manages storage location
- pgdata:/var/lib/postgresql/data
# Bind mount — maps host directory into container
- ./init-scripts:/docker-entrypoint-initdb.d
app:
image: my-app:latest
volumes:
# Bind mount for development (live reload)
- ./src:/app/src
# Anonymous volume to prevent overwriting node_modules
- /app/node_modules
# Declare named volumes at the top level
volumes:
pgdata:
# List volumes created by compose
docker volume ls
# Remove volumes when tearing down (deletes data!)
docker compose down -v
6. Networking in Compose
Compose creates a default network for your project. All services can reach each other by service name as the hostname.
services:
app:
image: my-app:latest
networks:
- frontend
- backend
db:
image: postgres:16-alpine
networks:
- backend
nginx:
image: nginx:alpine
networks:
- frontend
networks:
frontend:
backend:
In this setup, app can reach both db and nginx, but nginx cannot reach db directly — they are on separate networks.
# Inspect the default network compose created
docker network ls
docker network inspect <project>_default
Service names act as DNS hostnames. From inside the app container, db resolves to the database container's IP.
7. depends_on and Health Checks
depends_on controls startup order. By default it only waits for the dependency to start, not to be ready. Combine with health checks to wait for actual readiness.
services:
app:
image: my-app:latest
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: example # required by the postgres image
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 10
start_period: 10s
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
The app service will wait until db passes its health check and redis has started. start_period gives the service grace time before health checks count as failures.
8. Build Context
Instead of pulling a pre-built image, Compose can build images from a Dockerfile in your project.
services:
api:
build:
context: .
dockerfile: Dockerfile
args:
NODE_ENV: production
ports:
- "4000:4000"
frontend:
build:
context: ./frontend
dockerfile: Dockerfile.prod
ports:
- "3000:80"
# Build (or rebuild) images
docker compose build
# Build and start in one command
docker compose up -d --build
# Build without cache
docker compose build --no-cache
If you specify both build: and image:, Compose first tries to pull image and builds only if it can't be found (or when you pass --build); a built image is tagged with the image name. Set pull_policy: build to always build instead.
9. Environment Variables and .env Files
There are several ways to pass environment variables to services.
Inline in the compose file (fine for ordinary settings — for passwords and API keys prefer Compose secrets:, which mounts them as files under /run/secrets/ instead of exposing them as environment variables):
services:
app:
environment:
NODE_ENV: production
API_KEY: abc123
From a .env file loaded into the service:
services:
app:
env_file:
- .env
- .env.local
The project .env file — Compose automatically reads a .env file in the same directory as your compose file. Variables there can be referenced with ${VAR} syntax:
# .env
POSTGRES_PASSWORD=supersecret
APP_PORT=3000
services:
app:
ports:
- "${APP_PORT}:3000"
db:
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
# Verify variable substitution
docker compose config
10. Override Files
Compose automatically merges docker-compose.yml with docker-compose.override.yml if both exist. This is perfect for dev-specific settings without modifying the base file.
docker-compose.yml (base — shared across environments):
services:
app:
image: my-app:latest
ports:
- "3000:3000"
db:
image: postgres:16-alpine
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
docker-compose.override.yml (dev overrides — auto-loaded):
services:
app:
build: .
volumes:
- .:/app
environment:
NODE_ENV: development
# Uses base + override automatically
docker compose up -d
# Use a specific override file
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
# Preview the merged config
docker compose -f docker-compose.yml -f docker-compose.prod.yml config
11. Resource Limits
Without limits, one service in a stack can consume the whole host and take
its neighbours down with it. Compose expresses limits under deploy.
services:
app:
image: node:24-alpine
deploy:
resources:
limits:
# A ceiling. The container is killed if it exceeds memory.
cpus: "1.5"
memory: 512M
reservations:
# Outside Swarm there is no scheduler: memory becomes a soft limit
# (--memory-reservation) and cpus is ignored. Swarm uses both for placement.
cpus: "0.25"
memory: 128M
⚠️ The deploy key began as a Swarm-only feature, which is why a good deal of
older material says plain docker compose up ignores it. Support has changed
over releases, so rather than trusting either claim, confirm it on the version
you are running:
# What Compose actually resolved, after merges and variable substitution
docker compose config
# What the container really got — 0 means no limit was applied
docker inspect <container> --format '{{.HostConfig.Memory}} {{.HostConfig.NanoCpus}}'
# And live usage against whatever limit is in force
docker stats --no-stream
💡 A container killed for exceeding its memory limit exits with code 137. See
the Docker guide's resource-limits section for reading OOMKilled alongside
that code — a 137 does not on its own prove the limit was the cause.
12. Production Patterns
When running Compose in production, add restart policies, resource limits, and logging configuration.
services:
app:
image: my-app:1.2.3
restart: unless-stopped
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
reservations:
cpus: "0.25"
memory: 128M
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 5s
retries: 3
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
# The image will not start without it; Compose stops here if .env does not set it
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
volumes:
- pgdata:/var/lib/postgresql/data
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
volumes:
pgdata:
Key production tips:
- Always pin image tags (
my-app:1.2.3, not:latest). - Use
restart: unless-stoppedso services survive reboots. - Set resource limits to prevent one service from starving others.
- Configure log rotation to prevent disks from filling up.
Keep the database off the public network
A Compose file with one network puts every service on it, so a database that should only ever be reached by the application is reachable by anything else on that network — and, if you published its port, from outside the host.
Two networks fix it. The database joins only the internal one, which Compose will not attach to the host:
services:
app:
networks: [frontend, backend]
ports:
- "3000:3000"
db:
image: postgres:16-alpine
networks: [backend] # no ports: — nothing outside the compose network reaches it
networks:
frontend:
backend:
internal: true # no route to or from the host network
The check that matters is the absence of a ports: entry on db. Publishing a
database port to debug something and leaving it published is an easy way for a
development convenience to become an exposure.
13. Useful Commands
# Start all services in the background
docker compose up -d
# Stop and remove containers, networks
docker compose down
# Stop and also remove volumes (deletes data!)
docker compose down -v
# View logs (all services)
docker compose logs -f
# View logs for a specific service
docker compose logs -f app
# List running services
docker compose ps
# Execute a command in a running service
docker compose exec app sh
# Run a one-off command (starts a new container)
docker compose run --rm app npm test
# Pull latest images
docker compose pull
# Rebuild images
docker compose build
# Restart a single service
docker compose restart app
# Validate and view the merged config
docker compose config
# Scale a service (if no host port conflicts)
docker compose up -d --scale worker=3
Terms used here (1)plain definitions
Short definitions for the vocabulary on this page, in case any of it is new. Each links to the full entry.
- Docker Compose
- A tool for defining and running multi-container Docker applications using a YAML configuration file. With a single command you can spin up all the services your app needs — databases, caches, workers — in an isolated environment.
Where to go next
ComposeBot
Answers from the Docker Compose guide