Nodemon Docker Mac: legacyWatch Fix for Stale Code
On Docker for Mac, Nodemon silently ignores file saves.
20+ years shipping production JavaScript and front-end systems at scale. Lessons pulled from things that broke in production.
- Nodemon watches file changes and restarts Node.js automatically in development
- Install locally with --save-dev; never globally or in production
- Use nodemon.json to control watch paths, ignore patterns, and debounce delay
- The infinite restart loop happens when the app writes to watched dirs — fix with ignore
- On Docker for Mac, file events may not propagate; enable legacyWatch or polling mode
- For TypeScript, use exec key with tsx — 10x faster than ts-node for large codebases
Imagine you're editing a Google Doc and every time you change a word, someone has to manually walk over, close the document, and reopen it before you can see the update. That's exactly what developing a Node.js app without Nodemon feels like — you change one line, then you Ctrl+C the server, retype 'node server.js', and wait. Nodemon is the person standing over your shoulder who sees you saved the file and restarts the server automatically before you've even lifted your hands off the keyboard. You write code, flip to the browser, and your change is already live.
I watched a junior dev spend forty-five minutes debugging what turned out to be a change he'd made that was never actually running — because he'd forgotten to restart his server after saving the file. Not once. Three times in the same afternoon. That's not a character flaw; it's a workflow problem, and Nodemon exists to eliminate it entirely.
Node.js doesn't watch your files. When you start a Node.js server with 'node app.js', it loads your code once into memory and runs it. The moment you change a file and save it, Node.js has absolutely no idea. It's still running the old code. The only way to pick up your changes is to kill the process and restart it manually. In a fast feedback loop where you're editing, checking, editing, checking — that's a tax you pay hundreds of times a day. It sounds minor until you've done it for three hours straight and your muscle memory is shot and you've introduced a bug because you were staring at stale output.
After reading this, you'll be able to install Nodemon, wire it into any Node.js project, configure it to watch exactly the files you care about, ignore the ones you don't, and set it up so that every developer who clones your repo gets the same workflow automatically. You'll also know the three configuration mistakes that cause Nodemon to restart infinitely or miss changes entirely — both of which I've seen derail entire dev sessions.
Why Nodemon Needs a Docker Fix on Mac
Nodemon is a file-watching utility that restarts a Node.js process when source files change. Its core mechanic: it monitors the filesystem for modification events and kills/restarts the running process automatically. On Mac inside Docker, the default polling mechanism fails because macOS's native file events (fsevents) don't propagate reliably through Docker's filesystem layers. The result: nodemon misses changes, and developers manually restart containers — defeating the purpose. The fix is legacyWatch: true, which forces nodemon to use filesystem polling instead of event-driven watching. Polling checks file timestamps every interval (default 1 second), trading CPU overhead for correctness. In practice, this means nodemon works reliably in Docker-on-Mac environments, but you must configure it explicitly — the default behavior is broken. Teams that skip this setting waste hours debugging 'why didn't my code reload?'
legacyWatch: true to force polling — it's slower but reliable.Why Manual Restarts Kill Your Flow (And What Node.js Actually Does)
Before you can appreciate what Nodemon does, you need to understand why Node.js doesn't auto-restart by itself — and it's not an oversight, it's a deliberate design decision.
When you run 'node server.js', Node reads that file, compiles it to bytecode, loads it into memory, and starts executing. From that point on, Node is a running process. It owns a port, it holds database connections, it has state in memory. It doesn't scan your file system for changes because that's not its job. Its job is to serve requests as fast as possible. Polling the disk constantly would waste CPU cycles and slow everything down. So Node.js makes a deal with you: 'I'll be blazing fast, but you handle restarts.'
In development, that deal is terrible. You're changing files every two minutes. Every change requires a Ctrl+C and 'node server.js' again. Worse, you'll forget. You'll save a file, test your endpoint, get the old behaviour, spend ten minutes convinced your logic is wrong, and then finally notice you never restarted. I've seen this exact scenario cause someone to revert a perfectly correct fix because they thought it 'didn't work.'
Nodemon solves this by wrapping the Node.js process. It watches your project files for changes, and when it detects one, it kills the current Node.js process and starts a new one automatically. Your server is always running the latest version of your code. You save, you flip to Postman or your browser, your change is there.
Installing Nodemon and Running Your First Auto-Restarting Server
Here's exactly how to get Nodemon running from nothing. Two decisions upfront: global install vs. local install. I'll tell you the right answer and explain why the other one causes problems on a team.
Global install means you run 'npm install -g nodemon' and the 'nodemon' command becomes available everywhere on your machine. Sounds convenient. The problem is that your teammates might have a different version, or a CI/CD pipeline won't have it at all. Six months later someone clones your repo, runs the start script, gets 'nodemon: command not found', and wastes half an hour figuring out why. Don't do global installs for project tooling.
Local install — 'npm install --save-dev nodemon' — puts Nodemon in your project's node_modules folder and records it in package.json under devDependencies. Anyone who clones the repo and runs 'npm install' gets the exact same version of Nodemon automatically. This is the only approach that works on a team or in CI. The '--save-dev' flag is critical: Nodemon is a development tool, not something that should ever run in production. Separating dev from production dependencies keeps your production Docker image lean and your deployment safe.
Once installed locally, you can't just type 'nodemon server.js' in the terminal — your shell doesn't know where to find it. You access it through npm scripts in package.json, which automatically add your local node_modules/.bin to the PATH when running scripts.
Configuring Nodemon: Watch the Right Files, Ignore the Rest
Out of the box, Nodemon watches all .js, .mjs, .cjs, and .json files in your project directory and restarts on any change. For a tiny project that's fine. For anything real, it'll drive you insane.
Here's the real problem: Nodemon doesn't know the difference between your source code and generated files, log files, or temporary files. If your app writes to a log file on every request — which is completely normal — and that log file is inside your project directory, Nodemon sees the write, thinks your code changed, and restarts. Which triggers another request. Which writes to the log. Which triggers another restart. I've seen this infinite restart loop on three separate teams. It eats CPU, it makes your logs unreadable, and it looks like the world's most confusing bug because the server is constantly restarting for no visible reason.
The fix is a nodemon.json configuration file. It sits at the root of your project alongside package.json and gives you precise control over what Nodemon watches, what it ignores, and what file extensions trigger a restart. Every serious Node.js project should have one. It makes the development experience consistent for every person on the team — nobody's getting phantom restarts because of local tooling differences.
Nodemon With ES Modules, TypeScript, and Multi-Process Apps
Basic Nodemon for a CommonJS Express app is straightforward. But modern Node.js projects use ES Modules ('import'/'export' instead of 'require'), TypeScript, or even multiple processes. Each one needs a small adjustment or you'll hit a wall and not know why.
ES Modules in Node.js require either a .mjs extension or '"type": "module"' in your package.json. Nodemon itself handles this fine — the restart mechanism doesn't care about your module system. But the command you give Nodemon matters. If you're using '"type": "module"' and running Node 18+, plain 'nodemon server.js' still works. No change needed there.
TypeScript is the one that trips people up. Nodemon can't execute TypeScript directly — Node.js can't either. You need a TypeScript executor. The most common choice for development is 'ts-node'. You tell Nodemon to use ts-node as the executor instead of node, and it handles compilation on the fly. For larger projects, 'tsx' has become the faster alternative — it's built on esbuild and noticeably quicker for big codebases. I've seen teams on large TypeScript monorepos where ts-node's restart time was 8-12 seconds per change. Switching to tsx dropped that to under 2 seconds. That compounds over a full work day into real productivity.
For any of these setups, the exec key in nodemon.json is your control lever. It replaces 'node' with whatever executor you need.
Nodemon in Docker and Team Workflows
Running Nodemon inside a Docker container is common for development, but it introduces a critical gotcha: file change detection across volume mounts.
On Linux hosts running Docker natively, filesystem events (inotify) propagate correctly. Nodemon works out of the box. But on macOS (Docker Desktop) and Windows, the host filesystem is mounted via a network-like layer (osxfs on Mac, SMB on Windows). These layers do not forward inotify/FSEvents signals into the container. Nodemon sees nothing. You edit a file on your host, the container's filesystem updates, but Nodemon's watcher receives no event. You stare at a stale server.
The fix is polling mode. Set '"legacyWatch": true' in nodemon.json. This tells Nodemon's chokidar to poll the filesystem every few seconds instead of waiting for events. It uses more CPU (about 2-5% on a modern machine) but it's reliable across all Docker environments. Set the polling interval via '"pollingInterval": 1000' (milliseconds) to control frequency.
Another team workflow concern is consistent Nodemon configuration across multiple developers. The only way to guarantee this is to commit a nodemon.json file to your repository. Do not rely on each developer having the right global config or CLI flags. The nodemon.json file is checked in, versioned, and every team member sees exactly the same behaviour. If someone needs a local override (e.g., different port), they can use environment variables instead of modifying the committed config.
The One Config Mistake That Kills CPU on Large Projects
Nodemon's default behavior is to watch every file in your project directory. On a small Express API, that's fine. On a monorepo with 10,000+ files in node_modules, generated build artifacts, and log files — it'll peg your CPU at 100% and your editor will lag.
The root cause isn't nodemon being greedy. It's that developers don't understand how file-watching works under the hood. Nodemon relies on fs.watch or chokidar, which recursively scans directories for changes. Every time a file is saved, nodemon registers a change event, restarts your app, and re-scans the entire watch tree. Multiply that by hundreds of files, and you've got a feedback loop that kills performance.
The fix is brutally simple: be explicit about what nodemon should watch. Use the --watch flag or the nodemon.json config file to narrow the scope to your source directory only. Never watch node_modules, dist, .git, or log directories. The rule: if you didn't write it, don't watch it. Your CPU will thank you.
Debugging Nodemon With Breakpoints — The Clean Way
Developers love nodemon for the auto-restart. They hate it when they attach a debugger and restarting the app kills their breakpoints and drops the debug session. The standard approach — using node --inspect with nodemon — is fragile because nodemon restarts the process, which releases the debug port.
Here's the production pattern: use nodemon's --inspect flag or pass the Node.js inspect argument through nodemon's exec map. This tells nodemon to manage the debugger lifecycle. When your app restarts, nodemon re-attaches the debugger to the new process on the same port. No session drops. No manual re-connect.
Second, set a delay for restarts during debugging. The --delay flag with a value like 2000ms gives you a two-second window after saving a file before nodemon kills the process. That extra time lets your debugger catch the last state of the old process. Without it, you can lose the context of the bug you were chasing.
Finally, combine this with source maps for TypeScript or Babel projects. If nodemon isn't configured to use transpiled source maps, your debugger will show you compiled code — not the original TypeScript you wrote. That's a waste of time. Add "execMap": {"ts": "node --require ts-node/register --inspect=9229"} to your config.
--inspect flag instead of manually passing --inspect to Node. Nodemon preserves the debug port across restarts automatically.--inspect flag and a --delay of at least 1s when debugging — this prevents debug session loss and gives you time to inspect state before restart.Introduction
Developing Node.js applications often involves a tedious cycle: make a code change, stop the server, and restart it manually. Nodemon eliminates this friction by automatically monitoring your project files and restarting the Node process whenever a change is detected. It wraps your application in a file-watching layer that triggers graceful restarts—preserving your debugging state when combined with the right flags. This guide covers everything from local setup to team workflows, with a focus on why each configuration choice matters. By the end, you'll avoid the common pitfalls that waste CPU cycles and break Docker containers. Nodemon is not just a convenience tool; it's a reliability layer that keeps your feedback loop tight during development, letting you stay in flow state without breaking concentration on manual restarts.
Prerequisites
Before installing Nodemon, ensure you have Node.js version 14 or higher installed on your system. You can verify this by running node --version in your terminal. A basic understanding of the command line and package.json scripts is helpful. If you are using Docker or working with TypeScript, ES modules, or multi-process apps, you will need additional configuration—covered later. No prior Nodemon experience is required; this guide starts from scratch. For local development, you should also have a Node.js project initialized (npm init) and a working server file (e.g., server.js or index.js). If you already have a project, make sure your dependencies are installed. These prerequisites ensure that Nodemon can correctly hook into your application's lifecycle without interference from missing modules or incompatible runtime versions.
Silent Stale Code on Docker for Mac
- If Nodemon works on native Linux but not in Docker on macOS, the fix is almost always legacyWatch: true in nodemon.json.
- Always test your development Docker setup with a known file change before a full sprint. Catch this on day one, not day five.
- Document platform-specific configurations in your project README so every new developer doesn't have to rediscover this.
nodemon --legacy-watch --verbose server.jsAdd '"legacyWatch": true' to nodemon.jsonKey takeaways
Common mistakes to avoid
5 patternsInstalling Nodemon as a regular dependency (npm install nodemon without --save-dev)
Watching a directory that the running app writes to (e.g., logs folder inside src/)
Putting Nodemon configuration as CLI flags in package.json scripts instead of a nodemon.json file
Using Nodemon in production (accidentally via a 'start' script that calls nodemon, or by copying the 'dev' script)
Not setting a debounce delay for multi-file saves
Interview Questions on This Topic
Nodemon watches for file changes and restarts your Node.js process — but what's the underlying mechanism it uses to detect changes, and what are the implications of that mechanism on network file systems like NFS or Docker volume mounts on Windows?
Frequently Asked Questions
20+ years shipping production JavaScript and front-end systems at scale. Lessons pulled from things that broke in production.
That's Node.js. Mark it forged?
10 min read · try the examples if you haven't