Why hot reload stops working in Docker on a Mac, and how to fix it
By EZGH Cloud · · 2 min read
In short
Containers on a Mac run in a Linux VM, and file changes made on the Mac reach that VM through a shared file system that doesn't always deliver Linux inotify events. Dev servers that wait for those events never reload. Switch the watcher to polling, for example Vite's server.watch.usePolling or WATCHPACK_POLLING=true for Next.js.
You start your stack with docker compose up, edit a file in your editor, and nothing happens: the
dev server inside the container never reloads. Restarting the container picks up the change, so the
file did change. The watcher just never heard about it.
Why it happens
Linux containers need a Linux kernel, so on a Mac they run inside a virtual machine. When you
bind-mount your project (-v "$PWD:/app"), the VM reads your files from macOS through a shared file
system. On EZGH DevRun that’s VirtioFS.
File watchers on Linux, such as chokidar, watchpack, fsnotify and the tools built on them, normally don’t scan files. They ask the kernel to tell them about changes through inotify. The kernel raises those events for writes that happen inside the VM. An edit made by your editor on the Mac happens outside it: the new contents are readable from the container, usually within a second or two, but no inotify event fires. In DevRun’s testing, host writes to a VirtioFS mount didn’t raise inotify events at all.
So the fix isn’t in Docker or DevRun. It’s in the watcher: tell it to poll, checking file modification times on an interval, instead of waiting for events.
The fix for your tool
Vite
// vite.config.js
export default {
server: {
watch: { usePolling: true, interval: 300 },
},
}
Vite passes server.watch to its file watcher. Its docs warn that polling uses more CPU, so keep
the interval reasonable.
Next.js
Set WATCHPACK_POLLING on the dev container:
# compose.yaml
services:
web:
build: .
command: npm run dev
environment:
WATCHPACK_POLLING: "true"
volumes:
- ./:/app
- /app/node_modules
webpack (and webpack-dev-server)
// webpack.config.js
module.exports = {
watchOptions: { poll: 1000, ignored: /node_modules/ },
}
Anything built on chokidar
Many Node tools, including Vite and Create React App, watch through chokidar, which honors an environment variable that forces polling:
environment:
CHOKIDAR_USEPOLLING: "true"
nodemon
nodemon --legacy-watch src/index.js # or -L
or "legacyWatch": true in nodemon.json.
Go with air
# .air.toml
[build]
poll = true
poll_interval = 500 # ms
Python and Ruby
Django’s runserver reloads with its built-in StatReloader, which already polls, unless you’ve
installed Watchman. Rails’ default ActiveSupport::FileUpdateChecker compares modification times,
so it works too. If you switched Rails to EventedFileUpdateChecker, switch back for development in
containers.
Keep polling cheap
- Watch less. Ignore
node_modules, build output and.git. Most tools above take an ignore list. - Keep dependencies in the container. An anonymous volume over
node_modules(as in the Compose example) keeps thousands of dependency files out of the shared folder entirely. - Pick an interval you won’t notice. 300 to 1,000 ms reloads quickly enough for most work.
Do you need the bind mount at all?
For services you aren’t editing, such as a database, a queue or a cache, run the image as is, with a named volume for its data. Named volumes live inside the VM, so they’re fast and file events work normally. Keep bind mounts for the code you’re changing.
DevRun shares your home folder and /tmp by default; projects elsewhere need a folder added under
File sharing. See running Docker on a Mac without Docker Desktop
for the full setup.
Frequently asked questions
- Why doesn't my dev server reload when I edit files on my Mac?
- Linux containers on a Mac run inside a VM. Your project reaches the VM through a shared file system, and edits made on the Mac don't always raise Linux inotify events inside it. A watcher that waits for those events never sees the change.
- How do I make Vite reload inside Docker on a Mac?
- Set server.watch.usePolling to true in vite.config, optionally with server.watch.interval. Vite's docs note that polling uses more CPU.
- How do I make Next.js reload inside Docker?
- Set the environment variable WATCHPACK_POLLING=true on the dev container, in your Compose file's environment section or the Dockerfile.
- Does polling slow things down?
- A little. The watcher checks files on an interval instead of being told about changes, so it uses some CPU and adds up to one interval of delay. Watch only your source folders, not node_modules, to keep it cheap.