Docker Compose Explained: From Zero to Running

Docker Compose lets you define and run multi-container applications with a single YAML file and a single command. This guide walks you from an empty directory to a fully running web app with a database, cache, and background worker — all orchestrated by Compose.

Docker Docker Compose DevOps Containers

What Is Docker Compose?

Running a single Docker container is straightforward. But real applications have multiple services: a web server, a database, a caching layer, a message queue. Coordinating these manually with individual docker run commands — remembering all the ports, volumes, networks, and environment variables — is tedious and error-prone.

Docker Compose solves this by letting you describe your entire multi-container application in a single docker-compose.yml file. From there, one command starts everything, another stops it, and the configuration is version-controlled alongside your code.

Installing Docker Compose

Docker Compose V2 is included with Docker Desktop (Mac/Windows). On Linux:

# Check if already installed
docker compose version

# Install on Ubuntu/Debian if missing
sudo apt update && sudo apt install docker-compose-plugin

Note: The modern command is docker compose (no hyphen), which is the V2 plugin. The older standalone docker-compose (with hyphen) is V1 and deprecated.

Your First docker-compose.yml

Let's build a simple Node.js app with a PostgreSQL database. Create a docker-compose.yml in your project root:

version: '3.9'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgres://postgres:password@db:5432/myapp
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - .:/app
      - /app/node_modules

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: myapp
      POSTGRES_PASSWORD: password
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

volumes:
  postgres_data:

Key Concepts Explained

services

Each key under services is a container. The name (app, db) becomes the hostname on the internal Docker network — this is why the database URL uses @db:5432 rather than @localhost:5432.

build vs image

build: . tells Compose to build an image from the Dockerfile in the current directory. image: postgres:15-alpine pulls a pre-built image from Docker Hub. Use build for your own services and image for third-party services.

ports

"3000:3000" maps host port 3000 to container port 3000. Format is HOST:CONTAINER. Only map ports you need to expose to your host machine — containers communicate with each other over the internal network without port mapping.

volumes

Named volumes like postgres_data persist data across container restarts. Bind mounts like .:/app mount your local directory into the container for live code reloading during development.

depends_on with health checks

depends_on with condition: service_healthy ensures your app does not start until the database is ready to accept connections — not just started. Without this, your app may crash on startup because it cannot connect to a database that is still initializing.

Essential Docker Compose Commands

# Start all services in the background
docker compose up -d

# View running containers and their status
docker compose ps

# Stream logs from all services
docker compose logs -f

# Stream logs from one service
docker compose logs -f app

# Execute a command inside a running container
docker compose exec db psql -U postgres myapp

# Stop and remove containers (keeps volumes)
docker compose down

# Stop and remove everything including volumes
docker compose down -v

# Rebuild images and restart
docker compose up -d --build

Using Environment Variables Securely

Never hard-code secrets in your docker-compose.yml. Instead, use a .env file (which you .gitignore) and reference variables with ${VARIABLE} syntax:

# .env file (gitignored)
POSTGRES_PASSWORD=supersecret
JWT_SECRET=my-jwt-signing-key

# docker-compose.yml
services:
  db:
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}

Compose automatically loads the .env file from the same directory as docker-compose.yml.

Adding Redis for Caching

Extending the previous example with a Redis cache:

services:
  app:
    # ... (same as before)
    environment:
      - REDIS_URL=redis://cache:6379
    depends_on:
      - db
      - cache

  cache:
    image: redis:7-alpine
    command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
    ports:
      - "6379:6379"

Production Considerations

Docker Compose is excellent for development and single-server deployments. For production on a single server, it works well. For multi-server deployments with high availability requirements, consider Docker Swarm or Kubernetes. Key production practices:

✅ Start every new project with a docker-compose.yml on day one. The five minutes it takes will save hours of "works on my machine" problems when collaborators join or you deploy to a new server.

Related Snippets