Docker Compose for Local Development: Complete Guide
Why Docker Compose for Local Development?
Modern applications rarely run in isolation. A typical web app might need a Node.js server, a PostgreSQL database, a Redis cache, and perhaps an Elasticsearch instance. Docker Compose lets you define all of these services in a single YAML file and start them with one command: docker compose up.
The key benefits for local development are reproducibility (every developer gets the exact same environment), isolation (no conflicting versions of PostgreSQL or Node.js installed globally), and simplicity (new team members run one command instead of following a 20-step setup guide).
As of 2026, Docker Compose V2 is the standard. It is integrated into the Docker CLI as docker compose (no hyphen). The older docker-compose (with hyphen) is deprecated. All examples in this guide use the V2 syntax.
Your First docker-compose.yml
A Compose file defines services, networks, and volumes. Here is a minimal example for a Node.js app with PostgreSQL and Redis.
# docker-compose.yml
services:
app:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "3000:3000"
volumes:
- .:/app
- /app/node_modules # Prevent overwriting container's node_modules
environment:
- DATABASE_URL=postgres://postgres:secret@db:5432/myapp
- REDIS_URL=redis://cache:6379
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
db:
image: postgres:16-alpine
ports:
- "5432:5432"
environment:
POSTGRES_DB: myapp
POSTGRES_USER: postgres
POSTGRES_PASSWORD: secret
volumes:
- pgdata:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
cache:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
pgdata:You can validate the YAML syntax of your Compose file by converting it to JSON with our JSON Formatter (paste the YAML output of docker compose config). For a deeper comparison of YAML and JSON formats, see our YAML vs JSON guide.
Development-Optimized Dockerfile
Your development Dockerfile should prioritize fast rebuilds and hot reloading, not production optimization. Use a separate Dockerfile.dev for development.
# Dockerfile.dev
FROM node:22-alpine
WORKDIR /app
# Install dependencies first (cached layer)
COPY package.json package-lock.json ./
RUN npm ci
# Copy source code (changes frequently, so this layer is rebuilt often)
COPY . .
# Use nodemon or tsx for hot reloading
CMD ["npx", "tsx", "watch", "src/index.ts"]The key optimization is separating package.json installation from source code copying. Docker caches layers, so if your dependencies have not changed, npm ci will be skipped on subsequent builds. For production Dockerfiles with multi-stage builds, see our Docker Container Best Practices guide.
Volumes and Hot Reloading
The most important feature for local development is hot reloading. You want your code changes to be reflected inside the container instantly, without rebuilding the image. Bind mounts make this possible.
services:
app:
volumes:
# Bind mount: host directory → container directory
- .:/app
# Anonymous volume: preserve container's node_modules
# (prevents host node_modules from overwriting container's)
- /app/node_modules
# Named volume for build cache
- build_cache:/app/.next
# For frontend with Vite:
frontend:
volumes:
- ./frontend:/app
- /app/node_modules
environment:
# Vite needs this for HMR to work through Docker
- CHOKIDAR_USEPOLLING=true
- WATCHPACK_POLLING=true
volumes:
build_cache:On macOS and Windows, bind mount performance can be slow because Docker Desktop uses a virtual machine. Docker Desktop 4.x+ includes VirtioFS, which dramatically improves file system performance. Ensure it is enabled in Docker Desktop settings under "General" and "Use VirtioFS."
Docker Compose Watch (2026 Recommended)
Docker Compose 2.22+ introduced docker compose watch, which is now the recommended approach for development. It syncs files, rebuilds on dependency changes, and restarts services automatically.
services:
app:
build: .
develop:
watch:
# Sync source code changes without rebuild
- action: sync
path: ./src
target: /app/src
# Rebuild when dependencies change
- action: rebuild
path: package.json
# Restart on config changes
- action: sync+restart
path: ./config
target: /app/config
# Run with:
# docker compose watchNetworking Between Services
Docker Compose creates a default network for all services in the Compose file. Services can reach each other by their service name as the hostname. For example, the app service can connect to PostgreSQL at db:5432 and Redis at cache:6379.
services:
frontend:
build: ./frontend
ports:
- "3000:3000"
networks:
- frontend-net
api:
build: ./api
ports:
- "4000:4000"
networks:
- frontend-net
- backend-net
db:
image: postgres:16-alpine
networks:
- backend-net
# No ports exposed to host — only accessible from backend-net
networks:
frontend-net:
backend-net:
# frontend can reach api, but NOT db
# api can reach both frontend and db
# db can only be reached from backend-netCustom networks let you isolate services. In this example, the frontend cannot directly access the database, which mirrors a production architecture. For DNS-related configuration, check our DNS Records Explained guide.
Environment Variables and Secrets
Docker Compose supports multiple ways to pass environment variables to your services. For local development, .env files are the most convenient approach.
# .env file (automatically loaded by Docker Compose)
POSTGRES_DB=myapp
POSTGRES_USER=postgres
POSTGRES_PASSWORD=secret
NODE_ENV=development
API_PORT=4000
# docker-compose.yml
services:
api:
build: .
env_file:
- .env # Default env vars
- .env.local # Local overrides (gitignored)
environment:
# Inline vars override env_file
- DEBUG=true
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}Always add .env.local to your .gitignore. Commit a .env.example with placeholder values so new developers know which variables to set. For a deeper dive, read our Environment Variables Guide.
Database Seeding and Migrations
Starting with an empty database is rarely useful for development. Docker Compose makes it easy to seed initial data and run migrations automatically.
services:
db:
image: postgres:16-alpine
volumes:
- pgdata:/var/lib/postgresql/data
# Files in this directory run on first container creation
- ./db/init.sql:/docker-entrypoint-initdb.d/01-init.sql
- ./db/seed.sql:/docker-entrypoint-initdb.d/02-seed.sql
# Run migrations as a one-off service
migrate:
build: .
command: npx prisma migrate deploy
depends_on:
db:
condition: service_healthy
profiles:
- setup # Only runs when explicitly requested
# Run setup:
# docker compose --profile setup up migrate
# Reset database (destroy and recreate):
# docker compose down -v # -v removes named volumes
# docker compose upThe profiles feature is useful for services that should not run by default. Migration services, test runners, and one-off scripts can be placed in profiles and invoked only when needed.
Debugging Inside Containers
Debugging containerized applications requires exposing debug ports and configuring your IDE to attach to the container's debugger.
services:
app:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "3000:3000"
- "9229:9229" # Node.js debug port
command: node --inspect=0.0.0.0:9229 src/index.js
# Or for tsx:
# command: npx tsx --inspect=0.0.0.0:9229 watch src/index.ts
# VS Code launch.json for attaching to container:
# {
# "type": "node",
# "request": "attach",
# "name": "Docker: Attach",
# "port": 9229,
# "address": "localhost",
# "localRoot": "${workspaceFolder}",
# "remoteRoot": "/app",
# "restart": true
# }
# Shell into a running container for manual debugging:
# docker compose exec app sh
# docker compose exec db psql -U postgres -d myappThe docker compose exec command is invaluable for troubleshooting. You can open a shell, run database queries, check logs, or test network connectivity from inside any container.
Essential Docker Compose Commands
| Command | Purpose |
|---|---|
| docker compose up -d | Start all services in detached mode |
| docker compose down | Stop and remove containers (keeps volumes) |
| docker compose down -v | Stop, remove containers AND volumes (full reset) |
| docker compose logs -f app | Follow logs for a specific service |
| docker compose exec app sh | Open a shell inside a running container |
| docker compose build --no-cache | Rebuild images without cache |
| docker compose ps | List running services and their ports |
| docker compose watch | Start with file sync and auto-rebuild |
Common Service Recipes
Here are ready-to-use Compose configurations for popular development dependencies.
MongoDB
mongo:
image: mongo:7
ports:
- "27017:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: secret
volumes:
- mongodata:/data/dbElasticsearch
elasticsearch:
image: elasticsearch:8.15.0
environment:
- discovery.type=single-node
- xpack.security.enabled=false
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
ports:
- "9200:9200"
volumes:
- esdata:/usr/share/elasticsearch/dataMailpit (Email Testing)
mailpit:
image: axllent/mailpit
ports:
- "8025:8025" # Web UI
- "1025:1025" # SMTP
# Point your app's SMTP to mailpit:1025
# View caught emails at http://localhost:8025Best Practices Summary
- Use separate Dockerfiles --
Dockerfile.devfor development,Dockerfilefor production. - Use healthchecks on dependencies -- Ensure your app does not start before the database is ready.
- Pin image versions -- Use
postgres:16-alpinenotpostgres:latestfor reproducibility. - Use named volumes for data persistence -- Anonymous volumes are lost when containers are removed.
- Use .dockerignore -- Exclude
node_modules,.git, and build artifacts from the Docker build context. - Commit docker-compose.yml -- This file is documentation for your development environment and should be version-controlled.
- Use Docker Compose Watch -- Prefer
docker compose watchover bind mounts for better cross-platform performance. - Document the setup -- Include a "Getting Started" section in your project README with the exact commands to run.
Validate Your Configuration Files
Docker Compose YAML files are prone to indentation errors. Use our JSON Formatter to validate configuration, or check your environment variable syntax with our developer tools.
Open JSON FormatterRelated Articles
Docker for Developers
Containers, images, and Dockerfile fundamentals.
Docker Container Best Practices
Dockerfile optimization, security, and multi-stage builds.
Environment Variables Guide
Manage env vars across dev, staging, and production.
CI/CD Pipeline Guide
Integrate Docker Compose into your CI/CD workflow.