Docker Full Tutorial — Part 6: Docker Compose

By Suraj Ahir November 21, 2025 6 min read

Docker — Docker Volumes
Docker — Docker Volumes
← Part 5 Docker Tutorial · Part 6 of 12 Part 7 →

Running one container with a long docker run command is manageable. Running five containers with networks, volumes, environment variables, and port mappings by typing individual commands becomes chaotic and error-prone. Docker Compose solves this by letting you define your entire multi-container application in a single YAML file and start everything with one command.

What is Docker Compose?

Docker Compose is a tool for defining and running multi-container Docker applications. You describe your application's services, networks, and volumes in a docker-compose.yml file. Then you use the docker compose up command to create and start all services together. It is the standard way to manage local development environments and simple production deployments.

Your First docker-compose.yml

Let us build a real example — a web application with Nginx, Flask, and PostgreSQL:

docker-compose.yml
version: '3.8'

services:
  # PostgreSQL Database
  db:
    image: postgres:15
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: apppassword
    volumes:
      - postgres-data:/var/lib/postgresql/data
    networks:
      - app-network

  # Flask Backend
  backend:
    build: ./backend  # Build from local Dockerfile
    ports:
      - "5000:5000"
    environment:
      DATABASE_URL: postgresql://appuser:apppassword@db:5432/myapp
    depends_on:
      - db
    networks:
      - app-network
    volumes:
      - ./backend:/app  # Bind mount for development

  # Nginx Frontend Proxy
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - backend
    networks:
      - app-network

volumes:
  postgres-data:

networks:
  app-network:
    driver: bridge

Essential Docker Compose Commands

Compose Commands
# Start all services (build if needed)
docker compose up

# Start in background (detached mode)
docker compose up -d

# Build images without starting
docker compose build

# Rebuild a specific service
docker compose build backend

# Stop all services
docker compose down

# Stop and remove volumes
docker compose down -v

# View logs of all services
docker compose logs

# Follow logs of a specific service
docker compose logs -f backend

# List running services
docker compose ps

# Run a command in a service container
docker compose exec backend bash

# Scale a service (run multiple instances)
docker compose up -d --scale backend=3

Understanding depends_on

The depends_on option tells Docker Compose to start services in a specific order. In our example, the backend starts after the database, and Nginx starts after the backend. However, there is an important nuance: depends_on only waits for the container to start, not for the service inside it to be ready. A PostgreSQL container can be running but not yet accepting connections. For health checks and wait logic, you need additional tooling like wait-for-it scripts or Docker healthchecks.

Environment Variables in Compose

Hardcoding passwords in docker-compose.yml is bad practice. Use a .env file instead:

.env file
POSTGRES_PASSWORD=mysecurepassword
POSTGRES_USER=appuser
POSTGRES_DB=myapp
docker-compose.yml with .env
services:
  db:
    image: postgres:15
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_DB: ${POSTGRES_DB}

Docker Compose automatically reads the .env file in the same directory. Add .env to your .gitignore to avoid committing secrets.

Health Checks

Healthcheck in Compose
services:
  db:
    image: postgres:15
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d myapp"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

Development vs Production Compose Files

A common pattern is to have multiple Compose files — a base file and environment-specific overrides:

Override Files
# Base configuration
docker-compose.yml

# Development overrides (bind mounts, debug settings)
docker-compose.dev.yml

# Start with development overrides
docker compose -f docker-compose.yml -f docker-compose.dev.yml up

In Part 7, we will explore Docker in real-world CI/CD pipelines — how images are built, tagged, pushed to registries, and deployed automatically in modern development workflows.

Docker Compose Environment Variables and Secrets

Hardcoding sensitive values in docker-compose.yml is a security risk — these files often end up in version control. Docker Compose supports several approaches for managing configuration and secrets. The .env file in the same directory as docker-compose.yml is automatically loaded and its variables are available in the compose file using ${VARIABLE_NAME} syntax. This file should be in .gitignore. For even more flexibility, each service can specify an env_file path to load environment-specific configuration:

Docker Compose with Variables
version: '3.8'
services:
  db:
    image: postgres:15
    environment:
      POSTGRES_DB: ${DB_NAME}
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data

  app:
    build: .
    env_file:
      - .env
    ports:
      - "${APP_PORT:-3000}:3000"
    depends_on:
      - db

volumes:
  postgres_data:

Health Checks in Docker Compose

The depends_on directive in Compose only waits for a container to start, not for the service inside to be ready. Adding health checks ensures services are actually ready before dependent services start. Define a healthcheck in the service configuration: healthcheck: test: ["CMD", "pg_isready"] interval: 10s timeout: 5s retries: 5. Then use depends_on: db: condition: service_healthy to wait for the healthy state before starting the dependent service.

Practice Exercise

Build a complete three-service application with Docker Compose: a database (PostgreSQL or MySQL), a backend API (any language), and a frontend or nginx reverse proxy. Use a .env file for all credentials and configuration. Add health checks to the database service and make the API depend on it being healthy. Test the full stack with docker compose up and verify all services start correctly and can communicate.

Building Cloud Intuition Over Time

Cloud computing is a domain where deep intuition — the ability to make good architectural decisions quickly, to diagnose problems efficiently, and to anticipate how systems will behave under load — develops through accumulated hands-on experience. Every project you build on cloud infrastructure teaches you something that cannot be learned from documentation alone. The cost surprises, the permission errors, the networking debugging sessions, the performance investigations — these are not obstacles to learning, they are the learning. The engineers who have built genuinely deep cloud intuition have usually accumulated it through many projects over several years, not from any single course or certification. Start building things, make mistakes safely in learning environments, and accumulate that experience deliberately.

Disclaimer: This content is for educational purposes only. SRJahir Tech does not guarantee any specific outcome or job placement.