Docker lets you package one service. Docker Compose lets you describe the whole local environment: application, database, cache, background worker, and any supporting tools. Instead of starting each container by hand, you keep the setup in one compose.yaml file and run it with one command.
This guide explains Docker Compose from the point of view of everyday development. You will create a small stack, learn the core fields, and see the commands you will use most often.
When Docker Compose helps
Compose is useful when your app needs more than one process:
a web application and PostgreSQL,
an API and Redis,
a worker and a queue,
local development tools such as Mailpit or MinIO,
test environments that need repeatable services.
The main benefit is consistency. New team members do not need to install PostgreSQL, Redis and other tools manually. They run the same stack as everyone else.
A minimal Compose file
Create compose.yaml in your project root:
services:
app:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://app:secret@db:5432/app
depends_on:
- db
db:
image: postgres:18
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: app
volumes:
- postgres-data:/var/lib/postgresql/data
volumes:
postgres-data:services:
app:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://app:secret@db:5432/app
depends_on:
- db
db:
image: postgres:18
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: app
volumes:
- postgres-data:/var/lib/postgresql/data
volumes:
postgres-data:Start the stack:
docker compose updocker compose upCompose creates a private network automatically. The app container can reach PostgreSQL using the service name db as the hostname.
Run in the background
During development you usually want services to keep running while the terminal is free.
docker compose up -d
docker compose ps
docker compose logs -f appdocker compose up -d
docker compose ps
docker compose logs -f appStop the stack without deleting data:
docker compose downdocker compose downStop it and remove named volumes when you want a clean database:
docker compose down -vdocker compose down -vBuild context and Dockerfile
The build: . line tells Compose to build an image from the Dockerfile in the current directory.
FROM node:22-alpine
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
CMD ["pnpm", "dev"]FROM node:22-alpine
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
CMD ["pnpm", "dev"]For production images you would usually split build and runtime stages. For a beginner local setup, clarity is more important than maximum optimization.
Environment variables
Inline environment values are fine for tutorials, but real projects should keep local secrets outside the Compose file.
services:
app:
env_file:
- .env.localservices:
app:
env_file:
- .env.localNever commit real production secrets. Use development-only credentials for local containers.
Volumes: why your database survives restarts
A container can be recreated at any time. A named volume stores data outside the container lifecycle.
volumes:
postgres-data:volumes:
postgres-data:Without the volume, rebuilding or removing the database container would remove the database files too.
Useful commands
These commands cover most daily work:
docker compose up -d # start services
docker compose ps # show status
docker compose logs -f db # follow logs
docker compose exec db psql -U app # open shell inside a service
docker compose restart app # restart one service
docker compose down # stop services
docker compose down -v # stop and remove volumesdocker compose up -d # start services
docker compose ps # show status
docker compose logs -f db # follow logs
docker compose exec db psql -U app # open shell inside a service
docker compose restart app # restart one service
docker compose down # stop services
docker compose down -v # stop and remove volumesCommon mistakes
The most common issue is using localhost from inside a container. Inside the app container, localhost means the app container itself, not your host machine and not the database container. Use the Compose service name, for example db.
Another common issue is changing the Dockerfile but forgetting to rebuild:
docker compose up --builddocker compose up --buildCompose is not complicated once you remember the model: services run containers, service names become hostnames, volumes keep data, and one file documents how the local stack fits together.