Docker Compose: Running Multi-Container Applications
A real app rarely has just one container. It's usually web + database + cache, one container each. Running each docker run by hand with all its -p, -v, --network, -e... flags is tedious and error-prone. Docker Compose solves it: declare everything in one file, run it with a single command.
What Compose is
Compose lets you describe an app's containers (called services), networks, and volumes in one YAML file (compose.yaml). Then docker compose up brings everything up; docker compose down tears everything down. The whole configuration lives in the file, can go into Git, and reproduces identically on another machine.
Compose is now an integrated subcommand:
docker compose(with a space). The old version isdocker-compose(with a hyphen) running as a separate binary — if you see older docs using the hyphenated form, understand it's the same tool.
A minimal compose file
Create compose.yaml for a web server and a Redis cache:
services:
web:
image: nginx:alpine
ports:
- "8088:80"
depends_on:
- cache
cache:
image: redis:alpine
Reading the file: two services, web and cache. web uses the nginx image, publishes port 8088→80, and depends_on: cache (start cache first). cache uses the redis image.
Run both:
docker compose up -d
Container ..._cache-1 Started
Container ..._web-1 Started
Check status:
docker compose ps
SERVICE STATUS
cache Up
web Up
curl http://localhost:8088 returns the nginx page (HTTP 200). One command, and both containers are running with the right configuration.
The best part: services call each other by name
Compose auto-creates a user-defined network for the app (recall Article 7: a user-defined bridge has internal DNS). Every service in the same compose file sits on that network and calls the others by service name.
docker compose exec web ping -c 1 cache
PING cache (172.20.0.2): 56 data bytes
64 bytes from 172.20.0.2: seq=0 ttl=64 time=0.160 ms
web can reach cache just by the name cache — no need to know the IP. This is the most practical reason to use Compose: in web's code you configure the Redis host as cache, the database host as db... exactly the service names. No chasing IPs, no obsolete --link.
Compose project
┌──────────────────────────────────────────┐
│ private network (created by Compose) │
│ │
│ [web] ──calls "cache"──► [cache] │
│ │ │
│ :8088 (published to host) │
└──────────────────────────────────────────┘
A fuller compose file
A real example: your app (built from the Dockerfile in Article 5) + PostgreSQL with a volume holding data.
services:
app:
build: . # build from the Dockerfile in the current directory
ports:
- "3000:3000"
environment:
DATABASE_URL: "postgres://postgres:secret@db:5432/appdb"
depends_on:
- db
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: secret
POSTGRES_DB: appdb
volumes:
- pgdata:/var/lib/postgresql/data # volume holding data (Article 6)
volumes:
pgdata:
A few points:
build: .instead ofimage:— Compose builds the image from the Dockerfile itself. Useimage:to pull a ready-made image,build:to build your own.environmentpasses environment variables into the container. NoteDATABASE_URLpoints to the hostdb— the exact service name, thanks to internal DNS.volumesat the service level mounts a volume; thevolumes:block at the bottom declares the named volumepgdata(Article 6).depends_oncontrols startup order, but note: it only waits for the other container to start, not for the service inside to be ready to accept connections. The database may still be starting up while the app is already running. A real app should retry the connection itself, or use a healthcheck (condition: service_healthy).
Common Compose commands
docker compose up -d # bring up and run all services in the background
docker compose up -d --build # rebuild the images before running
docker compose ps # status of the services
docker compose logs -f # follow the combined logs of all services
docker compose logs -f app # logs of one service
docker compose exec app sh # open a shell in the app service
docker compose down # stop and remove containers + network
docker compose down -v # also remove the volumes (data loss!)
🧹 Cleanup
Compose cleans up neatly in a single command — that's another of its conveniences:
docker compose down
This command removes the containers and network that Compose created. Volumes are kept by default (so you don't lose data); add -v if you want to remove volumes too:
docker compose down -v
Wrap-up
Docker Compose describes a whole multi-container app in one YAML file and manages it with up/down. It auto-creates a private network so services call each other by name — neatly solving the wiring that Article 7 had to do by hand. Compose is the standard tool for running a multi-component app on a single machine, both during development and on a small server.
Our image runs fine so far but may still be large and unoptimized. Article 9 covers how to make images smaller and safer with multi-stage builds and a few best practices — before we step into Docker Swarm to run across multiple machines.