Fix: React hot reload doesn't work in a Docker container
See why editing a file on the host leaves the container blind, reproduce it with a negative control, and pick the right polling flag for your react-scripts or Vite version.
A React dev server running in a Docker container on Windows compiles fine on boot and serves the page — but editing an existing file never triggers a rebuild. The browser just sits there until the container is restarted by hand.
TL;DR#
On Windows, when Docker Desktop runs on its WSL2 backend, a file edited by a Windows application (your editor) does not raise a filesystem change notification that the Linux side — and therefore the container's watcher — can see. This is a documented WSL2 limitation, not a Docker bug, and it affects any "watch and rebuild" tool, not only React. The fix is to make the watcher poll the filesystem on an interval instead of waiting for an event: CHOKIDAR_USEPOLLING=true for react-scripts ≤ 4, WATCHPACK_POLLING=true for react-scripts 5 (webpack 5), or server.watch.usePolling in vite.config.js for Vite.
The symptom#
docker compose up starts the dev server, the initial build logs Compiled successfully!, and the page loads. Editing an existing component on the host and saving it produces no new build log line and no refresh — the container stays silent until it is restarted by hand.
This is a real, current question: the Stack Overflow thread (26 votes, 40,981 views) describes exactly this setup on Windows — create-react-app, a docker-compose.yml with a bind mount, CHOKIDAR_USEPOLLING=true already set, still no reload.
Why it happens#
This is a documented WSL2 limitation, not something specific to React or webpack. Vite's own configuration docs name it directly: "When running Vite on WSL2, file system watching does not work when a file is edited by Windows applications (non-WSL2 process)... This also applies to running on Docker with a WSL2 backend," linking a WSL2 issue still open on Microsoft's repository — "File changes made by Windows apps on Windows filesystem don't trigger notifications for Linux apps" — which names webpack --watch explicitly as one of the broken workflows.
Reproduced directly on this machine (Windows 11, Docker Desktop, 6 October 2026): a plain Node.js script watching a bind-mounted file with fs.watch inside a node:20-alpine container never sees an edit made from the Windows side, while the same script polling the file's mtime on an interval catches it reliably (repro.log, part 1).
The more useful test reproduces the Stack Overflow report itself, not just the general mechanism. The author had CHOKIDAR_USEPOLLING=true set and it still did not work — which only makes sense if the watcher their react-scripts version uses does not read that variable. react-scripts 5 ships webpack 5, and webpack 5's watcher package (watchpack) reads its own WATCHPACK_POLLING environment variable rather than going through chokidar:
// watchpack/lib/DirectoryWatcher.js (webpack 5's file watcher)
const WATCHPACK_POLLING = process.env.WATCHPACK_POLLING;
const FORCE_POLLING =
`${+WATCHPACK_POLLING}` === WATCHPACK_POLLING
? +WATCHPACK_POLLING
: !!WATCHPACK_POLLING && WATCHPACK_POLLING !== "false";Reproduced with that exact library (repro.log, part 2, three runs against a bind mount, only the environment variable changed each time):
- no polling variable set: the host-side edit is not seen;
CHOKIDAR_USEPOLLING=true— the negative control matching the Stack Overflow report: still not seen, becausewatchpacknever reads that variable;WATCHPACK_POLLING=true: the edit is detected (repro.log, run 3).
react/create-react-app issue #11879, opened for exactly this regression on react-scripts 5.0.0 and still open, confirms it is not a one-off local misconfiguration. Webpack's own docs describe the polling option and its ~5-second default interval when enabled without a custom value: webpack.js.org/configuration/watch.
Fix#
The exact variable depends on the dev server, not on Docker — pick the one matching your stack.
1. create-react-app / react-scripts ≤ 4 — set CHOKIDAR_USEPOLLING in the service's environment:
services:
frontend:
build: ./frontend
environment:
CHOKIDAR_USEPOLLING: "true"
volumes:
- "./frontend:/app"
- "/app/node_modules"2. create-react-app / react-scripts 5 — CHOKIDAR_USEPOLLING has no effect; set WATCHPACK_POLLING instead, either in docker-compose.yml or directly in the start script:
{
"scripts": {
"start": "WATCHPACK_POLLING=true react-scripts start"
}
}3. Vite — set usePolling under server.watch in vite.config.js; Vite's own docs list this as the fallback for the Docker-on-WSL2 case (after editing from a WSL2 app or moving the project off the Windows filesystem), with a documented CPU cost:
export default {
server: {
watch: {
usePolling: true,
},
},
};4. Next.js dev server (webpack, not Turbopack) — set WATCHPACK_POLLING=true the same way as react-scripts 5, since the Next.js webpack dev build uses the same watcher package.
Whichever variable applies, set it where the running process actually reads it: inside docker-compose.yml's environment: block, or baked into the start/dev script in package.json. react-scripts also loads a .env file itself, but only from the project root; one placed anywhere else is never read, and the container looks like it ignored the flag when it never saw it.
Verify the fix#
- Start the stack and confirm the dev server is up:
docker compose logs -f frontendshould showCompiled successfully!or Vite's ready message. - From the host, edit one line in an existing component (not a new file).
- Watch the container's logs: a rebuild (
webpack compiled successfullyagain, or Vite's HMR update) should appear a few seconds after saving — webpack's own docs put its default poll interval at about 5 seconds whenpoll: trueis used without a custom number.WATCHPACK_POLLINGset to a number instead oftruesets that interval in milliseconds. - Refresh the browser (or let HMR push the update) and confirm the edited line is live.
If step 3 still shows nothing, confirm the bind mount is actually mounted where the dev server watches (docker compose exec frontend ls -la /app/src, then edit the same file from the host and re-list its mtime) before assuming the polling flag is the problem — a mount pointed at the wrong path produces the identical symptom with no fix of this kind able to help.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
A variant worth knowing: a dev container with no Dockerfile at all#
The accepted answer in the source thread avoids the question entirely rather than fixing the watcher: instead of building an image from a Dockerfile and bind-mounting source into it, it runs a bare node image directly — docker run -v $(pwd):/app -w /app node … npm install && npm run dev — with the Dockerfile kept only for the separate job of packaging a production image. The underlying host-to-WSL2 notification gap described above still exists in that setup; this answer works around it by pointing out that many projects do not need their packaging Dockerfile at all during day-to-day development, which removes one layer to debug but is a workflow change, not a fix for the watcher itself.
If the next error is the dev server exiting with Could not find a required file: index.html, that is a different problem — the bind mount is hiding public/ rather than missing change events; see Fix Could not find a required file: index.html in Docker for the mount-path fix.
The docker-compose.yml conventions used above — a bind mount for source and an anonymous volume (/app/node_modules) to stop the host's node_modules clobbering the container's — are covered in full in the Docker dev environment tutorial. If the container fails before the watcher ever starts, with a YAML error on the environment: block rather than a silent watcher, that is a different, more basic mistake covered in Fix Docker Compose environment 'unexpected type map' error. If the app instead crashes at start with Cannot find module 'dotenv' once the bind mount is in place, the mount has hidden the image's node_modules: see Cannot find module in a Docker Compose Node app.
Hot reload across a Docker bind mount on Windows is never going to be as instant as running the dev server directly on the host — polling trades a fixed, small delay for the certainty that an edit is eventually seen, and that trade only needs to be made once, in whichever variable the dev server actually reads.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
Continue Reading
Fix Could not find a required file: index.html in Docker
Prove whether a volume is hiding public/index.html, fix the mount path or the COPY order, and know why docker run shows the file while Compose does not.
Fix Docker: Client Version Is Too Old
Separate an outdated Docker client from a pinned API or the wrong context, and check the minimum your exact Engine release accepts.
Fix Sequelize "Cannot find /config/config.json" in Docker
Reproduced in Docker: why moving WORKDIR alone still fails, why build-time migrations cannot reach the database, and a Compose setup that migrates on start.


