Skip to content

Hot reload & dev container

TL;DR

  • Edit a file anywhere under apps/web/app/, apps/web/components/, apps/web/public/, or apps/web/next.config.ts.
  • Save.
  • The browser at https://dev.fruitplug.co.uk/ auto-updates within 1–2 seconds.

No container restart needed. No build step needed.

How it works

flowchart LR
  You([You edit page.tsx]) --> Win[Windows FS<br/>C:\...\apps\web\app\page.tsx]
  Win -.bind mount.-> Container[fruitplug-web-dev<br/>/repo/apps/web/app/page.tsx]
  Container -->|webpack polling<br/>800ms interval| NextDev[next dev --webpack]
  NextDev -->|WS HMR| Caddy[Caddy]
  Caddy -->|WS| Browser[Your browser]

Why webpack, not Turbopack

Next.js 16 defaults to Turbopack. Turbopack's file watcher uses Rust's notify crate, which relies on inotify events. On Windows, bind-mounted files into a Linux container don't propagate inotify events through WSL2. Result: edits happen on disk but the watcher never fires.

Webpack's watcher supports explicit polling, which works fine through the bind mount:

// apps/web/next.config.ts
webpack: (config, { dev }) => {
  if (dev) {
    config.watchOptions = {
      poll: 800,                // poll every 800ms
      aggregateTimeout: 200,    // batch changes within 200ms
      ignored: ["**/node_modules/**", "**/.next/**", "**/.git/**"],
    };
  }
  return config;
}

And the Docker command:

CMD ["pnpm", "--filter", "web", "dev", "--webpack", ...]

The dev container

infra/dev.Dockerfile installs dependencies only. Source is bind-mounted at runtime so there's no image rebuild for source changes.

Start manually:

docker run -d \
  --name fruitplug-web-dev \
  --restart unless-stopped \
  -p 127.0.0.1:3030:3000 \
  -v "/c/Users/User/desktop/fruitplug/fruitplug-web/apps/web/app:/repo/apps/web/app" \
  -v "/c/Users/User/desktop/fruitplug/fruitplug-web/apps/web/components:/repo/apps/web/components" \
  -v "/c/Users/User/desktop/fruitplug/fruitplug-web/apps/web/public:/repo/apps/web/public" \
  -v "/c/Users/User/desktop/fruitplug/fruitplug-web/apps/web/next.config.ts:/repo/apps/web/next.config.ts" \
  -v "/c/Users/User/desktop/fruitplug/fruitplug-web/apps/web/tsconfig.json:/repo/apps/web/tsconfig.json" \
  -v "/c/Users/User/desktop/fruitplug/fruitplug-web/apps/web/postcss.config.mjs:/repo/apps/web/postcss.config.mjs" \
  fruitplug-web:dev

Or rebuild the image (when package.json changes):

docker build -f infra/dev.Dockerfile -t fruitplug-web:dev .

Caddy is transparent to HMR

Next.js dev uses WebSockets for HMR on /_next/webpack-hmr. Caddy's reverse_proxy handles WebSocket upgrade automatically — no special config needed.

The one gotcha: Next 16 dev blocks cross-origin requests to dev resources by default. We allow dev.fruitplug.co.uk in next.config.ts:

allowedDevOrigins: ["dev.fruitplug.co.uk", "localhost:3000", "localhost:3030"]

What doesn't hot-reload

  • package.json changes → rebuild the dev image (docker build -f infra/dev.Dockerfile -t fruitplug-web:dev .) then restart the container
  • next.config.ts changes to non-webpack sections (e.g. images.remotePatterns) → restart the container (docker restart fruitplug-web-dev)
  • Environment variables → restart

Troubleshooting

Page doesn't update after save.

Check the container logs:

docker logs fruitplug-web-dev --tail 30

If you don't see a "compiled" line, the poll interval may not have fired yet. Wait 1–2s and request the page again with a cache-busting query string:

curl -s "https://dev.fruitplug.co.uk/?t=$(date +%s)" | grep -o "<target text>"

"Cross-origin request blocked" warning.

Add the offending origin to allowedDevOrigins in next.config.ts and restart the container.

File shows updated inside container but browser still shows old.

Rule out browser cache: hard refresh (Ctrl+Shift+R) or open DevTools → Network → "Disable cache".