---
title: "Docker Compose Patterns for Local Development"
description: "Multi-service stacks, healthchecks, live reload, networking, profiles, and override files for productive local development with Docker Compose."
url: https://agent-zone.ai/knowledge/infrastructure/docker-compose-patterns/
section: knowledge
date: 2026-02-22
categories: ["infrastructure"]
tags: ["docker","docker-compose","containers","local-development","networking"]
skills: ["container-orchestration","local-dev-environment"]
tools: ["docker","docker-compose"]
levels: ["intermediate"]
word_count: 794
formats:
  json: https://agent-zone.ai/knowledge/infrastructure/docker-compose-patterns/index.json
  html: https://agent-zone.ai/knowledge/infrastructure/docker-compose-patterns/?format=html
  api: https://api.agent-zone.ai/api/v1/knowledge/search?q=Docker+Compose+Patterns+for+Local+Development
---


## Multi-Service Stack Structure

A typical local development stack has an application, a database, and maybe a cache or message broker. The compose file should read top-to-bottom like a description of your system.

```yaml
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    env_file:
      - .env
    volumes:
      - ./src:/app/src
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: myapp
      POSTGRES_PASSWORD: localdev
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U myapp"]
      interval: 5s
      timeout: 3s
      retries: 5

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

volumes:
  pgdata:
```

## depends_on and Healthchecks

The `depends_on` field controls startup order, but without a condition it only waits for the container to start, not for the service inside to be ready. A Postgres container starts in under a second, but the database process takes several seconds to accept connections. Use `condition: service_healthy` paired with a `healthcheck` to block until the dependency is actually ready.

Common healthcheck patterns for popular services:

```yaml
# PostgreSQL
healthcheck:
  test: ["CMD-SHELL", "pg_isready -U myapp"]
  interval: 5s
  timeout: 3s
  retries: 5

# MySQL
healthcheck:
  test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
  interval: 5s
  timeout: 3s
  retries: 5

# Redis
healthcheck:
  test: ["CMD", "redis-cli", "ping"]
  interval: 5s
  timeout: 3s
  retries: 3

# HTTP service
healthcheck:
  test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
  interval: 10s
  timeout: 5s
  retries: 3
  start_period: 15s
```

The `start_period` field is important for services with slow startup. During the start period, failed healthchecks do not count toward the retry limit.

## Volume Mounts for Live Reload

Bind mounts let you edit code on your host and see changes immediately inside the container. The key is mounting only the source directory, not the entire project (which would overwrite `node_modules` or other build artifacts inside the container).

```yaml
services:
  frontend:
    build: ./frontend
    volumes:
      - ./frontend/src:/app/src
      - ./frontend/public:/app/public
      # Prevent host node_modules from overwriting container's
      - /app/node_modules
    command: npm run dev
```

The anonymous volume `/app/node_modules` tells Docker to preserve the container's `node_modules` directory even though the parent `/app` directory has host-mounted content. Without it, the host's potentially empty or OS-mismatched `node_modules` would shadow the container's.

For Go or compiled languages, pair the mount with a file watcher like `air` or `watchexec` running inside the container.

## Networking

Compose creates a default bridge network for each project. Services can reach each other by service name as hostname. If your app config connects to `db:5432`, that resolves within the compose network automatically.

For more control, define explicit networks:

```yaml
services:
  app:
    networks:
      - frontend
      - backend

  db:
    networks:
      - backend

  nginx:
    networks:
      - frontend

networks:
  frontend:
  backend:
```

This isolates `db` from `nginx` — the proxy can reach `app` but not the database directly. Network aliases let a single service answer to multiple hostnames:

```yaml
services:
  db:
    networks:
      backend:
        aliases:
          - postgres
          - database
```

## Environment Files

Keep secrets and configuration out of the compose file. Use `.env` files per service or per environment:

```yaml
services:
  app:
    env_file:
      - .env
      - .env.local  # overrides values from .env
    environment:
      # Inline values override env_file values
      LOG_LEVEL: debug
```

The `.env` file in the project root is special: Compose automatically reads it for variable interpolation in the compose file itself (not inside containers). To use it for interpolation:

```yaml
services:
  db:
    image: postgres:${POSTGRES_VERSION:-16}-alpine
```

Always add `.env.local` and any secret-containing env files to `.gitignore`. Commit a `.env.example` with placeholder values.

## Profiles for Optional Services

Profiles let you define services that only start when explicitly requested. This keeps the default `docker compose up` fast while allowing optional tools on demand.

```yaml
services:
  app:
    build: .
    ports:
      - "8080:8080"

  db:
    image: postgres:16-alpine

  mailhog:
    image: mailhog/mailhog
    ports:
      - "8025:8025"
    profiles:
      - debug

  pgadmin:
    image: dpage/pgadmin4
    ports:
      - "5050:80"
    environment:
      PGADMIN_DEFAULT_EMAIL: admin@local.dev
      PGADMIN_DEFAULT_PASSWORD: admin
    profiles:
      - debug
```

Start with profiles: `docker compose --profile debug up`. Services without a `profiles` key always start. Services with a profile only start when that profile is activated.

## Override Files

The file `docker-compose.override.yml` is automatically merged with `docker-compose.yml` when you run `docker compose up`. Use this for developer-specific settings that should not affect CI or production builds.

```yaml
# docker-compose.override.yml
services:
  app:
    build:
      target: development
    volumes:
      - ./src:/app/src
    environment:
      DEBUG: "true"
    ports:
      - "9229:9229"  # debugger port
```

For CI, explicitly specify only the base file: `docker compose -f docker-compose.yml up`. This skips the override file entirely.

For more complex setups, chain multiple files:

```bash
docker compose -f docker-compose.yml -f docker-compose.ci.yml up
```

Later files override earlier ones. This lets you maintain a base config, a dev override, and a CI override without duplicating shared service definitions.

## Useful Commands

```bash
# Start in background
docker compose up -d

# Rebuild images and start
docker compose up --build

# View logs for a specific service
docker compose logs -f app

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

# Tear down everything including volumes
docker compose down -v

# View resource usage
docker compose top
```

