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.
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 |