Skip to content
Docker Compose Guide — Multi-Container Application Orchestration

Docker Compose Guide — Multi-Container Application Orchestration

DodaTech Updated Jun 7, 2026 7 min read

Docker Compose lets you define and run multi-container applications with a single YAML file — specifying services, networks, volumes, and dependencies so your entire application stack starts with one command.

What You’ll Learn

  • Writing docker-compose.yml files with services and configuration
  • Managing persistent data with volumes and bind mounts
  • Connecting containers with custom networks and service discovery
  • Using environment variables and .env files
  • Implementing healthchecks for service dependencies
  • Organizing configurations with profiles and multiple compose files

Why Docker Compose Matters

Running docker run commands for a web app, database, cache, and queue is tedious and error-prone — you have to specify ports, volumes, networks, and environment variables for each container separately. Docker Compose packages all of that into a single declarative YAML file. One docker compose up starts your entire stack, and docker compose down cleans everything up. DodaTech uses Docker Compose for local development of Durga Antivirus Pro’s web dashboard — the frontend dev server, API backend, PostgreSQL database, and Redis cache all start simultaneously with a single command.

    flowchart LR
    A[Docker & Bash Basics] --> B[Docker Compose]
    B --> C[Services]
    B --> D[Volumes]
    B --> E[Networks]
    B --> F[Profiles]
    C --> G[Multi-Container App]
    D --> H[Persistent Data]
    E --> I[Service Discovery]
    F --> J[Dev / Prod Configs]
    style B fill:#2496ed,color:#fff
  
Prerequisites: Basic Docker and Bash knowledge. Docker Desktop or Docker Engine installed on your machine.

Core Concepts

The docker-compose.yml File

version: "3.9"

services:
  web:
    build:
      context: ./web
      dockerfile: Dockerfile.dev
    ports:
      - "3000:3000"
    volumes:
      - ./web:/app  # Live code reload
      - /app/node_modules  # Exclude node_modules from bind mount
    environment:
      - NODE_ENV=development
      - DATABASE_URL=postgres://user:pass@db:5432/myapp
    depends_on:
      db:
        condition: service_healthy
    networks:
      - frontend
      - backend

  api:
    build: ./api
    ports:
      - "8080:8080"
    environment:
      - DATABASE_URL=postgres://user:pass@db:5432/myapp
      - REDIS_URL=redis://cache:6379
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    volumes:
      - ./api:/app
    networks:
      - backend

  db:
    image: postgres:16-alpine
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: myapp
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d myapp"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - backend

  cache:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redisdata:/data
    networks:
      - backend

volumes:
  pgdata:
  redisdata:

networks:
  frontend:
  backend:

Running the Stack

# Start all services
docker compose up

# Output:
# [+] Running 5/5
#  ✔ Container db      Healthy
#  ✔ Container cache   Started
#  ✔ Container api     Started
#  ✔ Container web     Started

# Start in detached mode
docker compose up -d

# View logs
docker compose logs -f web api

# List running services
docker compose ps

# Output:
# NAME                SERVICE             STATUS              PORTS
# app-web-1           web                 Up                  0.0.0.0:3000->3000/tcp
# app-api-1           api                 Up                  0.0.0.0:8080->8080/tcp
# app-db-1            db                  Up (healthy)        0.0.0.0:5432->5432/tcp
# app-cache-1         cache               Up                  0.0.0.0:6379->6379/tcp

# Stop all services
docker compose down

# Stop and remove volumes (destroys data)
docker compose down -v

Output: Compose creates containers with the configured ports, volumes, and networks. The depends_on with condition: service_healthy ensures the API waits until PostgreSQL is accepting connections.

Environment Variables and .env Files

# docker-compose.yml — reference variables
services:
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: ${DB_USER:-postgres}
      POSTGRES_PASSWORD: ${DB_PASSWORD:-secret}
      POSTGRES_DB: ${DB_NAME:-myapp}
# .env file (in the same directory as docker-compose.yml)
DB_USER=admin
DB_PASSWORD=supersecret
DB_NAME=production_app

# Or pass inline
DB_PASSWORD=other ./up docker compose up
# Use env_file for shared configurations
services:
  web:
    env_file:
      - ./web/.env
      - ./web/.env.local  # Override values (gitignored)

Profiles — Dev vs Prod

services:
  web:
    image: myapp:latest
    profiles: ["production"]  # Only started with --profile production

  web-dev:
    build: ./web
    ports:
      - "3000:3000"
    volumes:
      - ./web:/app
    profiles: ["development"]

  db:
    image: postgres:16-alpine
    # No profile — always started
# Start development stack
docker compose --profile development up

# Start production stack
docker compose --profile production up

# Start multiple profiles
docker compose --profile development --profile tools up

Healthchecks

Healthchecks ensure services are actually ready — not just started:

services:
  db:
    image: postgres:16-alpine
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d myapp"]
      interval: 5s
      timeout: 5s
      retries: 5
      start_period: 10s  # Wait 10s before checking

  api:
    build: ./api
    depends_on:
      db:
        condition: service_healthy  # Wait for healthy db
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3

Multiple Compose Files

Split configuration across files for different environments:

# docker-compose.yml — base config
# docker-compose.override.yml — local development overrides (auto-loaded)
# docker-compose.prod.yml — production overrides

# Override for production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

# Note: docker-compose.override.yml is loaded automatically by default
# docker-compose.prod.yml
services:
  web:
    build:
      context: ./web
      dockerfile: Dockerfile.prod
    ports:
      - "80:80"
      - "443:443"
    environment:
      - NODE_ENV=production
    restart: always

Common Mistakes

  1. Not using condition: service_healthy for database dependencies: depends_on: db only waits for the container to start, not for PostgreSQL to accept connections. Without a healthcheck, your app crashes on startup.

  2. Exposing database ports in production: ports: "5432:5432" on the database service should be removed in production. Only the API should connect to the database — no external access.

  3. Storing production secrets in docker-compose.yml: Use .env files (gitignored for secrets) or Docker secrets. Never commit passwords, API keys, or tokens to your repository.

  4. Not using volumes for persistent data: Without volumes, database data is lost when the container stops. Always define named volumes for stateful services.

  5. Overriding the CMD for database images: Editing the command for the postgres image breaks its initialization scripts. Use environment variables for configuration.

Practice Questions

  1. What is the difference between a named volume and a bind mount? Answer: Named volumes (pgdata:) are managed by Docker and persist across container lifecycles. Bind mounts (./web:/app) map a host directory into the container and allow live code editing.

  2. How does service discovery work in Docker Compose? Answer: Services on the same network can reach each other by service name (e.g., db:5432). Docker’s embedded DNS resolves service names to container IPs.

  3. What is the purpose of the depends_on directive? Answer: depends_on controls the startup order and, with condition, waits for healthchecks. However, it doesn’t wait for the service to be ready — use healthchecks for that.

  4. How do profiles help manage different environments? Answer: Profiles let you define which services start in each context (development, production, testing), preventing disk/memory waste from running unnecessary services.

Challenge

Build a full-stack application stack: create a compose file with a React frontend (with hot reload), a Node.js API, PostgreSQL with persistent volume, and Redis for caching. Add healthchecks to all services, use profiles to exclude dev tools (pgadmin, redis-commander) from production, create a .env file for database credentials, and practice the full up/down lifecycle.

FAQ

Is Docker Compose suitable for production?
: Docker Compose is ideal for single-host production deployments. For multi-host orchestration, use Docker Swarm or Kubernetes.
How does Docker Compose differ from Kubernetes?
: Compose is simpler — single-host, YAML-only, no learning curve. Kubernetes is multi-host, auto-scaling, self-healing, but more complex. Use Compose for local dev and simple deployments.
Can I use Docker Compose with Windows containers?
: Yes. Docker Compose supports Windows containers. Specify platform: windows in service definitions.
What is the difference between docker compose and docker-compose?
: docker compose (v2, no hyphen) is the newer Go-based plugin. docker-compose (v1, with hyphen) is the older Python tool. Use docker compose.
How do I debug a failing container?
: Use docker compose logs <service>, docker compose exec <service> sh for interactive shell, or docker compose run --rm <service> <command> for one-off tasks.

Try It Yourself

# Create a minimal demo
mkdir compose-demo && cd compose-demo

cat > docker-compose.yml << 'EOF'
services:
  web:
    image: nginx:alpine
    ports:
      - "8080:80"
    volumes:
      - ./html:/usr/share/nginx/html

  whoami:
    image: traefik/whoami
    ports:
      - "8081:80"
EOF

mkdir html
echo "<h1>Hello from Compose!</h1>" > html/index.html

docker compose up -d
curl http://localhost:8080
# <h1>Hello from Compose!</h1>

curl http://localhost:8081
# Hostname: abc123...

docker compose down

What’s Next

TopicDescription
GitHub Actions
CI/CD with containers
Docker
The container engine behind Compose

Related topics: Docker, Bash, Linux, AWS

What’s Next

Congratulations on completing this Docker Compose tutorial! Here’s where to go from here:

  • Practice daily — Consistency is more important than long study sessions
  • Build a project — Apply what you learned by building something real
  • Explore related topics — Check out other tutorials in the same category
  • Join the community — Discuss with other learners and share your progress

Remember: every expert was once a beginner. Keep coding!

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro