A Docker image already contains files at `/app/node_modules`. What does the container see at that path if you mount an empty named volume there, versus if you mount an empty host directory there as a bind mount?
answer
- mount replaces, never merges
- empty volume → seeded from image, once
- bind mount → shadows, never copies
- stale volume beats rebuilt image
- anonymous volume on node_modules over a bind-mounted /app
basics
~20 sAn empty named volume is pre-populated: Docker copies the image's files at that path into the volume on first use, so the container still sees them. An empty bind mount is not — it simply hides the image content, and the container sees an empty directory.
solid answer
~60 sBoth mounts replace the path; only one of them seeds itself from the image. - **Empty volume:** the first time a container mounts a volume that has no content, Docker copies whatever the image has at the target path into the volume, preserving ownership and permissions, then mounts it. So `/app/node_modules` still shows the image's modules — and the volume now owns that copy. This happens only when the volume is genuinely empty; on later runs the existing volume content wins and stale data can quietly shadow a newer image. `volume-nocopy` disables the seeding. - **Empty bind mount:** no copy ever happens. The host directory is mounted over the path, so the container sees exactly the host content — empty means empty. The image files still exist in the layer, they are just unreachable while the mount is in place; unmount and they are back. The classic use is a Node dev container: bind-mount the source tree, then add an anonymous volume on `node_modules` so the image-built dependencies are not hidden by the host directory.
code
bash · 11 lines# image has /app/node_modules baked in
docker run --rm -v nm:/app/node_modules myapp ls /app/node_modules | head
# -> lists the image's modules: the empty volume was seeded
mkdir /tmp/empty
docker run --rm -v /tmp/empty:/app/node_modules myapp ls /app/node_modules
# -> prints nothing: the bind mount hid the image content
# opt out of seeding
docker run --rm --mount type=volume,source=nm2,target=/app/node_modules,volume-nocopy myapp \
ls /app/node_modulesgo deeper
Know the one-line rule: an empty volume picks up the image's files at that path, an empty bind mount hides them.
Add that seeding happens only when the volume is empty, so an old volume can shadow a freshly built image, and be able to explain the node_modules anonymous-volume idiom.
Discuss it as a debugging pattern — "container runs stale content" almost always traces to a pre-seeded volume — plus nocopy, and the rule that a volume should hold data the app owns, not build output.
Point out that relying on seeding couples image content to Docker-specific behaviour that does not hold under Kubernetes, and argue for images that initialise their data directories explicitly at startup instead.
## The mechanic being tested Mounting is not merging. When Docker mounts anything at `/app/node_modules`, the kernel puts a new filesystem over that directory in the container's mount namespace. Whatever the image layers held there is still on disk but is no longer visible at that path. The only question is what the *new* filesystem contains at mount time — and that is where volumes and bind mounts diverge. ## Volumes: copy-on-first-use When you mount a volume (named or anonymous) whose data directory is empty, Docker first copies the content that the image has at the target path into the volume, including file modes and ownership, and then performs the mount. The intent is convenience: mount a volume on a database's data directory and it comes up with the image's initialised skeleton rather than an empty directory. Three consequences worth stating in an interview: 1. **It happens once.** Seeding is conditional on the volume being empty. On the second run the volume already has content, so nothing is copied. If you rebuild the image with new files at that path and reuse the old volume, the container keeps running the *old* copied content — a genuinely common "my change didn't take effect" bug. The fix is to remove and recreate the volume, or to stop putting mutable image content behind a volume. 2. **You can turn it off.** `--mount type=volume,volume-nocopy,...` (Compose: `volume: {nocopy: true}`) mounts the empty volume as-is. Useful when the target holds a large tree you do not want duplicated, or with network-backed volumes where the copy is expensive. 3. **It is Docker-specific.** Kubernetes volume mounts do not seed themselves from the image; teams that rely on this behaviour locally are sometimes surprised when the same image is moved to an orchestrator. (That side of the story belongs to the Kubernetes storage material — here, just know the behaviour is not universal.) ## Bind mounts: pure shadowing A bind mount never copies. The host directory's content is what the container sees, full stop. If the host directory is empty, the container sees an empty directory, no matter how much the image had there. If it has different files, the container sees those instead. This is not a bug — it is the entire point for config injection and dev workflows: you *want* the host's `nginx.conf` to replace the image default. It only becomes a surprise when you bind-mount a whole project directory over a path where the image had build output. ## The `node_modules` pattern The two behaviours combine into a widely-used idiom. A Node dev container is usually built with `npm ci` inside the image, so `/app/node_modules` exists in the image layer. Then you bind-mount your host project directory at `/app` so edits are live. That bind mount shadows *everything* under `/app`, including `node_modules` — and your host tree may have no `node_modules` at all, or one built on a different OS/architecture with native modules that will not load. The fix is to layer a second mount deeper than the first: an anonymous (or named) volume at `/app/node_modules`. Mount ordering is by path depth, so the volume wins for that subtree, and because the volume starts empty it is seeded from the image's own `node_modules`. Result: source is live from the host, dependencies come from the image. The same shape appears for compiled output: `/app/target` in a Java or Rust dev container, `/app/.next` for Next.js, `/app/build` for CRA-era tooling. ## Single files Bind mounts can target a single file, and then the shadowing is file-scoped: `-v ./my.cnf:/etc/mysql/my.cnf:ro` replaces exactly that file and leaves siblings visible. Two caveats: the target must already exist in the image for a file bind on some setups, and because the mount points at an inode, editors that replace files atomically (write temp + rename) break the mount — the container keeps seeing the old inode until restart. Mounting the *directory* avoids that. ## How to answer State the rule crisply — "empty volume gets seeded from the image, bind mount just hides it" — then show that you know the follow-on: seeding happens only once, stale volumes shadow fresh images, `nocopy` exists, and the `node_modules` trick is the practical application.
- You rebuilt the image with new files under a path that is backed by an existing named volume, but the container still shows the old files. Why?Because volume seeding only happens when the volume is empty. The volume already has the content copied from the first build, and a mount always shows the volume's content, not the image's. Remove and recreate the volume (or use a fresh volume name) to pick up the new image content — and treat it as a signal that mutable image content probably should not sit behind a volume.
- Why does `- /app/node_modules` in a Compose file work even though there is no source before the colon?A single-field entry declares an anonymous volume at that container path: Docker creates a volume with a generated ID and mounts it there. Because it is deeper than the `./:/app` bind mount, it takes precedence for that subtree, and because it starts empty it is seeded from the image's `node_modules`. The cost is a stray anonymous volume per container, which is why some teams name it instead.
A named volume is like moving into an empty flat that the landlord furnishes with the show-home furniture the first time only; a bind mount is like parking a shipping container over the front door — whatever was inside is still there, you just cannot reach it.
saying these in an interview costs you the question
- Saying the image files and the mounted content are merged or unioned together.
- Believing a bind mount also copies image content into the empty host directory.
- Assuming volume seeding re-runs on every start, so a rebuilt image will always refresh the volume.
- Claiming the image files are deleted or overwritten by the mount — they are only hidden while mounted.
- Thinking the mount order in the Compose list decides precedence rather than path depth.