Back to Blog
DockerDocker ComposeDevelopmentDevOps

Docker Compose for Local Development

Set up efficient local development with Docker Compose. From basic services to multi-container apps with hot reload.

B
Bootspring Team
Engineering
March 24, 2022
6 min read

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 watch
1# 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=true

Service 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 up

Development 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:seed

Debugging 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.js
1// .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: 5

Volume 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 volumes

Override 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 up

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

Share this article

Help spread the word about Bootspring