Docker Compose for Local Development
Docker Compose transforms local development by providing consistent, reproducible environments. No more "works on my machine" problems.
Basic Setup
Project Structure
textproject/ ├── docker-compose.yml ├── docker-compose.override.yml # Dev-specific overrides ├── .env # Environment variables ├── backend/ │ ├── Dockerfile │ ├── Dockerfile.dev # Development Dockerfile │ └── ... ├── frontend/ │ ├── Dockerfile │ └── ... └── nginx/ └── nginx.conf
docker-compose.yml (Base Configuration)
yamlversion: '3.8' services: backend: build: context: ./backend dockerfile: Dockerfile environment: - DATABASE_URL=postgresql://postgres:password@db:5432/myapp - REDIS_URL=redis://redis:6379 depends_on: db: condition: service_healthy redis: condition: service_started frontend: build: context: ./frontend depends_on: - backend db: image: postgres:15-alpine environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: password POSTGRES_DB: myapp volumes: - postgres_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s timeout: 5s retries: 5 redis: image: redis:7-alpine volumes: - redis_data:/data volumes: postgres_data: redis_data:
docker-compose.override.yml (Development)
yamlversion: '3.8' services: backend: build: dockerfile: Dockerfile.dev volumes: - ./backend:/app - /app/node_modules # Preserve container's node_modules ports: - "3000:3000" - "9229:9229" # Node.js debugger environment: - NODE_ENV=development command: npm run dev frontend: volumes: - ./frontend:/app - /app/node_modules ports: - "5173:5173" environment: - NODE_ENV=development command: npm run dev db: ports: - "5432:5432" # Expose for local tools redis: ports: - "6379:6379"
Development Dockerfiles
Backend Dockerfile.dev
dockerfileFROM node:20-alpine WORKDIR /app # Install dependencies first (better caching) COPY package*.json ./ RUN npm install # Don't copy source - it's mounted as volume # COPY . . # Expose app and debugger ports EXPOSE 3000 9229 CMD ["npm", "run", "dev"]
Frontend Dockerfile.dev
dockerfileFROM node:20-alpine WORKDIR /app COPY package*.json ./ RUN npm install EXPOSE 5173 CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
Hot Reload Configuration
Backend with Nodemon
json// package.json { "scripts": { "dev": "nodemon --inspect=0.0.0.0:9229 src/index.js" } }
json// nodemon.json { "watch": ["src"], "ext": "js,json", "ignore": ["node_modules"], "delay": "500" }
Frontend with Vite
javascript// vite.config.js export default { server: { host: '0.0.0.0', port: 5173, watch: { usePolling: true // Required for Docker on some systems } } }
Database Management
Initialization Scripts
yaml# docker-compose.yml services: db: volumes: - ./db/init:/docker-entrypoint-initdb.d
sql-- db/init/01-schema.sql CREATE TABLE users ( id SERIAL PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, created_at TIMESTAMP DEFAULT NOW() ); -- db/init/02-seed.sql INSERT INTO users (email) VALUES ('test@example.com'), ('admin@example.com');
Database Migrations
yamlservices: migrate: build: ./backend command: npm run migrate depends_on: db: condition: service_healthy profiles: - tools # Only run with --profile tools
bash# Run migrations docker compose --profile tools run migrate
Useful Commands
Daily Development
bash# Start everything docker compose up # Start in background docker compose up -d # View logs docker compose logs -f backend # Restart a service docker compose restart backend # Rebuild after dependency changes docker compose up --build backend # Stop everything docker compose down # Stop and remove volumes (fresh start) docker compose down -v
Debugging
bash# Shell into a running container docker compose exec backend sh # Run a one-off command docker compose run --rm backend npm test # Check service status docker compose ps # View resource usage docker compose top
Environment Variables
.env File
bash# .env POSTGRES_PASSWORD=development_password JWT_SECRET=local_development_secret API_KEY=test_api_key
Using in Compose
yamlservices: backend: environment: - DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@db:5432/myapp - JWT_SECRET=${JWT_SECRET} env_file: - .env # Load all variables from file
Networking
Custom Networks
yamlservices: backend: networks: - frontend - backend frontend: networks: - frontend db: networks: - backend networks: frontend: backend:
Nginx Reverse Proxy
yamlservices: nginx: image: nginx:alpine ports: - "80:80" volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro depends_on: - backend - frontend
nginx# nginx/nginx.conf events {} http { upstream backend { server backend:3000; } upstream frontend { server frontend:5173; } server { listen 80; location /api { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; } location / { proxy_pass http://frontend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; } } }
Performance Tips
Build Caching
dockerfile# Order matters: least-changing first COPY package*.json ./ RUN npm ci COPY tsconfig.json ./ COPY src ./src RUN npm run build
Volume Performance (macOS/Windows)
yamlservices: backend: volumes: - ./backend:/app:cached # Improves performance - /app/node_modules # Named volume for deps
Resource Limits
yamlservices: backend: deploy: resources: limits: cpus: '1' memory: 1G
Conclusion
Docker Compose simplifies local development by providing:
- Consistent environments across team members
- Easy service orchestration
- Hot reload support
- Isolated databases and caches
Start with a simple setup and add complexity as needed. The goal is to make onboarding new developers as simple as docker compose up.