Building a real application rarely means running a single process. A typical stack pairs a web service with a database, a cache, and often a message queue. Starting each one by hand with docker run, juggling ports, networks, and restart flags, works for a demo and turns painful the moment something else needs adding. Docker Compose solves this by describing your whole stack in one declarative file and managing it as a single unit.
Note: this guide covers Compose v2, the current plugin that ships with Docker and runs as
docker compose(with a space). Early tutorials showdocker-compose(with a hyphen), the legacy standalone binary — Docker deprecated v1 after June 2023 and has since dropped it from Desktop and CI images. Treat the hyphenated form as deprecated — you wantdocker compose.
The compose file
Create a docker-compose.yml (Compose also accepts compose.yaml) alongside your project. It uses the modern format, which opens directly with services: — no version: key needed (Compose v2 ignores it anyway):
services:
web:
image: nginx:alpine
ports:
- "8080:80"
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: app
POSTGRES_DB: app
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
Two services, one command. web publishes port 80 inside the container to 8080 on your host, while db keeps its credentials simple for local development and stores its data in a named volume so it survives containers being recreated.
The daily workflow
Start the stack in the background — detached, so it keeps running after your terminal closes:
docker compose up -d
This pulls what's needed, creates the network and volumes, and starts both services. When you change the file later, running it again applies only the changes to affected containers.
Check what's running:
docker compose ps
This lists each service's container, its state, and the ports it's mapped to. To watch what the app and database are actually doing:
docker compose logs -f
The -f flag follows the output live, streaming logs from all services into one terminal — the easiest way to see the whole stack at once. Add a service name to target one, e.g. docker compose logs -f web.
While things run, you can reach inside a container. To open a shell in the web container:
docker compose exec web sh
Use exec when the service is already running and you need to inspect state or run a quick command — it behaves like a normal process, not a new container. To talk directly to the database:
docker compose exec db psql -U app -d app
Tearing it down
When you're done, stop everything cleanly:
docker compose down
This stops the containers and removes the network Compose created, returning your machine to its pre-stack state. Your named volume (the data) is kept, so the next up finds your database exactly where you left it. If you ever want to start over and wipe the data too, docker compose down -v removes the volumes as well — use that one carefully.
Where to go next
Compose handles far more than two services: watch mode for live-reloading code, health checks to control startup order, and profiles to run different subsets of your stack. Master the five commands above first — they carry you through ninety percent of day-to-day work with a Compose project, whether it's a two-service prototype or the foundation of a larger production deployment.
