Use Docker Compose
Use Docker Compose to define multi‑container apps — Docker. Learn to define and run multi‑container applications with Compose in this hands‑on tutorial.
Focus: use docker compose to define multi‑container apps
Manually wiring a web app to a database, a Redis cache, and a background worker with raw docker run commands quickly becomes a nightmare. Every container needs the right network, environment variables, and startup order — one typo and everything breaks. That's exactly why Docker Compose exists: to declare every piece of your multi-container app in a single, version-controlled YAML file and spin it all up with one command.
The problem this lesson solves
Running a modern application — say a Python web API backed by PostgreSQL and a Redis queue — with individual docker run calls is brittle and hard to reproduce. You must manually create networks (docker network create myapp_net), link containers, pass environment variables on each --env flag, and maintain a mental map of startup order. A colleague who clones your repo needs a wiki page or, worse, tribal knowledge to get the app running. Docker Compose eliminates this pain by letting you define the entire application stack in a declarative YAML file (compose.yaml) and manage it as a single unit.
Core concept / mental model
Think of Docker Compose as an application-level orchestrator for a single host. The mental model has three layers:
Services
A service is a container whose configuration you define — the image, ports, environment variables, volumes, and dependencies. In Compose, you don't run a PostgreSQL container; you run a PostgreSQL service that happens to be backed by a container.
Networks
Compose automatically creates a default network for all services in the same file. Services can reach each other by their service name (which acts as DNS). No need to --link or manually specify IPs.
Volumes
Named volumes are declared at the top level and mounted into services. Compose ensures volumes persist across up/down cycles unless you deliberately remove them.
Pro tip: If you've ever used Kubernetes, Compose is like a single-node, developer-friendly subset. You describe the desired state, and Compose reconciles reality to match.
How it works step by step
Here's the flow from zero to running multi-container app:
- Create a
compose.yamlfile (ordocker-compose.yaml) in your project root. - Define services under the
services:key. Each service gets a name, animage:(or abuild:context), and configuration likeports:,environment:, andvolumes:. - Declare dependencies with the
depends_on:key to control startup order. - Add networks (optional) if you need more than one isolated network.
- Run
docker compose up -d— Compose creates networks, pulls/builds images, starts containers in dependency order. - Inspect with
docker compose psordocker compose logs -f. - Tear down with
docker compose down(with-vto remove volumes).
Hands-on walkthrough
Let's build a three-service application: a Python web server, a PostgreSQL database, and a Redis cache.
Project structure
myapp/
├── app/
│ └── server.py
├── Dockerfile
└── compose.yaml
Example 1: Minimal web + database
# compose.yaml
services:
web:
build: .
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql://user:pass@db/mydb
depends_on:
- db
db:
image: postgres:16
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: mydb
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
Run it:
docker compose up -d
Example 2: Adding Redis and a worker
services:
web:
build: .
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql://user:pass@db/mydb
- REDIS_URL=redis://cache:6379
depends_on:
- db
- cache
worker:
build: .
command: python worker.py
environment:
- DATABASE_URL=postgresql://user:pass@db/mydb
- REDIS_URL=redis://cache:6379
depends_on:
- db
- cache
db:
image: postgres:16
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: mydb
volumes:
- pgdata:/var/lib/postgresql/data
cache:
image: redis:7-alpine
volumes:
pgdata:
Expected output when running docker compose up -d:
[+] Running 5/5
✔ Network myapp_default Created
✔ Volume myapp_pgdata Created
✔ Container myapp-cache-1 Started
✔ Container myapp-db-1 Started
✔ Container myapp-web-1 Started
✔ Container myapp-worker-1 Started
Example 3: Using environment variables from a file
services:
web:
build: .
env_file:
- .env
ports:
- "8000:8000"
With a .env file:
DATABASE_URL=postgresql://prod_user:securepass@prod-db.internal/mydb
SECRET_KEY=supersecret
Pro tip: Never commit
.envfiles to version control. Use.env.exampleas a template.
Compare options / when to choose what
| Approach | Pros | Cons | Best for |
|---|---|---|---|
docker run per container |
Simple for one-off demos, no extra config | Manual networking, no startup ordering, not reproducible | Quick tests, single-container apps |
| Shell scripts | Freeform logic | Hard to maintain, no standard syntax, error-prone | Ad-hoc automation |
| Docker Compose | Declarative, version-controlled, dependency management, built-in DNS | Single-host only, not suitable for production clusters | Multi-container local dev, CI, small production deployments |
| Kubernetes | Production-grade, self-healing, multi-node, rolling updates | Complexity, heavy resource usage, overkill for dev | Large-scale production, orchestration |
Choose Docker Compose when your app needs two or more containers that communicate, and you want a reproducible, developer-friendly setup. Skip it for single-container apps (just use docker run) or production clusters (use K8s).
Troubleshooting & edge cases
Service not reachable by name
- Cause: Containers on different networks.
- Fix: Ensure both services are in the same Compose file and network. Don't override
networks:unless necessary.
depends_on doesn't wait for database readiness
- Cause:
depends_ononly waits for container start, not for the process inside to be ready. - Fix: Use a healthcheck and
condition: service_healthy(Compose 2.1+).
services:
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
interval: 5s
timeout: 5s
retries: 5
web:
depends_on:
db:
condition: service_healthy
Port conflicts
- Symptom:
Error starting userland proxy: listen tcp4 0.0.0.0:5432: bind: address already in use - Fix: Change the host port mapping (e.g.,
"5433:5432") or stop the conflicting container.
Volume permissions in Linux
- Issue: Containers write files as root; host user can't read them.
- Fix: Set user ID in the container via
user: "1000:1000"(match your host UID).
What you learned & what's next
You now know how to use Docker Compose to define multi‑container apps — declare services, networks, and volumes in a compose.yaml file, manage startup order with depends_on, and run the whole stack with a single command. You saw hands-on examples with a web server, database, cache, and worker, and you learned when Compose beats raw docker run or Kubernetes.
Next step: Connect Docker Compose to Your CI/CD Pipeline — learn how to integrate Compose-based tests into GitHub Actions or GitLab CI.
Key takeaways from this lesson
- ✅ Docker Compose defines multi‑container apps declaratively in YAML.
- ✅ Services communicate over a default network using service names as DNS.
- ✅
depends_oncontrols startup order; use healthchecks for readiness. - ✅ Volumes declared at the top level persist data across container restarts.
- ✅ Use
docker compose up -dto start anddocker compose down -vto clean up. - ✅ Compose is ideal for local dev and CI, not for production multi-node clusters.
Practice recap
Create a compose.yaml for your current side project: define at least two services (e.g., a web app and a database), mount a named volume for data, and add environment variables via .env. Run docker compose up -d and verify connectivity with docker compose exec web curl http://db:5432. Finally, tear it down with docker compose down -v.
Common mistakes
- Using
depends_onwithout healthchecks and wondering why a service fails to connect — the database container may be running but not yet accepting connections. - Exposing ports that conflict with other containers already on the host — always check
docker psor use random host ports withports: 0:8080in development. - Hardcoding environment variables in the Compose file instead of using an
.envfile — leads to secret leakage in version control. - Mounting host directories without matching user permissions — files appear owned by
rootinside the container because the container user doesn't match the host UID.
Variations
- Use Docker Compose V2 (
docker composecommand) instead of the legacy V1 (docker-compose) — newer syntax and better performance. - Add a
profileskey to start only a subset of services (e.g.,profiles: ["debug"]) for conditional service activation. - Use
docker compose configto validate and view the resolved YAML including merged defaults — great for debugging complex files.
Real-world use cases
- Local development environment for a Django/Node.js app with PostgreSQL, Redis, and a background task worker — all defined in a single
compose.yaml. - CI pipeline that spins up integration test dependencies (e.g., MySQL, RabbitMQ) exactly matching production versions, then tears them down after tests pass.
- Small production deployment for a side project receiving fewer than 1000 requests/day — Compose handles the web server, database, and reverse proxy (e.g., Nginx).
Key takeaways
- Docker Compose lets you define every container, network, and volume in a single
compose.yamlfile — the stack becomes reproducible and version-controllable. - Services on the same Compose network resolve each other by service name — no need to manage IPs.
- Use
depends_onplus healthchecks to ensure dependent services wait until the backing process is truly ready. - Declare named volumes in the top-level
volumes:block to persist data beyond container lifecycles. - Run the entire stack with
docker compose up -dand clean it up withdocker compose down -v(volumes included). - Docker Compose is a developer productivity tool, not a production orchestrator — use Kubernetes for cluster-level management.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.