Most real applications are not just one container. A typical web app has:
- A web server (nginx)
- An API server (your app)
- A database (PostgreSQL)
- A cache (Redis)
Running four separate docker run commands, connecting them manually, and managing their configuration is painful. Docker Compose solves this.
What is Docker Compose?
Docker Compose lets you define your entire application stack in a single YAML file. Then you start everything with one command:
docker compose up
Stop everything with:
docker compose down
That is the core idea. One file, one command.
Docker Compose v2 (Important)
There are two versions of Docker Compose:
- docker-compose (v1) — the old Python-based CLI tool. Deprecated since 2021. Do not use it.
- docker compose (v2) — a Go plugin built into Docker Engine. This is what you should use.
The difference is a hyphen: docker-compose vs docker compose.
If you see tutorials that use docker-compose up, they are outdated. The v1 CLI is no longer maintained.
Docker Compose v2 ships with Docker Desktop automatically. On Linux, it is included when you install Docker Engine from Docker’s official repository. If you installed Docker a different way on Linux, you may need to install the Compose plugin separately.
Your First docker-compose.yml
Create a file called docker-compose.yml in your project root:
# docker-compose.yml — Compose Specification v5
services:
web:
image: nginx:1.27
ports:
- "8080:80"
api:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgres://alex:secret@db:5432/myapp
depends_on:
db:
condition: service_healthy
db:
image: postgres:17
environment:
POSTGRES_USER: alex
POSTGRES_PASSWORD: secret
POSTGRES_DB: myapp
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U alex -d myapp"]
interval: 10s
timeout: 5s
retries: 5
volumes:
db_data:
This defines three services: web, api, and db.
Core Compose Concepts
services
Each services entry is one container. You define its image (or build context), ports, environment variables, and dependencies.
image vs build
Use image: when you want to pull a pre-built image from a registry:
services:
db:
image: postgres:17
Use build: when you want Docker Compose to build an image from a Dockerfile:
services:
api:
build: . # looks for ./Dockerfile
Or with more options:
services:
api:
build:
context: .
dockerfile: Dockerfile.prod
ports
Maps host ports to container ports:
ports:
- "8080:80" # host:container
- "5432:5432"
environment
Set environment variables inside the container:
environment:
DATABASE_URL: postgres://alex:secret@db:5432/myapp
APP_ENV: development
Or use a list format:
environment:
- DATABASE_URL=postgres://alex:secret@db:5432/myapp
depends_on
Control startup order. This simple form just controls the order, not health:
depends_on:
- db
This better form waits until the database is healthy before starting the API:
depends_on:
db:
condition: service_healthy
For service_healthy to work, the db service must define a healthcheck.
volumes
Named volumes persist data between container restarts:
services:
db:
volumes:
- db_data:/var/lib/postgresql/data # named volume
volumes:
db_data: # declare the volume here
Bind mounts map a host directory into the container (useful in development):
services:
api:
volumes:
- ./src:/app/src # host path : container path
Using .env Files
Never hardcode passwords in docker-compose.yml. Use a .env file instead:
# .env
POSTGRES_USER=alex
POSTGRES_PASSWORD=s3cr3t!
POSTGRES_DB=myapp
Then reference variables in your Compose file:
services:
db:
image: postgres:17
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
Important: Add .env to your .gitignore. Never commit passwords to git.
echo ".env" >> .gitignore
Profiles: Start Optional Services
Compose profiles let you define services that only start when explicitly requested. Useful for debug tools, admin panels, or optional services:
services:
api:
build: .
ports:
- "3000:3000"
db:
image: postgres:17
pgadmin:
image: dpage/pgadmin4
profiles:
- debug
ports:
- "5050:80"
Start everything including debug services:
docker compose --profile debug up
Start only the default services (api + db, no pgadmin):
docker compose up
Watch Mode: Auto-sync Code Changes
Compose Watch Mode is one of the most useful features for development. It watches your files and automatically syncs changes into running containers — no manual restart needed.
Requires: Docker Compose 2.22 or newer.
Add a develop.watch block to your service:
services:
api:
build: .
develop:
watch:
- action: sync
path: ./src
target: /app/src
- action: rebuild
path: requirements.txt
There are four watch actions:
| Action | What it does | Best for |
|---|---|---|
sync | Copy changed files into the running container | Python, Node.js, Ruby (no compile step) |
rebuild | Rebuild the image and restart the container | Go, Rust, compiled languages |
sync+restart | Sync files then restart the container process | Config file changes |
restart | Restart the container without syncing (since Compose 2.32) | Environment variable changes |
Start with watch mode:
docker compose watch
Or combine with up:
docker compose up --watch
Now when you save a Python file, it is instantly copied into the container. No restart needed.
Common Commands
# Start all services (build if needed)
docker compose up
# Start in detached mode (background)
docker compose up -d
# Build images and start
docker compose up --build
# Stop all services (keep volumes)
docker compose down
# Stop and remove volumes
docker compose down -v
# View running services
docker compose ps
# View logs from all services
docker compose logs
# Follow logs
docker compose logs -f
# Follow logs from one service
docker compose logs -f api
# Run a command in a running service
docker compose exec api bash
# Scale a service to 3 replicas
docker compose up --scale api=3
Complete 3-Tier App Example
Create a .env file with your secrets before running this example:
DB_PASSWORD=s3cr3t!
Here is a complete example of a web app with nginx, a Python API, and PostgreSQL:
# docker-compose.yml
services:
web:
image: nginx:1.27
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- api
api:
build:
context: .
dockerfile: Dockerfile
environment:
DATABASE_URL: postgresql://alex:${DB_PASSWORD}@db:5432/myapp
APP_ENV: production
env_file:
- .env
depends_on:
db:
condition: service_healthy
develop:
watch:
- action: sync
path: ./app
target: /app/app
- action: rebuild
path: requirements.txt
db:
image: postgres:17
environment:
POSTGRES_USER: alex
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: myapp
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U alex -d myapp"]
interval: 10s
timeout: 5s
retries: 5
volumes:
db_data:
The nginx.conf:
server {
listen 80;
location / {
proxy_pass http://api:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Notice that nginx uses api as the hostname. Docker Compose automatically creates a network where each service is reachable by its service name.
Common Mistakes
Still using docker-compose (v1)
The old docker-compose CLI is deprecated and no longer receives updates. Always use docker compose (v2 plugin). The difference is a hyphen in the command name.
Not using health checks with depends_on
depends_on without condition: service_healthy only controls the startup order. It does not wait for the database to be ready to accept connections. Your app can start before the database is ready and fail. Always add a healthcheck to your database service and use condition: service_healthy.
Hardcoding passwords in docker-compose.yml
Never put passwords directly in your Compose file. Use .env files with ${VARIABLE} syntax, and make sure .env is in .gitignore.
What’s Next?
Next: Docker Tutorial #6: Docker Volumes and Networking Explained