Dev Mode with Bind‑Mounts + Hot Reload (HMR): The Complete Guide Print

  • 0

The Quick Guide to Dev Mode: Bind Mounts, HMR, and Instant Refresh : Executive Summary

What it is (in one line):
A dev setup where your app runs in a controlled runtime (often a container) while your source lives on your machine via a bind-mount; a dev server watches those files and performs Hot Module Replacement (HMR) so code changes instantly refresh in the browser or API without a full rebuild or process restart.

Why it matters:

  • Instant code refresh: Save → HMR swaps only the changed modules → UI/API updates in ~sub-second, usually without losing app state.

  • Production parity: Same OS/libs/runtime as prod (when containerized), reducing "works on my machine" issues.

  • Developer flow: Keep local IDE, linters, and debuggers; no slow rebuild loops.

  • Team consistency: Uniform onboarding and repeatable environments across macOS/Windows/Linux.

When to use it:

  • Day-to-day development on web frontends (React/Vue/Svelte) and Node/Python/Go services that support live reload/HMR.

  • Multi-dev teams needing consistent environments and rapid iteration.

  • Microservices/monorepos where fast feedback is critical.

How the instant refresh actually works:

  1. Bind-mount shares your local src/ into the runtime (/app).

  2. File watcher detects a change (e.g., save a React component).

  3. HMR pipeline recompiles just the affected module(s), pushes updates over a WebSocket to the browser or reloads the server route/function.

  4. State preservation: The HMR client hot-swaps modules so UI or in-memory state often survives (e.g., form inputs, component state).

What's required:

  • A dev server with HMR (e.g., Vite, Next.js, Nuxt, SvelteKit) or an equivalent live-reload tool (nodemon, air, reflex, etc.).

  • Bind-mounts for source; a separate cached volume for dependencies (e.g., node_modules) for speed.

  • Open dev ports (app + HMR WebSocket) and optional debugger port.

Performance cheatsheet:

  • Mount only what you need: src/, public/, configs; do not bind-mount node_modules.

  • Ignore heavy paths in watchers (.git, dist, coverage).

  • On macOS/Windows, prefer VirtioFS / WSL2; enable watch-polling only if native FS events are unreliable.

  • Keep images slim and turn on framework-specific optimizations (e.g., Vite optimizeDeps).

Security/ops notes:

  • Run as non-root and align UID/GID to avoid permission issues.

  • Mount least privilege: don't expose secrets; use .env.development only in dev.

  • Keep dev and prod images distinct (no dev tools in prod).

Common pitfalls (and quick fixes):

  • Slow refreshes: Move deps to a named volume, reduce watched paths.

  • No change detection: Switch to polling or fix host FS notifications.

  • State lost on edit: Ensure true HMR is enabled; avoid full page reload fallbacks.

  • Permission errors: Set user: 1000:1000 (or your UID/GID) or fix via entrypoint chown.

Outcome:
A tight feedback loop with instant code refresh, consistent environments, and fewer integration surprises--so you ship faster with higher confidence.


Table of Contents

  1. Concepts & Terminology

  2. When to Use Dev Mode vs. Prod Mode

  3. Reference Architecture

  4. Folder Layout (Example)

  5. Docker Compose: Dev‑Oriented Setup

  6. Frontend Example (Next.js) with Fast Refresh

  7. Backend Example (NestJS) with Watch Mode

  8. Reverse Proxy (Caddy) for Stable URLs

  9. Performance Tuning for Bind‑Mounts

  10. Troubleshooting File Watching & HMR

  11. Security Considerations in Dev

  12. Testing, Linting, and Type‑Checking in Dev

  13. Switching Between Dev and Prod

  14. Common Pitfalls & Anti‑Patterns

  15. FAQ

  16. Appendix: Useful Commands & Sysctls


1) Concepts & Terminology

  • Bind‑mount: A Docker volume type that maps a host directory (your source code) into the container's filesystem at a specified path. Edits on the host appear instantly inside the container.

  • HMR (Hot Module Replacement) / Fast Refresh: A feature of dev servers/build tools that reloads only the changed modules (or restarts the app) when files change--resulting in near‑instant updates without full rebuilds or losing in‑memory state (when safe).

  • Watchers: File‑system watch processes (e.g., Webpack/Next.js watcher, Chokidar, nodemon, Nest's --watch) that detect file changes and trigger recompiles or restarts.

  • Dev image: A Docker image (or multi‑stage target) that contains dev dependencies and tooling (e.g., TypeScript, linters, test runners, HMR tooling). Often larger than prod images.

  • Named volume for node_modules: Keep dependencies inside the container to avoid host/container OS mismatch and speed issues, while bind‑mounting only the source code.

Why this matters: Dev speed depends on tight feedback loops. Bind‑mounts + HMR let you edit locally and see changes instantly in containers, while keeping runtime parity with production (operating system, libc, OpenSSL, etc.).


2) When to Use Dev Mode vs. Prod Mode

  • Dev Mode

    • You are actively editing code and want instant feedback.

    • You need dev‑only tooling (TypeScript watch, React Fast Refresh, source maps).

    • You accept a heavier image and looser security constraints on your local machine.

  • Prod Mode

    • Immutable images (no bind‑mounts), reproducible builds.

    • Compiled output only (e.g., node dist/, next start).

    • Minimum attack surface: dev tools removed, non‑root user, read‑only FS, etc.


3) Reference Architecture

A typical local dev stack:

  • Frontend: Next.js dev server with Fast Refresh (port 3000)

  • Backend: NestJS in watch mode or HMR (port 3001)

  • Reverse Proxy: Caddy/Nginx routing / to frontend and /api to backend

  • Database/Cache (optional): Postgres/Redis (prod‑like, but with dev configs)

The proxy keeps your browser URL stable while services reload behind the scenes.


4) Folder Layout (Example)

acme-app/
├─ apps/
│  ├─ web/            # Next.js frontend
│  └─ api/            # NestJS backend
├─ deploy/
│  ├─ Caddyfile       # reverse proxy routes
│  └─ docker/         # Dockerfiles (multi-stage) for dev & prod
├─ docker-compose.yml
├─ package.json       # workspaces (optional)
└─ README.md

This layout keeps sources isolated and makes it easy to bind‑mount per app.


5) Docker Compose: Dev‑Oriented Setup

Key ideas:

  • Bind‑mount only source code directories.

  • Keep node_modules in named volumes inside containers.

  • Enable robust file watching using environment flags.

  • Expose ports for dev servers; put a proxy in front for stable routing.

# docker-compose.yml (DEV)
version: "3.9"

services:
  web:
    build:
      context: .
      dockerfile: deploy/docker/Dockerfile.web
      target: dev
    command: npm run dev --workspace apps/web
    working_dir: /workspace
    volumes:
      - ./:/workspace:rw
      - web_node_modules:/workspace/apps/web/node_modules
    environment:
      - NODE_ENV=development
      - NEXT_TELEMETRY_DISABLED=1
      # Ensure watchers work reliably in Docker Desktop (macOS/Windows):
      - CHOKIDAR_USEPOLLING=1
      - CHOKIDAR_INTERVAL=150
      - WATCHPACK_POLLING=true
    ports:
      - "3000:3000"
    depends_on:
      - api

  api:
    build:
      context: .
      dockerfile: deploy/docker/Dockerfile.api
      target: dev
    command: npm run start:dev --workspace apps/api
    working_dir: /workspace
    volumes:
      - ./:/workspace:rw
      - api_node_modules:/workspace/apps/api/node_modules
    environment:
      - NODE_ENV=development
      - TS_NODE_TRANSPILE_ONLY=1
      - CHOKIDAR_USEPOLLING=1
    ports:
      - "3001:3001"

  caddy:
    image: caddy:latest
    volumes:
      - ./deploy/Caddyfile:/etc/caddy/Caddyfile:ro
    ports:
      - "80:80"
      - "443:443"
    depends_on:
      - web
      - api

volumes:
  web_node_modules:
  api_node_modules:

Why mount the repo root (.:/workspace) rather than only apps/web and apps/api? It simplifies monorepos (shared packages, ESLint/TS configs). If performance is poor on macOS/Windows, bind‑mount only the app folders and keep the rest in the image.

Multi‑stage Dev Dockerfiles (conceptual):

# deploy/docker/Dockerfile.web
FROM node:20-slim AS base
WORKDIR /workspace

FROM base AS dev
# Install only once into image cache (optional):
COPY package*.json .
RUN npm i -g npm@latest && npm ci || true  # optional; in dev you can rely on volume
CMD ["npm","run","dev","--workspace","apps/web"]
# deploy/docker/Dockerfile.api
FROM node:20-slim AS base
WORKDIR /workspace

FROM base AS dev
COPY package*.json .
RUN npm i -g npm@latest && npm ci || true
CMD ["npm","run","start:dev","--workspace","apps/api"]

In dev, you can skip copying sources in the Dockerfile since bind‑mounts will provide them at runtime. In prod, you'll copy sources and build artifacts into a minimal image.


6) Frontend Example (Next.js) with Fast Refresh

package.json scripts (excerpt):

{
  "scripts": {
    "dev:web": "next dev -p 3000",
    "build:web": "next build",
    "start:web": "next start -p 3000"
  }
}

How HMR works here:

  • Next.js watches files under apps/web.

  • On change, it recompiles the affected module and updates the browser via WebSocket.

  • Component state is preserved when it's safe to do so.

Gotchas:

  • Editing next.config.js or changing env vars may trigger a full reload.

  • Very large dependency graphs can slow down; consider on‑demand entries and code‑splitting.

Verification steps:

  1. docker compose up --build.

  2. Open http://localhost/ (proxied by Caddy) or http://localhost:3000/ directly.

  3. Edit a React component; observe instant browser update without full refresh.


7) Backend Example (NestJS) with Watch Mode

package.json scripts (excerpt):

{
  "scripts": {
    "start:dev": "nest start --watch",
    "build:api": "nest build",
    "start:prod": "node dist/main.js"
  }
}

How watch works:

  • NestJS uses chokidar under the hood to detect changes.

  • On change, it recompiles TypeScript and restarts the app.

Optional HMR:

  • You can enable Webpack HMR for NestJS to hot‑swap certain modules without full restart, but many teams find regular watch‑restart sufficient and simpler.

Verification:

  1. curl http://localhost/api/health (through proxy) should return a health payload.

  2. Change a controller message; repeat the curl; the response updates immediately.


8) Reverse Proxy (Caddy) for Stable URLs

Minimal Caddyfile (dev):

{
  auto_https off
}

:80 {
  @api path /api/*
  route @api {
    uri strip_prefix /api
    reverse_proxy api:3001
  }

  route {
    reverse_proxy web:3000
  }
}
  • Hit http://localhost/ for the frontend.

  • Hit http://localhost/api/... for the backend (forwarded to api:3001).

  • Your browser/proxy URL stays stable even during hot reloads.


9) Performance Tuning for Bind‑Mounts

  • Keep node_modules inside the container via named volumes.

  • Reduce bind‑mounted scope on macOS/Windows: mount only apps/web and apps/api.

  • Enable polling (Docker Desktop): CHOKIDAR_USEPOLLING=1, WATCHPACK_POLLING=true.

  • Increase Linux inotify limits (see Appendix) to prevent "too many open files" errors.

  • Exclude heavy directories from watchers (.git, .next, dist, coverage).

  • Use fast disks (NVMe) and avoid antivirus scanning your workspace (allow‑list if possible).


10) Troubleshooting File Watching & HMR

Symptoms & Fixes

  • Edits not detected

    • Ensure env flags for polling are set.

    • Verify the path you bind‑mounted matches the watcher's root.

    • On Linux, increase inotify watches.

  • Full page reloads instead of HMR

    • Some edits (config/env) require full reload by design.

    • Make sure you're running the dev server (next dev, not next start).

  • Very slow rebuilds

    • Limit the bind‑mounted scope.

    • Add experimental: { workerThreads: true } where supported, or parallelize TS builds.

  • Native module errors

    • Reinstall deps inside the container; don't reuse host node_modules.

  • Permissions/UID issues

    • Align container user with host UID/GID (or use --user); avoid root‑owned artifacts on host.


11) Security Considerations in Dev

  • Dev images are bigger and expose tooling--use only on trusted machines.

  • Keep secrets out of the repo; use .env and never commit it.

  • Don't publish dev ports beyond localhost if you're on untrusted networks.

  • Avoid using real production credentials; use sandbox/test accounts.


12) Testing, Linting, and Type‑Checking in Dev

  • Run watch‑mode linters and tests in separate containers or as npm scripts:

{
  "scripts": {
    "lint": "eslint .",
    "typecheck": "tsc -p tsconfig.json --noEmit",
    "test:watch": "vitest --watch"
  }
}
  • Optionally, mount your repo into a dedicated tools container that runs these tasks continuously.


13) Switching Between Dev and Prod

  • Dev: bind‑mounts + watchers (npm run dev, nest --watch).

  • Prod: copy compiled artifacts into a slim image; no bind‑mounts; next start or node dist/main.js.

Compose override pattern:

  • docker-compose.yml = baseline (prod‑like)

  • docker-compose.dev.yml = adds bind‑mounts + dev commands

  • Run docker compose -f docker-compose.yml -f docker-compose.dev.yml up for dev.


14) Common Pitfalls & Anti‑Patterns

  • Mounting host node_modules → native binary mismatch, slow I/O. Prefer container volume.

  • No proxy → constantly changing ports/URLs; break frontend→backend calls.

  • Relying on host global tools inside container → inconsistent versions. Pin versions in images.

  • Watching the entire repo on macOS/Windows → slow. Mount only needed subfolders.

  • Editing env/config and expecting HMR → some changes require a restart; this is normal.


15) FAQ

Q: Do I need Docker for HMR?
A: No--HMR works natively. Docker adds environment parity (OS/libs), consistent tooling, and easy orchestration.

Q: Why does macOS/Windows need polling flags?
A: Docker Desktop's file sharing can miss filesystem events. Polling ensures reliability at the cost of some CPU.

Q: Can I use Vite instead of Next/Webpack?
A: Yes. The same principles apply: bind‑mount sources, run the dev server in the container, expose the port.

Q: Should I commit dev Dockerfiles?
A: Yes--checked‑in dev/prod Dockerfiles and compose files make onboarding reproducible.

Q: How do I debug inside the container?
A: Expose debug ports (e.g., 9229 for Node) and attach your editor's debugger to the container process.


16) Appendix: Useful Commands & Sysctls

Compose lifecycle

docker compose up --build
docker compose down
docker compose logs -f web

Linux inotify (increase watch limits)

# Temporary (until reboot):
sudo sysctl fs.inotify.max_user_watches=524288
sudo sysctl fs.inotify.max_user_instances=1024

# Persist across reboots (e.g., /etc/sysctl.d/99-inotify.conf):
fs.inotify.max_user_watches=524288
fs.inotify.max_user_instances=1024

Reinstall deps inside container (avoid host/node mismatch)

# In a shell inside the container
npm ci

Example health checks

# Frontend (Next.js)
curl -I http://localhost:3000

# Backend (NestJS)
curl http://localhost:3001/health

Debugging file events

# See changes detected by chokidar (run a small watcher script) or
# use inotify-tools on Linux:
inotifywait -m -r /workspace/apps/web -e modify,create,delete

Summary

Dev mode with bind‑mounts + HMR lets you develop inside Docker without sacrificing iteration speed. The best results come from: keeping dependencies in container volumes, enabling reliable file watching (polling if needed), using a reverse proxy for stable URLs, and clearly separating dev images/commands from prod images/commands. This pattern is language‑agnostic and works equally well with modern JS/TS frameworks, Python (uvicorn reload), Go (air/reflex), Ruby on Rails (Spring), and more.


Was this answer helpful?

« Back