devops

Docker Compose for Development Environments: Best Practices 2026

Master Docker Compose for local development with hot reload, multi-service orchestration, debugging setup, and production parity. Complete guide for 2026.

June 26, 2026·6 min read·
#docker#docker-compose#dev-environment#containers#hot-reload

Introduction

Every developer has experienced the "it works on my machine" problem. Docker Compose solves this by giving every team member an identical, reproducible environment — no more missing dependencies, version conflicts, or OS-specific quirks.

In 2026, Docker Compose has matured significantly. With Compose v2.30+, GPU support, Watch mode, and include for multi-file projects, it's the backbone of modern local development workflows.

Here's what we'll cover:

  • Structuring your Compose files for real projects
  • Hot reload with bind mounts and Watch mode
  • Multi-service orchestration (app + database + cache + queue)
  • Debugging inside containers with VS Code
  • Production parity and environment management
  • Performance tuning for fast builds

Let's build a development environment you'll actually enjoy using.

Project Structure: Where Do Files Go?

A clean project structure is half the battle. Here's what I recommend:

project/
├── docker-compose.yml          # Main compose file
├── docker-compose.override.yml # Dev overrides (auto-loaded)
├── .env                         # Environment variables
├── backend/
│   ├── Dockerfile
│   ├── Dockerfile.dev
│   └── src/
├── frontend/
│   ├── Dockerfile
│   └── src/
└── docker/
    ├── nginx/
    │   └── default.conf
    └── postgres/
        └── init.sql

The key insight: docker-compose.override.yml is automatically merged with docker-compose.yml when both exist. This means your base file defines production-like configuration, and the override adds development-specific tweaks like bind mounts, exposed ports, and debug flags.

docker-compose.yml (Base Configuration)

version: "3.9"

services:
  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile
    image: myapp-backend:latest
    environment:
      - NODE_ENV=production
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_started
    networks:
      - app-network

  postgres:
    image: postgres:17-alpine
    environment:
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: ${DB_NAME}
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./docker/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - app-network

  redis:
    image: redis:7-alpine
    networks:
      - app-network

networks:
  app-network:
    driver: bridge

volumes:
  pgdata:

This base configuration is clean and close to what you'd run in production. Now let's add the development override.

docker-compose.override.yml (Development Magic)

services:
  backend:
    build:
      dockerfile: Dockerfile.dev
    ports:
      - "3000:3000"
      - "9229:9229"  # Node.js debugger
    environment:
      - NODE_ENV=development
      - DEBUG=myapp:*
    volumes:
      - ./backend/src:/app/src          # Hot reload source
      - ./backend/package.json:/app/package.json
      - /app/node_modules               # Anonymous volume
    command: npm run dev
    develop:
      watch:
        - action: sync
          path: ./backend/src
          target: /app/src
        - action: rebuild
          path: ./backend/package.json

  postgres:
    ports:
      - "5432:5432"

  redis:
    ports:
      - "6379:6372"

When you run docker compose up, both files merge automatically. No need for -f flags.

Hot Reload: Bind Mounts vs Watch Mode

Method 1: Bind Mounts (Tried and True)

volumes:
  - ./backend/src:/app/src
command: npm run dev  # Uses nodemon/ts-node-dev

Works with any language that has a file watcher. The host and container share the same filesystem — when you save a file on your host, it instantly appears inside the container.

Downsides: Slow on macOS (osxfs overhead). Large node_modules can cause issues.

Method 2: Compose Watch (New in 2026)

develop:
  watch:
    - action: sync
      path: ./backend/src
      target: /app/src
    - action: rebuild
      path: ./backend/package.json
    - action: sync+restart
      path: ./backend/tsconfig.json
      target: /app/tsconfig.json

Available actions:

  • sync: Copy changed files into the running container (fast, no restart)
  • rebuild: Rebuild the image and restart the container
  • sync+restart: Copy files then restart the container

This is significantly faster than bind mounts on macOS because it copies only changed files instead of sharing the entire filesystem.

Multi-Service Orchestration: A Real Example

Here's a typical web application stack:

services:
  nginx:
    image: nginx:1.27-alpine
    ports:
      - "80:80"
    volumes:
      - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - backend
      - frontend

  backend:
    build: ./backend
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_started
    environment:
      DATABASE_URL: postgresql://${DB_USER}:${DB_PASSWORD}@postgres:5432/${DB_NAME}
      REDIS_URL: redis://redis:6379

  frontend:
    build: ./frontend
    environment:
      API_URL: http://backend:3000

  worker:
    build: ./backend
    command: npm run worker
    depends_on:
      - redis
      - postgres

  postgres: ...
  redis: ...

Pro tip: Use condition: service_healthy instead of bare depends_on. This waits for the database to actually accept connections, not just for the container to start.

Debugging Inside Containers with VS Code

Add this to your backend's Dockerfile.dev:

FROM node:22-alpine
WORKDIR /app
RUN npm install -g nodemon
EXPOSE 3000 9229
CMD ["nodemon", "--inspect=0.0.0.0:9229", "src/index.js"]

Then in VS Code, create .vscode/launch.json:

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "attach",
      "name": "Docker: Attach to Backend",
      "port": 9229,
      "address": "localhost",
      "localRoot": "${workspaceFolder}/backend/src",
      "remoteRoot": "/app/src",
      "restart": true
    }
  ]
}

Set breakpoints, hit F5, and you're debugging code running inside Docker. The localRoot and remoteRoot mapping ensures VS Code can match source files.

Environment Management

.env file

DB_USER=myapp
DB_PASSWORD=dev-secret-123
DB_NAME=myapp_dev
JWT_SECRET=local-dev-only

Per-environment overrides

# Start with dev configuration
docker compose up

# Override with staging
docker compose -f docker-compose.yml -f docker-compose.staging.yml up

# Production (no override)
docker compose -f docker-compose.yml up

Security note: Never commit .env with real secrets. Use .env.example as a template:

DB_USER=myapp
DB_PASSWORD=change_me
JWT_SECRET=change_me

Performance Tuning

1. Use BuildKit

DOCKER_BUILDKIT=1 docker compose build

Or set permanently:

echo '{"features":{"buildkit":true}}' | sudo tee /etc/docker/daemon.json

2. Layer Caching Strategy

# Bad: invalidates cache on every source change
COPY . .
RUN npm install

# Good: install deps first, copy source later
COPY package.json package-lock.json ./
RUN npm ci
COPY src/ ./src/

3. .dockerignore

node_modules
.git
.env
*.log
dist
coverage

This dramatically speeds up the build context transfer.

4. Use Alpine Images

ImageSize
node:221.1 GB
node:22-alpine130 MB

The smaller image means faster pulls, less disk usage, and quicker CI builds.

Common Pitfalls & Solutions

"Port already in use"

# Find what's using port 5432
sudo lsof -i :5432
# Stop local postgres
sudo systemctl stop postgresql

"Changes not reflecting"

# Force rebuild without cache
docker compose build --no-cache backend
docker compose up -d

"node_modules disappears after npm install"

volumes:
  - ./backend:/app
  - /app/node_modules  # Anonymous volume prevents overwrite

The anonymous volume at /app/node_modules takes priority over the bind mount for that specific path.

Conclusion

Docker Compose in 2026 is more than just a "dev tool" — it's the foundation of a professional development workflow. The combination of base + override files, Watch mode, and proper debugging setup means you can onboard new developers in minutes, not days.

Key takeaways:

  1. Split base and override filesdocker-compose.yml + docker-compose.override.yml keeps dev and prod concerns separate
  2. Use Compose Watch on macOS for faster hot reload than bind mounts
  3. Always add healthchecksdepends_on without them is a race condition
  4. Set up VS Code remote debugging — you'll thank yourself later
  5. Layer your Dockerfiles — copy package.json first, then source

The next step: take this Compose setup into CI with docker compose -f docker-compose.yml -f docker-compose.ci.yml up --abort-on-container-exit for integration tests. But that's a topic for another article.

#docker#docker-compose#dev-environment#containers#hot-reload
D
DevToCashAuthor

Senior DevOps/SRE Engineer · 10+ years · Professional Trader (IDX, Crypto, US Equities)

I write about real infrastructure patterns and trading strategies I use in production and in live markets. No courses, no affiliate hype — just documentation of what actually works.

More about me →