← Workflows

阅读中文版本

Set Up Local Development with Docker Compose

Configure a docker-compose.yml file to run your application, database, and cache services together with hot-reload for development, ensuring consistent environments across team members.

beginner~20 mindockerdevelopmentdevopscontainers

Objective

Create a docker-compose.yml that starts a Node.js app, PostgreSQL, and Redis with persistent volumes, network isolation, and healthchecks for local development.

Environment

  • Docker Desktop 4.0+ or Docker Engine 20.10+
  • docker-compose v2 (docker compose, not docker-compose)
  • Node.js project with pnpm

Steps

1. Create docker-compose.yml

Create a docker-compose.yml in the project root with services for app, postgres, redis

Expected state: File defines three services on a shared network with volume mounts for hot-reload

2. Add environment variables

Create a .env file with DATABASE_URL and REDIS_URL matching the docker-compose service names

Expected state: DATABASE_URL uses 'postgres' as hostname (service name resolves to container IP on the docker network)

3. Add healthchecks

Configure healthcheck on postgres and redis so 'app' waits for them to be ready before starting

Expected state: depends_on with condition: service_healthy on both dependencies

4. Start all services

Run docker compose up -d to start all services in detached mode

Expected state: Three containers running: app (port 3000), postgres (5432), redis (6379)

5. Verify connectivity

Check logs and test the API endpoint

Expected state: App starts without DB connection errors, curl localhost:3000/health returns 200

Decision points

Should you use bind mount or named volume for node_modules?

  • Bind mount (mirror host)
  • Named volume (container-managed)

Named volume for node_modules — bind mounts overwrite the container's node_modules (including compiled native addons) with the host's (empty on macOS/Windows)

Should you include production dependencies in dev container?

  • Yes, full image
  • Minimal + devDependencies

Keep dev dependencies — you need TypeScript, test runners, and linters in the dev container

Expected outcome

One command (docker compose up -d) starts the full stack with hot-reload. Code changes on the host appear in the container instantly.

Verification

Run docker compose ps — all services show 'running' and (healthy). Edit a source file — the app reloads without restarting the container.

Cleanup

docker compose down -v to stop and remove volumes

Why Docker Compose for Development

Docker Compose ensures every developer (and CI) uses identical database versions, Redis configurations, and environment variables. No more “works on my machine” issues.

Complete docker-compose.yml

services:
  app:
    build:
      context: .
      target: development
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgresql://dev:devpass@postgres:5432/myapp
      REDIS_URL: redis://redis:6379
      NODE_ENV: development
    volumes:
      - .:/app                      # hot-reload: mirror host code into container
      - node_modules:/app/node_modules  # named volume prevents host overwrite
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    command: pnpm dev               # your dev server command

  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: dev
      POSTGRES_PASSWORD: devpass
      POSTGRES_DB: myapp
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U dev -d myapp"]
      interval: 5s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redisdata:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5

volumes:
  pgdata:
  redisdata:
  node_modules:

Key Patterns

named volume for node_modules

Without the named volume, Docker bind-mounts the host directory /app which includes node_modules. On macOS/Windows, the host has no node_modules → the container’s compiled modules get overwritten → native addons break.

Healthchecks vs depends_on

Without healthchecks, app starts immediately when postgres container is created (not when ready):

# ❌ Starts app when container created, not when postgres accepts connections
depends_on: [postgres]

# ✅ Waits until pg_isready succeeds
depends_on:
  postgres:
    condition: service_healthy

Hot-Reload with Bind Mount

The bind mount .:/app syncs host → container. Your dev server (vite, tsx watch, nodemon) detects changes instantly.

Common Commands

# Start all services in background
docker compose up -d

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

# Run a one-off command (e.g., migrations)
docker compose exec app pnpm migrate

# Stop everything and remove volumes
docker compose down -v

# Rebuild only the app container after Dockerfile change
docker compose build app

Multi-Stage Dockerfile Reference

FROM node:20-alpine AS development
WORKDIR /app
COPY package*.json ./
RUN pnpm install
COPY . .
CMD ["pnpm", "dev"]

FROM development AS production
RUN pnpm prune --prod
RUN pnpm build
CMD ["node", "dist/server.js"]

Troubleshooting

Symptom Cause Fix
ECONNREFUSED postgres:5432 Healthcheck missing; app starts before DB ready Add healthcheck + condition: service_healthy
"node: not found" in container bind mount overwrites container’s node_modules Add named volume for node_modules
Changes not reflected in container Bind mount working but file watcher not running Ensure pnpm dev (not pnpm start) is the command
Port conflicts Local service already on 3000/5432 Stop local services: docker compose down removes only containers