Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes

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

Sponsored

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 old docker-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-cache to 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 localhost instead of the service name to connect between containers — e.g., redis://localhost:6379 instead of redis://redis:6379.
  • Forgetting depends_on and wondering why the app container fails to connect to the database because the database isn't ready yet.
  • Placing docker-compose.yml outside the project root or missing the build: . context, causing build failures.
  • Overwriting environment variables — if you set POSTGRES_PASSWORD in both environment: and an .env file, Compose may use the wrong one.

Variations

  1. Use docker compose up --detach to run in the background, or docker compose up --build to rebuild images before starting.
  2. Specify a custom network name under networks: to isolate groups of services (e.g., backend and frontend networks).
  3. Use docker compose config to 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.yml file defines services, networks, and volumes for multi-container Docker applications.
  • Containers reach each other by service name, not localhost.
  • Use depends_on to 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 config before running.
  • The version key is optional in modern Compose — you can omit it for the latest features.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.