Docker Compose
Define and run multi-container applications with a compose.yaml file

Pixl presents
Starting five containers by hand, one terminal each? Please stop. One compose file, one command, whole stack.Starting five containers by hand, one terminal each? Please stop. One compose file, one command, whole stack.

Last lesson you stood up a web app, a database and a cache with a pile of docker run commands. It works... but it's tedious, and one typo means you're starting over. So what if you could describe the whole multi-container app in one file and start everything with one command? That's EXACTLY what Docker Compose does.
What is Docker Compose?#
Docker Compose is a tool for defining and running apps made of several containers. You describe all your services, networks and volumes in one YAML file, then run the whole thing with a couple of simple commands. The current docs call that file compose.yaml, but plenty of projects still use the older docker-compose.yml name and Compose happily reads either one.
# Start all services defined in your compose file
docker compose up
# Stop everything
docker compose downYep, that's it. One command starts your whole app stack.
Your First compose.yaml#
Here's a simple example with a web app and a PostgreSQL database.
services:
web:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://postgres:secret@db:5432/myapp
depends_on:
- db
db:
image: postgres:16
environment:
- POSTGRES_PASSWORD=secret
- POSTGRES_DB=myapp
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:Let's pick it apart piece by piece.
Services: Your Containers#
The services section lists every container your app needs. Each service gets a name (like web or db), and the other services can use that name as a hostname.
services:
web:
build: . # Build from Dockerfile in current directory
ports:
- "3000:3000" # Map port 3000
api:
image: myapp-api:latest # Use a pre-built image
ports:
- "8080:8080"Ports: Expose Services#
The ports section maps ports on your machine to ports in the container, same deal as the -p flag.
services:
web:
ports:
- "3000:3000" # host:container
- "9229:9229" # You can map multiple ports (e.g., for debugging)Environment Variables#
Here's how you set environment variables for your containers.
services:
web:
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://postgres:secret@db:5432/myapp
- REDIS_URL=redis://cache:6379Or you can use an .env file and keep your secrets out of the compose file altogether.
services:
web:
env_file:
- .envVolumes: Persist Data#
Set up volumes so your data sticks around.
services:
db:
image: postgres:16
volumes:
- pgdata:/var/lib/postgresql/data # Named volume
web:
build: .
volumes:
- ./src:/app/src # Bind mount for development
# Declare named volumes at the bottom
volumes:
pgdata:depends_on: Control Startup Order#

depends_on makes sure your services start in the right order.
services:
web:
build: .
depends_on:
- db
- cache
# web starts AFTER db and cache are running
db:
image: postgres:16
environment:
- POSTGRES_PASSWORD=secret
cache:
image: redis:7services:
web:
build: .
depends_on:
db:
condition: service_healthy
db:
image: postgres:16
environment:
- POSTGRES_PASSWORD=secret
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5Networks: Automatic Communication#
Docker Compose builds a network for your services automatically. Every service can reach every other one using its service name as a hostname, so most of the time you don't have to define networks at all.
services:
web:
environment:
# "db" resolves to the db service automatically
- DATABASE_URL=postgresql://postgres:secret@db:5432/myapp
# "cache" resolves to the cache service
- REDIS_URL=redis://cache:6379
db:
image: postgres:16
cache:
image: redis:7A Complete Real-World Example#
Here's a full-stack app with a Next.js frontend, an Express API, a PostgreSQL database and a Redis cache.
services:
frontend:
build: ./frontend
ports:
- "3000:3000"
environment:
- API_URL=http://api:8080
depends_on:
- api
api:
build: ./api
ports:
- "8080:8080"
environment:
- DATABASE_URL=postgresql://postgres:secret@db:5432/myapp
- REDIS_URL=redis://cache:6379
depends_on:
- db
- cache
volumes:
- ./api/src:/app/src # Live reload in development
db:
image: postgres:16
environment:
- POSTGRES_PASSWORD=secret
- POSTGRES_DB=myapp
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- "5432:5432" # Expose for local database tools
cache:
image: redis:7
ports:
- "6379:6379"
volumes:
pgdata:Essential Docker Compose Commands#
# Start all services (with live output)
docker compose up
# Start in the background
docker compose up -d
# Stop and remove all containers
docker compose down
# Stop and remove containers + volumes (deletes data!)
docker compose down -v
# Rebuild images before starting
docker compose up --build
# View logs for all services
docker compose logs
# View logs for one service
docker compose logs api
# Follow logs in real-time
docker compose logs -f
# List running services
docker compose ps
# Open a shell inside a running service
docker compose exec api sh
# Restart a specific service
docker compose restart apiDevelopment vs Production#
You can use separate compose files for separate environments.
# Development (default)
docker compose up
# Production (with override file)
docker compose -f compose.yaml -f compose.prod.yaml up -dA production override file might strip out the bind mounts, add resource limits and swap some environment variables.
TL;DR#
- Docker Compose describes multi-container apps in one
compose.yamlfile (older projects call itdocker-compose.yml) - services lists each container (web, db, cache, etc.)
- ports maps ports on your machine to ports in the container
- environment sets environment variables (use
env_filefor secrets) - volumes keeps data around and syncs your code live during development
- depends_on controls the order services start in
- Services find each other by name automatically (no IP addresses needed)
docker compose upstarts everything, anddocker compose downstops everything
What's Next?#
Your Docker setup is humming along now, but your images are probably bigger and slower to build than they need to be. Next lesson gets into slimming them down with tricks like multi-stage builds, .dockerignore and smarter layer caching, so your images shrink and your builds speed up...
This lesson ends with a short activity.
