Gerson

Gerson

Passionate developer specializing in web development, cloud architecture, and system design.

TypeScriptReactNext.jsPythonFastAPISQLNode.jsAWS

Docker Compose for Full-Stack Developers: A Practical Guide

Stop fighting environment setup. Docker Compose lets you define your entire development stack — database, API, frontend, cache — in one file and start it with a single command. A practical guide with real-world patterns.

Gersonhttps://docs.docker.com/compose/
Container shipping yard representing Docker containers

If you've ever spent an afternoon helping a teammate set up PostgreSQL, Redis, and three microservices just to run the project locally, Docker Compose is the solution. It lets you define your entire development stack in a single YAML file and start everything with docker compose up.

This guide covers the Docker Compose patterns I use daily for full-stack web development, with practical examples for common setups.

A Complete Development Stack

Here's a realistic compose.yml for a Next.js application with a PostgreSQL database, Redis cache, and a Python API service:

compose.yml

services:
  db:
    image: postgres:17
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: devpassword
      POSTGRES_DB: myapp
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 5s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s

  api:
    build:
      context: ./api
      dockerfile: Dockerfile.dev
    ports:
      - "8000:8000"
    volumes:
      - ./api:/app
    environment:
      DATABASE_URL: postgresql://app:devpassword@db:5432/myapp
      REDIS_URL: redis://redis:6379
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy

  web:
    build:
      context: ./web
      dockerfile: Dockerfile.dev
    ports:
      - "3000:3000"
    volumes:
      - ./web:/app
      - /app/node_modules
    environment:
      API_URL: http://api:8000

volumes:
  postgres_data:

Key Patterns

Health Checks and Dependency Ordering

The depends_on with condition: service_healthy pattern is essential. Without health checks, Docker only waits for the container to start, not for the service inside it to be ready. A PostgreSQL container that started 100ms ago will reject connections, causing your API to crash on boot.

Volume Mounts for Hot Reload

Mounting your source code as a volume (./api:/app) means changes you make locally are immediately reflected inside the container. Combined with a development server that watches for file changes (uvicorn with --reload, Next.js dev server), you get the same hot-reload experience as running locally.

The /app/node_modules anonymous volume prevents your local node_modules from overwriting the container's — this is important when your host OS differs from the container OS (e.g., macOS host, Linux container).

Environment Variables

Notice that the API service uses db as the hostname to reach PostgreSQL, not localhost. Docker Compose creates a network where services can reach each other by their service name. This is the most common source of confusion for developers new to Docker.

Development vs. Production Dockerfiles

Use separate Dockerfiles for development and production:

Dockerfile.dev (development)

FROM node:24-slim
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]

Dockerfile (production)

FROM node:24-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:24-slim AS runner
WORKDIR /app
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
CMD ["node", "server.js"]

The development Dockerfile installs all dependencies (including devDependencies) and runs the dev server. The production Dockerfile uses a multi-stage build to create a minimal image with only what's needed to run the application.

Useful Commands

Common Docker Compose commands

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

# View logs from a specific service
docker compose logs -f api

# Run a one-off command in a service
docker compose exec db psql -U app -d myapp

# Rebuild after changing a Dockerfile
docker compose up -d --build api

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

# Scale a service (e.g., multiple API workers)
docker compose up -d --scale api=3

Pro Tip: Add a Makefile with common commands so teammates don't need to remember Docker Compose syntax. make dev is easier than docker compose up -d --build.

When Not to Use Docker Compose

Docker Compose is great for development environments but isn't the right tool for everything:

  • If you're deploying to Vercel, AWS Lambda, or similar serverless platforms, your production deployment doesn't use Docker at all — use Compose only for local development.
  • For orchestrating containers in production across multiple machines, use Kubernetes, ECS, or a similar production orchestrator.
  • If your project is a single frontend app with no backend dependencies, Docker Compose adds complexity without much benefit — just run npm run dev.

Resources