Docker Compose simplifies running multi-container applications locally. Here's how to set up an efficient development environment.
Basic Setup#
1# docker-compose.yml
2version: '3.8'
3
4services:
5 app:
6 build:
7 context: .
8 dockerfile: Dockerfile.dev
9 ports:
10 - '3000:3000'
11 volumes:
12 - .:/app
13 - /app/node_modules
14 environment:
15 - NODE_ENV=development
16 - DATABASE_URL=postgres://user:password@db:5432/myapp
17 depends_on:
18 - db
19 - redis
20
21 db:
22 image: postgres:15-alpine
23 ports:
24 - '5432:5432'
25 environment:
26 POSTGRES_USER: user
27 POSTGRES_PASSWORD: password
28 POSTGRES_DB: myapp
29 volumes:
30 - postgres_data:/var/lib/postgresql/data
31
32 redis:
33 image: redis:7-alpine
34 ports:
35 - '6379:6379'
36
37volumes:
38 postgres_data:1# Dockerfile.dev
2FROM node:20-alpine
3
4WORKDIR /app
5
6# Install dependencies first (cached layer)
7COPY package*.json ./
8RUN npm install
9
10# Copy source code
11COPY . .
12
13# Expose port
14EXPOSE 3000
15
16# Start with hot reload
17CMD ["npm", "run", "dev"]Development with Hot Reload#
1# docker-compose.yml for hot reload
2version: '3.8'
3
4services:
5 frontend:
6 build:
7 context: ./frontend
8 dockerfile: Dockerfile.dev
9 ports:
10 - '3000:3000'
11 volumes:
12 - ./frontend:/app
13 - /app/node_modules
14 environment:
15 - CHOKIDAR_USEPOLLING=true # For file watching in Docker
16 - WATCHPACK_POLLING=true # For webpack
17
18 backend:
19 build:
20 context: ./backend
21 dockerfile: Dockerfile.dev
22 ports:
23 - '8080:8080'
24 volumes:
25 - ./backend:/app
26 - /app/node_modules
27 environment:
28 - NODE_ENV=development
29 command: npm run dev # nodemon or tsx watch1# frontend/Dockerfile.dev (Vite)
2FROM node:20-alpine
3
4WORKDIR /app
5
6COPY package*.json ./
7RUN npm install
8
9COPY . .
10
11EXPOSE 3000
12
13CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]Full Stack Example#
1# docker-compose.yml - Full stack app
2version: '3.8'
3
4services:
5 nginx:
6 image: nginx:alpine
7 ports:
8 - '80:80'
9 volumes:
10 - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
11 depends_on:
12 - frontend
13 - backend
14
15 frontend:
16 build:
17 context: ./frontend
18 dockerfile: Dockerfile.dev
19 volumes:
20 - ./frontend:/app
21 - /app/node_modules
22 environment:
23 - VITE_API_URL=http://localhost/api
24
25 backend:
26 build:
27 context: ./backend
28 dockerfile: Dockerfile.dev
29 volumes:
30 - ./backend:/app
31 - /app/node_modules
32 environment:
33 - DATABASE_URL=postgres://user:password@db:5432/myapp
34 - REDIS_URL=redis://redis:6379
35 - JWT_SECRET=dev-secret
36 depends_on:
37 db:
38 condition: service_healthy
39 redis:
40 condition: service_started
41
42 db:
43 image: postgres:15-alpine
44 environment:
45 POSTGRES_USER: user
46 POSTGRES_PASSWORD: password
47 POSTGRES_DB: myapp
48 volumes:
49 - postgres_data:/var/lib/postgresql/data
50 - ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql
51 healthcheck:
52 test: ['CMD-SHELL', 'pg_isready -U user -d myapp']
53 interval: 5s
54 timeout: 5s
55 retries: 5
56
57 redis:
58 image: redis:7-alpine
59 volumes:
60 - redis_data:/data
61
62 mailhog:
63 image: mailhog/mailhog
64 ports:
65 - '8025:8025' # Web UI
66 - '1025:1025' # SMTP
67
68volumes:
69 postgres_data:
70 redis_data:1# nginx/nginx.conf
2events {
3 worker_connections 1024;
4}
5
6http {
7 upstream frontend {
8 server frontend:3000;
9 }
10
11 upstream backend {
12 server backend:8080;
13 }
14
15 server {
16 listen 80;
17
18 location / {
19 proxy_pass http://frontend;
20 proxy_http_version 1.1;
21 proxy_set_header Upgrade $http_upgrade;
22 proxy_set_header Connection "upgrade";
23 }
24
25 location /api {
26 proxy_pass http://backend;
27 proxy_set_header Host $host;
28 proxy_set_header X-Real-IP $remote_addr;
29 }
30 }
31}Environment Configuration#
1# docker-compose.yml with .env support
2version: '3.8'
3
4services:
5 app:
6 build: .
7 env_file:
8 - .env
9 - .env.local
10 environment:
11 - NODE_ENV=development
12 # Override specific variables
13 - DEBUG=${DEBUG:-false}1# .env
2DATABASE_URL=postgres://user:password@db:5432/myapp
3REDIS_URL=redis://redis:6379
4API_KEY=your-api-key
5
6# .env.local (not committed)
7API_KEY=my-local-api-key
8DEBUG=trueService Profiles#
1# docker-compose.yml with profiles
2version: '3.8'
3
4services:
5 app:
6 build: .
7 ports:
8 - '3000:3000'
9
10 db:
11 image: postgres:15-alpine
12 profiles:
13 - db
14 - full
15 ports:
16 - '5432:5432'
17
18 redis:
19 image: redis:7-alpine
20 profiles:
21 - cache
22 - full
23
24 elasticsearch:
25 image: elasticsearch:8.9.0
26 profiles:
27 - search
28 - full
29 environment:
30 - discovery.type=single-node
31
32 monitoring:
33 image: grafana/grafana
34 profiles:
35 - monitoring
36 ports:
37 - '3001:3000'# Run with specific profiles
docker compose --profile db up
docker compose --profile full up
docker compose --profile db --profile cache upDevelopment Scripts#
1// package.json
2{
3 "scripts": {
4 "docker:up": "docker compose up -d",
5 "docker:down": "docker compose down",
6 "docker:logs": "docker compose logs -f",
7 "docker:build": "docker compose build --no-cache",
8 "docker:shell": "docker compose exec app sh",
9 "docker:db": "docker compose exec db psql -U user -d myapp",
10 "docker:reset": "docker compose down -v && docker compose up -d"
11 }
12}1# Makefile
2.PHONY: up down logs build shell db reset
3
4up:
5 docker compose up -d
6
7down:
8 docker compose down
9
10logs:
11 docker compose logs -f
12
13build:
14 docker compose build --no-cache
15
16shell:
17 docker compose exec app sh
18
19db:
20 docker compose exec db psql -U user -d myapp
21
22reset:
23 docker compose down -v
24 docker compose up -d
25
26migrate:
27 docker compose exec app npm run db:migrate
28
29seed:
30 docker compose exec app npm run db:seedDebugging in Docker#
1# docker-compose.yml with debugging
2version: '3.8'
3
4services:
5 app:
6 build:
7 context: .
8 dockerfile: Dockerfile.dev
9 ports:
10 - '3000:3000'
11 - '9229:9229' # Node.js debugger
12 volumes:
13 - .:/app
14 - /app/node_modules
15 command: node --inspect=0.0.0.0:9229 src/index.js1// .vscode/launch.json
2{
3 "version": "0.2.0",
4 "configurations": [
5 {
6 "name": "Docker: Attach to Node",
7 "type": "node",
8 "request": "attach",
9 "port": 9229,
10 "address": "localhost",
11 "localRoot": "${workspaceFolder}",
12 "remoteRoot": "/app",
13 "restart": true
14 }
15 ]
16}Health Checks and Dependencies#
1version: '3.8'
2
3services:
4 app:
5 build: .
6 depends_on:
7 db:
8 condition: service_healthy
9 redis:
10 condition: service_healthy
11 migrations:
12 condition: service_completed_successfully
13
14 migrations:
15 build: .
16 command: npm run db:migrate
17 depends_on:
18 db:
19 condition: service_healthy
20
21 db:
22 image: postgres:15-alpine
23 healthcheck:
24 test: ['CMD-SHELL', 'pg_isready -U user']
25 interval: 5s
26 timeout: 5s
27 retries: 5
28 start_period: 10s
29
30 redis:
31 image: redis:7-alpine
32 healthcheck:
33 test: ['CMD', 'redis-cli', 'ping']
34 interval: 5s
35 timeout: 3s
36 retries: 5Volume Management#
1version: '3.8'
2
3services:
4 app:
5 volumes:
6 # Named volume for node_modules (faster than bind mount)
7 - node_modules:/app/node_modules
8 # Bind mount for source code
9 - ./src:/app/src
10 # Read-only config
11 - ./config:/app/config:ro
12
13 db:
14 volumes:
15 # Named volume for persistence
16 - postgres_data:/var/lib/postgresql/data
17 # Init scripts (read-only)
18 - ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
19
20volumes:
21 node_modules:
22 postgres_data:# Volume management commands
docker compose down -v # Remove volumes
docker volume ls # List volumes
docker volume rm volume_name # Remove specific volume
docker volume prune # Remove unused volumesOverride Files#
1# docker-compose.yml (base)
2version: '3.8'
3
4services:
5 app:
6 build: .
7 environment:
8 - NODE_ENV=production
9
10# docker-compose.override.yml (automatically applied in dev)
11version: '3.8'
12
13services:
14 app:
15 build:
16 context: .
17 dockerfile: Dockerfile.dev
18 volumes:
19 - .:/app
20 environment:
21 - NODE_ENV=development
22 ports:
23 - '3000:3000'
24
25# docker-compose.prod.yml (for production)
26version: '3.8'
27
28services:
29 app:
30 image: myapp:latest
31 restart: always
32 deploy:
33 replicas: 3# Use specific files
docker compose -f docker-compose.yml -f docker-compose.prod.yml upBest Practices#
Development:
✓ Use volume mounts for hot reload
✓ Cache node_modules in named volumes
✓ Use health checks for dependencies
✓ Separate dev and prod Dockerfiles
Performance:
✓ Use Alpine images when possible
✓ Layer caching for dependencies
✓ Use .dockerignore
✓ Named volumes over bind mounts for dependencies
Organization:
✓ Use profiles for optional services
✓ Environment files for configuration
✓ Override files for environments
✓ Makefile for common commands
Conclusion#
Docker Compose streamlines local development with multi-container applications. Use volume mounts for hot reload, health checks for proper startup order, and profiles for optional services. The investment in a good Compose setup pays off in consistent, reproducible development environments.