A team wants edits made in a developer's editor to appear instantly inside a running Docker container so the app hot-reloads. How would you set that up, and what problems does that arrangement typically cause?
answer
- bind-mount source + framework watch mode
- deeper volume for node_modules / target / .next
- seeded once → stale after dependency changes
- Docker Desktop: inotify doesn't cross the VM → polling
- prod images COPY the code; never bind-mount source
basics
~20 sBind-mount the project directory into the container and run the framework's watch mode. Then handle the fallout: shadow build output and dependency directories with volumes, expect slow IO and missed file-change events on Docker Desktop, and never ship this setup — production images copy the code in.
solid answer
~60 sBind-mount the source tree (`-v ./:/app`) and run the dev server in watch mode inside the container. Then deal with the three problems it reliably creates: 1. **The bind mount hides image-built content.** Everything the build produced under `/app` — `node_modules`, `target`, `.next` — disappears behind the host directory. Fix by mounting a volume one level deeper (`-v /app/node_modules`), which wins because it is the deeper path and is seeded from the image. 2. **File-change events may not cross the mount.** On Docker Desktop the source lives on the host and is shared into the Linux VM; inotify events do not always propagate reliably, so watchers see nothing. Fall back to polling (`CHOKIDAR_USEPOLLING`, `--poll`, Spring devtools polling) and accept the CPU cost, or run the watcher on the host. 3. **Performance and ownership.** Host↔VM file sharing on macOS/Windows adds real latency for large trees; keep dependency and build directories inside the VM as volumes. Files the container creates in the bind mount arrive with the container user's ownership, which is a well-known irritation on Linux hosts. Production images `COPY` the code; the bind mount is a dev-only affordance.
code
yaml · 18 linesservices:
web:
build:
context: .
target: dev
command: npm run dev
environment:
CHOKIDAR_USEPOLLING: "true"
CHOKIDAR_INTERVAL: "500"
volumes:
- ./:/app # live source from the host
- deps:/app/node_modules # deeper mount wins; seeded from the image
- /app/.next # build output stays inside the VM
ports:
- "3000:3000"
volumes:
deps:go deeper
Know that a bind mount of the project directory is what makes edits show up live, and that the dev server must be running in watch mode inside the container.
Add the shadowing problem and the deeper-volume fix, and explain why that volume ends up holding the image's dependencies.
Own the failure modes end to end: missed inotify events and the polling tradeoff, stale seeded volumes after dependency changes, host↔VM performance, root-owned generated files, and the hard line against bind-mounting source in production.
Frame it as developer-experience infrastructure: one documented dev override that everyone shares, an explicit reset ritual for dependency changes, guardrails so dev mounts cannot reach production, and a judgement call on how much loop speed justifies the divergence between the dev container and the shipped image.
## The setup The mechanic is simple: a bind mount makes a host directory the container's view of a path, so an editor save on the host is immediately visible to the process in the container. Combine that with the framework's own watcher (`next dev`, `nodemon`, `vite`, `webpack --watch`, `spring-boot:run` with devtools, `cargo watch`) and you get an edit-reload loop without rebuilding the image. A typical Compose dev service bind-mounts the project root at the app's working directory, overrides the command to the watch-mode entrypoint, publishes the dev port, and layers volumes over the paths that must not be shadowed. ## Problem 1 — the mount hides what the image built A bind mount replaces a path; it never merges. Mounting the host project over `/app` therefore hides everything the Dockerfile installed or compiled under `/app`. For a Node image that means `node_modules` vanishes, and the container fails with "module not found". If the host *does* have a `node_modules`, it may be worse: dependencies with native bindings built on macOS or Windows will not load on the Linux container, producing errors that look like corrupted packages. The fix is a second, deeper mount. Mounts are applied by path depth, so `/app/node_modules` takes precedence over `/app`. Using an anonymous or named volume there means the path starts empty and is therefore seeded from the image's own `node_modules` — dependencies from the image, source from the host. The same shape covers `/app/target`, `/app/.next`, `/app/build`, `/app/dist`, `/app/.venv`. The cost: those volumes go stale. Change a dependency and the container keeps the seeded copy, because seeding only happens when the volume is empty. Teams handle it with `docker compose down -v` (or removing that one volume) as part of the "dependencies changed" ritual, or by naming the volume and documenting it. ## Problem 2 — file-change events Watchers use inotify on Linux. That works natively when host and container share the same kernel. On Docker Desktop, the host filesystem is shared into a Linux VM through VirtioFS/gRPC-FUSE, and inotify events raised on the host do not reliably translate into events inside the VM. The symptom is characteristic: you edit, nothing happens; you touch the file inside the container, and reload fires. Options: enable polling in the watcher (`CHOKIDAR_USEPOLLING=true`, `WATCHPACK_POLLING=true`, `--poll` in various tools, Spring Boot devtools poll interval), which works everywhere at the cost of constant CPU and battery — tune the interval and limit the watched paths. Or keep the watcher/build on the host and only run the process in the container. Even on native Linux there is a resource ceiling: `fs.inotify.max_user_watches` and `max_user_instances` are host-wide sysctls, and large trees plus several containers exhaust them, producing "ENOSPC: System limit for number of file watchers reached", which is a sysctl problem rather than a disk problem. ## Problem 3 — performance and ownership On macOS and Windows, every stat and read across the host↔VM boundary carries overhead, so operations that touch thousands of files (dependency installs, test runs, TypeScript project loads) can be many times slower than on the host. Keeping `node_modules` and build output in volumes inside the VM is as much a performance fix as a correctness one. The legacy `:cached` / `:delegated` consistency options exist for this and are accepted but effectively no-ops under VirtioFS on current Docker Desktop; do not present them as the fix. Ownership: a container running as root writes files into the bind mount that appear as root-owned on a Linux host, so the developer then cannot edit or delete generated files without `sudo`. The remedies (running the container as the host user's uid/gid, user namespace remapping) are the subject of container permission mapping and belong with that material — for this question it is enough to name the symptom and know it is not a Docker Desktop issue since macOS/Windows sharing already translates ownership. ## The boundary with production This is a development affordance and nothing else. The production image `COPY`s the source in and builds it, so the image is self-contained and reproducible. A bind mount in production would mean the running code is whatever happens to be on that host's disk — unversioned, unauditable, and a genuine escape route into the host filesystem. Keep the dev overrides in a separate Compose override file so nobody can deploy them by accident. ## What a strong answer includes The bind mount, the deeper-volume trick with the reason it works (depth precedence plus seeding), polling as the answer to missed events with its CPU cost, VM-boundary performance, root-owned files, and the explicit statement that production copies code into the image.
- After bind-mounting the project directory, the container reports that a dependency cannot be found even though the image built successfully. What happened?The bind mount replaced the container's `/app`, hiding the dependency directory the image built there — mounts shadow, they never merge. Add a volume one level deeper (for example on `/app/node_modules`); the deeper path takes precedence, and because that volume starts empty Docker seeds it from the image's own copy. Source then comes from the host while dependencies come from the image.
- Edits on the host no longer trigger a reload, but touching the file inside the container does. What is the cause and the fix?The watcher relies on inotify, and inotify events do not reliably cross the host-to-VM file-sharing layer used by Docker Desktop, so the container never learns the file changed. Switch the watcher to polling (`CHOKIDAR_USEPOLLING`, `--poll`, or the tool's equivalent) and tune the interval, accepting the CPU cost; alternatively run the watcher on the host. On native Linux the analogous failure is exhausting `fs.inotify.max_user_watches`.
- Would you use the same bind-mount setup in production?No. A production image copies the source in at build time so the artifact is self-contained, versioned and reproducible; a bind mount would make the running code depend on whatever is on that host's disk and would widen the container's access to the host filesystem. Keep the dev mounts in a separate Compose override so they cannot be deployed by accident.
saying these in an interview costs you the question
- Expecting a bind mount over the working directory to merge with, rather than hide, the image's installed dependencies.
- Reusing the host's dependency directory in a Linux container when it was installed on macOS or Windows with native modules.
- Believing `:cached` / `:delegated` still meaningfully improve performance on current Docker Desktop, where they are effectively no-ops under VirtioFS.
- Blaming the framework when a watcher misses changes, instead of recognising the inotify-across-the-VM boundary and switching to polling.
- Proposing the same bind-mounted source layout for production deployments.