Docker Compose: Multi‑Service Orchestration

In real‑world commercial projects, no application runs in isolation. A modern SaaS stack is typically composed of several independent services that must communicate, scale, and be deployed together. Common components include:

  • Frontend Service: Next.js / React App
  • Backend API: FastAPI / Express.js
  • Database: PostgreSQL / MySQL
  • Cache Layer: Redis
  • Task Queue: RabbitMQ / Bull Queue
  • Scheduled Scrapers: Python Scraper

Manually starting each container with docker run is tedious, error‑prone, and hard to coordinate. Docker Compose was built to solve exactly this problem.

What is Docker Compose?

Docker Compose is a tool for defining and running multi‑container Docker applications. You write a single compose.yaml (or docker‑compose.yml) file that describes your entire service architecture, and with one command you can bring the whole system up or down.

Core Value Proposition

| Feature | What it Does | Why It Matters | |---------|--------------|----------------| | Single‑Command Startup | docker compose up -d launches all services | Saves time, reduces human error | | Automatic Networking | All services join a shared virtual network | Enables service discovery by name | | Dependency Management | depends_on and health checks | Guarantees correct startup order | | Consistent Environments | Same Compose file used by every developer | Eliminates “works on my machine” bugs | | Resource Limits | CPU, memory constraints per service | Prevents runaway containers from hogging host resources |

Why Docker Compose is a Business Asset

  • Faster Time‑to‑Market: Developers can spin up a full stack locally in seconds, accelerating feature cycles.
  • Reduced Operational Footprint: Consistent environments mean fewer support tickets and easier onboarding.
  • Cost Efficiency: By setting resource limits, you avoid over‑provisioning and keep infrastructure costs predictable.
  • Scalable Development Workflow: Compose can be extended to run in CI pipelines, ensuring that integration tests run against a realistic multi‑service environment.

How to Build a Three‑Tier Application with Compose

We’ll walk through creating a complete example: PostgreSQL database, FastAPI backend, and Next.js frontend. The steps are fully reproducible and can be adapted to any stack.

1. Project Layout

my-fullstack-app/
├── frontend/           # Next.js application
│   ├── package.json
│   ├── Dockerfile
│   └── ...
├── backend/            # FastAPI application
│   ├── main.py
│   ├── requirements.txt
│   ├── Dockerfile
│   └── ...
├── docker-compose.yml  # Compose configuration
└── .env                # Environment variables

2. Create docker-compose.yml

# docker-compose.yml
version: "3.8"

services:
  # === Database Service ===
  postgres:
    image: postgres:16-alpine
    container_name: myapp-postgres
    restart: unless-stopped
    environment:
      POSTGRES_USER: ${DB_USER:-myapp}
      POSTGRES_PASSWORD: ${DB_PASSWORD:-secret123}
      POSTGRES_DB: ${DB_NAME:-myapp}
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./backend/init.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-myapp}"]
      interval: 10s
      timeout: 5s
      retries: 5

  # === Backend API Service ===
  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile
    container_name: myapp-backend
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy
    environment:
      DATABASE_URL: postgresql://${DB_USER:-myapp}:${DB_PASSWORD:-secret123}@postgres:5432/${DB_NAME:-myapp}
      API_PORT: 8000
    ports:
      - "8000:8000"
    volumes:
      - ./backend:/app
    command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload

  # === Frontend Service ===
  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile
    container_name: myapp-frontend
    restart: unless-stopped
    depends_on:
      - backend
    environment:
      NEXT_PUBLIC_API_URL: http://backend:8000
    ports:
      - "3000:3000"
    volumes:
      - ./frontend:/app
      - /app/node_modules
      - /app/.next

volumes:
  postgres_data:

Detailed Breakdown

  • Top‑Level Keys

    • services: Declares each container.
    • volumes: Defines named volumes for persistent storage.
  • postgres Service

    • image: Pulls the official PostgreSQL 16 Alpine image.
    • restart: Keeps the container alive unless manually stopped.
    • environment: Uses environment variables with defaults; values are overridden by .env.
    • volumes: Persists data across container restarts and mounts an SQL init script.
    • healthcheck: Periodically verifies that the database is ready before other services start.
  • backend Service

    • build: Builds from the ./backend directory.
    • depends_on: Waits for the PostgreSQL healthcheck to succeed.
    • environment: Passes the database URL and API port.
    • volumes: Mounts source code for hot reloading.
    • command: Runs Uvicorn with --reload for development.
  • frontend Service

    • depends_on: Starts after the backend.
    • environment: Exposes the backend URL to the Next.js client.
    • volumes: Mounts source code and uses anonymous volumes for node_modules and .next to avoid host‑container conflicts.

3. Dockerfiles

Backend (backend/Dockerfile)

FROM python:3.12-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]

Frontend (frontend/Dockerfile)

FROM node:20-alpine

WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

COPY . .

EXPOSE 3000
CMD ["npm", "run", "dev"]

4. Environment Variables (.env)

# .env
DB_USER=myapp
DB_PASSWORD=super_secret_password
DB_NAME=myapp

Why use .env?
• Keeps secrets out of source control.
• Allows different values per environment (dev, test, prod).
• Keeps the Compose file clean and reusable.

5. Starting the Stack

# From the project root
docker compose up -d

You should see output similar to:

[+] Running 4/4
 ✔ Network my-fullstack-app_default   Created
 ✔ Volume my-fullstack-app_postgres_data Created
 ✔ Container myapp-postgres           Started
 ✔ Container myapp-backend            Started
 ✔ Container myapp-frontend           Started

6. Inspecting Services

# List running containers
docker compose ps

# Follow logs for all services
docker compose logs -f

# Tail logs for a specific service
docker compose logs -f backend

7. Running One‑Off Commands

# Run a database migration inside the backend container
docker compose exec backend alembic upgrade head

# Query the database directly
docker compose exec postgres psql -U myapp -d myapp -c "SELECT * FROM users;"

8. Rebuilding Services

# Rebuild and restart only the backend
docker compose up -d --build backend

# Rebuild everything
docker compose up -d --build

9. Stopping and Cleaning Up

# Stop all services (containers remain)
docker compose stop

# Stop and remove containers, networks, but keep volumes
docker compose down

# Remove everything, including volumes (dangerous!)
docker compose down -v

⚠️ Caution: down -v deletes all named volumes, wiping persistent data. Use only when you intentionally want a clean slate.

10. Advanced Compose Features

| Feature | How to Use | Business Benefit | |---------|------------|------------------| | Service Profiles | profiles: [dev, prod] | Run only the services you need for a given environment. | | Networks | networks: {frontend: {}, backend: {}} | Isolate traffic between tiers for security. | | Secrets | secrets: { db_password } | Store sensitive data in Docker secrets instead of plain env vars. | | Healthcheck Customization | healthcheck: { test: ["CMD", "curl", "-f", "http://localhost:8000/health"] } | Ensure API health before exposing to the frontend. | | Resource Limits | deploy: { resources: { limits: { cpus: '0.5', memory: '512M' } } } | Prevent runaway containers from exhausting host resources. |

11. Integrating Vibe Coding for Compose Generation

If you prefer not to hand‑write YAML, you can ask Vibe Coding to generate a Compose file from natural language. For example:

🔥 Vibe Coding Compose Prompt
“Create a docker-compose.yml that includes:

  1. PostgreSQL 16 with user myapp, password from .env, data in a volume.
  2. Redis 7 as a cache layer.
  3. Node.js Express API that connects to PostgreSQL and Redis, with hot reload via nodemon.
  4. Next.js frontend that consumes the API.
  5. Proper dependency ordering: backend waits for PostgreSQL health, frontend waits for backend.
  6. Include healthchecks and restart policies.”

Vibe Coding will output a ready‑to‑use Compose file, saving you time and reducing syntax errors.

12. Troubleshooting Common Issues

| Symptom | Likely Cause | Fix | |---------|--------------|-----| | Backend cannot connect to PostgreSQL | Wrong host or port | Ensure depends_on and healthcheck are set; use postgres as host. | | Frontend shows 404 on API calls | CORS disabled | Add CORS middleware in FastAPI or configure Next.js proxy. | | Container crashes on start | Missing environment variable | Verify .env is present and exported; use defaults in Compose. | | Volume not persisting | Named volume mis‑named | Ensure volume name matches in volumes: section. | | Slow startup | Large image download | Use --pull=always or pre‑pull images. |

13. Summary of Key Takeaways

  1. Docker Compose is the de‑facto standard for orchestrating multi‑container local development.
  2. A single docker-compose.yml file defines services, networks, volumes, and environment variables.
  3. The three‑tier example demonstrates best practices: health checks, dependency ordering, hot reloading, and persistent storage.
  4. Compose commands (up, down, logs, exec, ps) give you full control over the lifecycle.
  5. Environment variables keep secrets out of code; .env files are per‑environment.
  6. Vibe Coding can auto‑generate Compose files from natural language, accelerating setup.

Transition to the Next Chapter

Having mastered Docker Compose, you now have a solid foundation for local multi‑service development. The next logical step is to move from single‑host orchestration to cluster‑level orchestration with Kubernetes. In the upcoming chapter, we’ll explore how to translate a Compose‑defined stack into Kubernetes manifests, manage deployments, services, config maps, secrets, and rolling updates. This will empower you to deploy the same stack reliably to production environments, scale it horizontally, and integrate with CI/CD pipelines for continuous delivery. Stay tuned as we dive into the world of Kubernetes and learn how to orchestrate containers at scale.

Unlock Full Tutorial

This chapter is paid content. Join the project to unlock over 5000 words of deep analysis, including 10+ god-tier Prompts and real Source Code examples!