Docker Compose Complete Guide

Docker Compose defines and runs multi-container applications using a single YAML file. Instead of writing long docker run commands with many flags, you define your entire application stack in docker-compose.yml and bring it up with docker compose up. This makes the application configuration reproducible, version-controllable, and portable between environments. Compose v2 (the modern version) is included with Docker CE as a plugin.

Why Docker Compose?

Without Compose (maintaining multiple docker run commands):
  docker run -d --name db -e MYSQL_ROOT_PASSWORD=secret -v db-data:/var/lib/mysql mysql:8.0
  docker run -d --name app --link db:db -e DB_HOST=db -p 8080:8080 myapp:latest
  # Manual dependency, manual network, manual volumes

With Compose (one file, one command):
  docker compose up -d
  # Auto-creates network, volumes, starts in correct order

Compose file structure

nano docker-compose.yml

docker-compose.yml — full web application example

services:
  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: "${DB_ROOT_PASSWORD}"
      MYSQL_DATABASE: appdb
      MYSQL_USER: appuser
      MYSQL_PASSWORD: "${DB_PASSWORD}"
    volumes:
      - db-data:/var/lib/mysql
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 3

  app:
    image: myapp:latest
    environment:
      DB_HOST: db
      DB_NAME: appdb
      DB_USER: appuser
      DB_PASS: "${DB_PASSWORD}"
    ports:
      - "8080:8080"
    depends_on:
      db:
        condition: service_healthy    # Wait until DB passes healthcheck
    restart: unless-stopped

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - /etc/letsencrypt:/etc/letsencrypt:ro
    depends_on:
      - app
    restart: unless-stopped

volumes:
  db-data:    # Named volume managed by Docker

Essential Compose commands

# Start all services in detached mode:
docker compose up -d

# Stop all services:
docker compose down           # Stops and removes containers
docker compose down -v        # Also removes named volumes (DATA LOSS!)

# View logs:
docker compose logs           # All services
docker compose logs -f app    # Follow specific service

# Restart a single service:
docker compose restart app

# Pull latest images and recreate containers:
docker compose pull && docker compose up -d

# Run a command in a service container:
docker compose exec app bash
docker compose exec db mysql -u root -p

Production-ready Compose patterns

# Use .env file for secrets (never hardcode passwords in compose file):
cat .env
# DB_ROOT_PASSWORD=StrongRootPass!42
# DB_PASSWORD=AppPass!42
# .env is referenced automatically by docker compose

# Add .env to .gitignore:
echo ".env" >> .gitignore

# Use override files for dev vs production:
# docker-compose.yml           — base config
# docker-compose.override.yml  — dev overrides (auto-loaded)
# docker-compose.prod.yml      — production overrides

# Production deployment:
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

Conclusion

Every multi-container application should have a docker-compose.yml. Use the depends_on with condition: service_healthy to ensure containers start in the right order after health checks pass, not just when the container starts. Never put passwords directly in the compose file — use environment variables from a .env file, which stays on the server and out of version control. The restart: unless-stopped policy ensures containers automatically restart after a server reboot.

Need help with Ubuntu administration?

Work directly with Muhammad Irfan Aslam for Ubuntu Server, Linux, cloud, Docker, DevOps, CI/CD, or infrastructure troubleshooting support.

Work with Muhammad Irfan Aslam