Docker Compose Basics: Run Multi-Container Apps with One Command

In the previous post, you learned how to write a Dockerfile and build your own images. But real apps don't run in isolation — they need a database, maybe a cache, maybe a background worker. Running each one with a separate docker run command gets unwieldy fast.
That's where Docker Compose comes in.
What is Docker Compose?
Docker Compose is a tool for defining and running multi-container Docker applications. Instead of typing long docker run commands for each service, you declare everything in a single docker-compose.yml file — and spin up the entire stack with one command:
docker compose upThat's it. App, database, cache — all running together.
Docker Compose is especially well-suited for development and testing environments. Commit the docker-compose.yml to your repo and every teammate can get the full stack running without installing PostgreSQL, Redis, or anything else on their machine.
The docker-compose.yml Structure
Here's what a typical Compose file looks like:
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=development
depends_on:
- db
volumes:
- .:/app
db:
image: postgres:16
ports:
- "5432:5432"
environment:
POSTGRES_USER: admin
POSTGRES_PASSWORD: secret
POSTGRES_DB: mydb
volumes:
- db_data:/var/lib/postgresql/data
volumes:
db_data:Two services: app (your Node.js application) and db (PostgreSQL). One named volume to persist database data. Let's break down each field.
The Key Fields
services
The top-level block that lists all the containers you want to run. Each key under services is the container's name — this name also becomes its hostname on the internal Docker network.
image
Use a pre-built image from Docker Hub (or local registry) instead of building from source.
image: postgres:16
image: redis:alpine
image: nginx:latestbuild
Tells Docker to build an image from a Dockerfile rather than pulling one. Points to the directory containing the Dockerfile.
build: . # Use Dockerfile in current directory
build: ./backend # Use Dockerfile in the backend/ directoryports
Maps container ports to your host machine. Format: "host:container".
ports:
- "3000:3000" # localhost:3000 → container port 3000
- "5432:5432"environment
Sets environment variables inside the container. Supports both list and map formats:
# List format
environment:
- NODE_ENV=production
- PORT=3000
# Map format
environment:
POSTGRES_USER: admin
POSTGRES_PASSWORD: secretvolumes
Two types of volume mounts:
volumes:
- .:/app # Bind mount — sync local code into container
- db_data:/var/lib/postgresql/data # Named volume — persist data across container restartsBind mounts (.:/app) link a host directory directly into the container. Change a file locally — the container sees it immediately. No rebuild needed. Perfect for development.
Named volumes (db_data:...) are managed by Docker and persist even when the container is removed. Essential for database data you don't want to lose.
depends_on
Controls startup order — this service starts after the listed services.
depends_on:
- db
- redisNote: depends_on only waits for the container to start, not for the service inside to be ready. For databases, the app might start before PostgreSQL finishes initializing. For production-grade setups, use health checks or a retry loop in your app.
Essential Commands
# Start the entire stack in the background
docker compose up -d
# View logs for all services
docker compose logs -f
# View logs for a specific service
docker compose logs -f app
# Stop all containers
docker compose down
# Stop and remove everything: containers, volumes, images
docker compose down -v --rmi all
# Rebuild images then start (use after changing Dockerfile)
docker compose up -d --build
# Run a command inside a running container
docker compose exec app sh
# Check status of all services
docker compose psReal Example: Node.js + PostgreSQL
Here's a complete setup for a Node.js app connected to PostgreSQL:
services:
app:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgres://admin:secret@db:5432/mydb
depends_on:
- db
volumes:
- .:/app
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: admin
POSTGRES_PASSWORD: secret
POSTGRES_DB: mydb
volumes:
- db_data:/var/lib/postgresql/data
volumes:
db_data:docker compose up -d
# App running at http://localhost:3000
# PostgreSQL running at localhost:5432One important detail: services in the same docker-compose.yml can reach each other using the service name as hostname. Notice the DATABASE_URL uses @db:5432 — not @localhost:5432. The app container connects to the db service by its name, because Docker Compose puts all services on a shared internal network automatically.
Summary
✅ Docker Compose manages multi-container apps from a single docker-compose.yml
✅ docker compose up -d starts the entire stack; docker compose down tears it down
✅ Use build: to build from a Dockerfile, image: to pull a pre-built image
✅ Bind mounts (.:/app) sync local code into containers — no rebuild on code changes
✅ Named volumes persist data across container restarts — essential for databases
✅ Services connect to each other using the service name as hostname, not localhost
Series: Docker & Kubernetes Learning Roadmap
Previous: Writing a Dockerfile: Build Your Own Docker Images
Next: Docker Networking & Volumes
Next up: a deeper look at how Docker networking and volumes work under the hood.
📬 Subscribe to Newsletter
Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.
We respect your privacy. Unsubscribe at any time.
💬 Comments
Sign in to leave a comment
We'll never post without your permission.