Write a docker-compose.yml
Learn to write a docker-compose.yml file step by step. This lesson covers core concepts, a hands-on walkthrough, troubleshooting, and edge cases for multi-container Docker applications.
Focus: write a docker-compose.yml file
Spinning up a single container with docker run works for learning, but real-world applications rarely live in isolation. A web app needs a database, a cache, a message queue — and starting each by hand, in the right order, with the right networks and volumes, becomes tedious and error-prone. Docker Compose fixes this by letting you define your entire multi-container application in a single YAML file, so you can start everything with one command.
The problem this lesson solves
Without Docker Compose, you run separate docker run commands for each service, manually manage networks and volumes, and worry about startup order. A single typo in a port mapping or a missing environment variable can break your stack. Worse, sharing this setup with teammates means sharing shell scripts or copy-pasted commands. Docker Compose eliminates this chaos by codifying your application's infrastructure in a docker-compose.yml file.
Core concept / mental model
Think of a docker-compose.yml file as a blueprints for your application's microservices ecosystem. It declares:
- Services: Each container (e.g., web server, database, cache)
- Networks: How containers talk to each other
- Volumes: Persistent data storage
- Environment variables: Configuration per service
Docker Compose reads this file and does the heavy lifting: creates networks, starts containers in the right order, and wires everything together. The result is a self-documenting, reproducible setup that anyone on your team can run with docker-compose up.
How it works step by step
1. Install Docker Compose
Docker Desktop includes Compose. On Linux, you may need to install it separately:
sudo apt-get install docker-compose-plugin
Check the version:
docker compose version
Note: Newer versions use
docker compose(no hyphen). The olddocker-compose(hyphen) is deprecated. We use the plugin version throughout this lesson.
2. Understand the basic structure
A minimal docker-compose.yml file has a services block. Each service maps to a container image and its configuration.
version: '3.8'
services:
web:
image: nginx:alpine
ports:
- "80:80"
db:
image: postgres:13-alpine
environment:
POSTGRES_PASSWORD: example
3. Define services, networks, and volumes
- services: The list of containers you want to run.
- networks: Custom networks (optional — Compose creates a default network automatically).
- volumes: Named volumes for persistent data.
4. Start the stack
Place the file in a project folder (often the root of your codebase) and run:
docker compose up -d
The -d flag runs containers in the background (detached mode).
5. View running services
docker compose ps
6. Stop and remove everything
docker compose down
Hands-on walkthrough
Let's create a real example: a Node.js app talking to a Redis cache.
Project structure
myapp/
├── docker-compose.yml
├── app.js
├── package.json
└── Dockerfile
1. Create package.json
{
"name": "myapp",
"version": "1.0.0",
"scripts": {
"start": "node app.js"
},
"dependencies": {
"express": "^4.18.0",
"redis": "^4.5.0"
}
}
2. Create app.js
const express = require('express');
const redis = require('redis');
const app = express();
const client = redis.createClient({ url: 'redis://redis:6379' });
client.on('error', err => console.error('Redis error', err));
app.get('/', async (req, res) => {
await client.connect();
const visits = await client.incr('visits');
res.send(`Hello! You are visitor ${visits}`);
await client.disconnect();
});
app.listen(3000, () => console.log('Server running on port 3000'));
3. Create Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
CMD ["node", "app.js"]
4. Write docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
depends_on:
- redis
environment:
- NODE_ENV=production
redis:
image: redis:7-alpine
ports:
- "6379:6379"
5. Run it
docker compose up --build -d
Check logs:
docker compose logs -f
Open http://localhost:3000 — you should see the visit counter increment each time.
6. Tear down
docker compose down
Compare options / when to choose what
| Feature | docker-compose.yml |
Raw docker run |
Kubernetes YAML |
|---|---|---|---|
| Setup simplicity | Very easy — one file | Medium — many commands | Complex — many files |
| Multi-container | Native — services block | Manual — scripts or scripts | Native — pods, deployments |
| Networking | Automatic default network | Manual --network flags |
Manual — services, ingress |
| Volume management | Declare named volumes | Manual --volume flags |
Declare PersistentVolumeClaim |
| Ideal for | Local dev & small projects | Single containers, quick tests | Production, large clusters |
When to choose Compose: You have 2–10 containers that need to work together, and you want a repeatable setup for local development, CI, or small deployments.
Troubleshooting & edge cases
Service name resolution
Containers can reach each other by service name (e.g., redis from app). If you use localhost, it won't work — use the service name.
Missing .env variables
Compose reads a .env file in the same directory automatically. If you set environment variables inline, make sure the syntax is correct:
environment:
- DB_PASSWORD=secret
Port conflicts
If port 3000 is already in use, change the host port:
ports:
- "3001:3000"
Startup order
depends_on only waits for the container to start, not for the service inside to be ready. For database readiness, use a health check or wait script.
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
Build fails
- Check that the Dockerfile and app code are in the same directory as
docker-compose.yml. - Use
docker compose build --no-cacheto force a fresh build.
What you learned & what's next
You now know how to write a docker-compose.yml file to define a multi-container application, including services, networks, volumes, and environment variables. You practiced with a real Node.js + Redis example, learned when to choose Compose over raw Docker commands, and how to troubleshoot common issues like port conflicts and startup order.
Key learning objectives achieved: - Explain the core concept of Docker Compose as a blueprint for multi-container apps. - Complete a practical exercise that runs two services and makes them communicate.
Next lesson: In the next step, you'll learn about Docker Compose networking — how to customize networks, control DNS resolution, and connect services across multiple Compose files.
Safe orchestration!
Practice recap
Create a new docker-compose.yml that runs an Nginx reverse proxy in front of a static site container. Use a named volume to serve the site's HTML files from the host. Test it with docker compose up and verify Nginx serves the page at http://localhost:8080.
Common mistakes
- Using
localhostinstead of the service name to connect between containers — e.g.,redis://localhost:6379instead ofredis://redis:6379. - Forgetting
depends_onand wondering why the app container fails to connect to the database because the database isn't ready yet. - Placing
docker-compose.ymloutside the project root or missing thebuild: .context, causing build failures. - Overwriting environment variables — if you set
POSTGRES_PASSWORDin bothenvironment:and an.envfile, Compose may use the wrong one.
Variations
- Use
docker compose up --detachto run in the background, ordocker compose up --buildto rebuild images before starting. - Specify a custom network name under
networks:to isolate groups of services (e.g., backend and frontend networks). - Use
docker compose configto validate your YAML and see the expanded configuration before running.
Real-world use cases
- Local development environment: Run a web application, database, and cache together for iterative coding.
- CI/CD pipeline: Use Compose to spin up integration test dependencies (e.g., PostgreSQL, Redis) and tear them down after tests.
- Small production deployments: Deploy a simple stack (web app + database) on a single VM using Compose with restart policies.
Key takeaways
- A
docker-compose.ymlfile defines services, networks, and volumes for multi-container Docker applications. - Containers reach each other by service name, not
localhost. - Use
depends_onto control startup order, but consider health checks for true readiness. - Docker Compose is ideal for local development, CI, and small deployments; for production scale, use Kubernetes.
- Always validate your compose file with
docker compose configbefore running. - The
versionkey is optional in modern Compose — you can omit it for the latest features.
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.