skip to content

You mount a brand-new, empty Docker named volume at `/usr/share/nginx/html`, a path that already contains files baked into the image. What does the container see on the first start, what does it see on the second start, and how would the result differ if you mounted a host directory at that path instead?

level: middleimportance: should knowfreq 48%

answer

  1. empty volume + image content = copy at create
  2. non-empty volume → no copy, ever
  3. bind mount = never copies, just masks
  4. volume-nocopy opts out
  5. stale assets → delete the volume, not rebuild

basics

~20 s

On the first start Docker copies the image's content at that path into the empty volume ("copy-up"), so the container sees the image files. On the second start the volume is no longer empty, so it is mounted as-is — stale content persists. A host bind mount never copies; it simply hides the image files.

solid answer

~50 s

Docker pre-populates an **empty volume** from the image: at container creation, if the mount target has content in the image and the volume has none, the daemon copies that content (including ownership and permissions) into the volume before mounting. So the first run of `nginx` with an empty volume at `/usr/share/nginx/html` shows the default index page, and those files now live in the volume. On every later start the volume is non-empty, so no copy happens — the volume wins. This is the classic "I rebuilt the image with new assets and the container still serves the old ones" bug: the fix is to delete and recreate the volume, not to rebuild again. Bind mounts behave differently: a host directory is mounted straight over the path with **no** copy-up, so an empty host directory produces an empty directory inside the container and the image's files are simply masked. You can also disable copy-up explicitly with `--mount type=volume,...,volume-nocopy`.

code

bash · 9 lines
bash
docker volume create site
docker run --rm -v site:/usr/share/nginx/html nginx:1.27 ls /usr/share/nginx/html
# 50x.html  index.html   <- copied from the image

# volume is now non-empty; a newer image's assets will NOT be copied
docker run --rm -v site:/usr/share/nginx/html my-site:v2 ls /usr/share/nginx/html
# 50x.html  index.html   <- still the old files

docker volume rm site   # the actual fix

go deeper

for a junior

Say the rule: an empty volume gets seeded from the image at first start; a bind mount does not and just hides what was there.

for a middle

State all three conditions (volume type, volume empty, at creation only), mention volume-nocopy, and note that ownership and permissions come across with the files.

for a senior

Lead with the failure it causes — stale assets after a deploy — and the diagnosis: compare the image listing with the container listing, then delete the volume rather than rebuild.

for a principal

Draw the boundary as a design rule: volumes hold data the app writes, images hold build output, and any mount that spans both invites drift. Encode it in review — no volume over a path the build populates.

## The behaviour When the daemon creates a container it processes each mount. For a mount of type `volume`, it checks two things: does the image have content at the target path, and is the volume currently empty? If both are true, it copies the image's directory tree into the volume — files, subdirectories, ownership (uid/gid), and permission bits — and only then performs the mount. This is universally called **copy-up** (sometimes "volume pre-population"). So with `docker run -v site:/usr/share/nginx/html nginx` on a fresh `site` volume: 1. Daemon sees the image ships `index.html` and `50x.html` at that path. 2. `site` is empty → copy those files into it. 3. Mount `site` over `/usr/share/nginx/html`. 4. The container starts and serves the default page — which now physically lives in the volume. ## Why it exists It makes "just add a volume" work for stateful images. Postgres, MySQL and similar images ship initialised directory skeletons; without copy-up, adding a volume would blank them out and the container would fail to start. Copy-up means an operator can attach persistence to an image without knowing its internal layout. ## The three conditions people forget - **Only for volumes.** Bind mounts (`-v /host/path:/container/path` or `--mount type=bind`) never copy. The host directory is mounted as-is; if it is empty, the container sees an empty directory and the image content underneath is masked, not deleted. This asymmetry is the single most common source of "my node_modules disappeared" confusion. - **Only when the volume is empty.** One stray file, including one written by a previous run, and copy-up is skipped forever. There is no merge and no diffing. - **Only at container creation.** Restarting a container does not re-evaluate anything; neither does `docker start`. And it can be turned off: `--mount type=volume,src=site,dst=/usr/share/nginx/html,volume-nocopy` mounts an empty volume empty, which is what you want when you deliberately intend to shadow image content. ## The bug it causes A team bakes front-end assets into the image and mounts a volume at the asset directory "for persistence". Everything looks right on day one because copy-up seeds the volume. Then they ship a new image with updated assets, `docker compose up -d`, and the site still serves the old files — because the volume is non-empty, the new image's content is never copied and the mount hides it. Rebuilding again changes nothing. Symptoms: the file is present in `docker run --rm newimage ls /usr/share/nginx/html` but missing or stale in the running container. The fix is to stop treating immutable build output as state: do not mount a volume over a path whose content comes from the image. If the volume must exist, recreate it as part of the deploy (`docker compose down -v`, or `docker volume rm site`) so the next start re-seeds it — with the obvious caveat that this is only safe when the path holds nothing you need to keep. The useful mental line: a **volume is for data the application writes**, an **image path is for artefacts the build produced**. Copy-up blurs that line for one moment at first start and then the volume owns the path permanently. ## Interactions worth knowing - Copy-up preserves ownership from the image, which is how a Postgres data directory ends up owned by the `postgres` uid inside a fresh volume rather than by root. That is often the only reason the container can write to it. - If the target path does not exist in the image, Docker creates it (as root-owned) and there is nothing to copy. - The copy happens on the daemon host and can be slow and disk-hungry for large trees; a multi-gigabyte image directory is copied in full on that first start. - Docker Compose has the same semantics — declaring a named volume in `volumes:` for a service path with image content seeds it on first `up` and never again. ## How to verify quickly Run the image with no mount and list the path, then compare with the running container. If they differ and the path is a volume, you are looking at stale copy-up content.

  • A deploy ships new static assets in the image but the running container still serves the old ones. What do you check first?
    Whether a volume is mounted over the asset directory. Compare `docker run --rm <newimage> ls <path>` with the same listing inside the running container: if the image has the new files and the container does not, a non-empty volume is shadowing them. Copy-up only ran when the volume was empty, so the fix is to remove and recreate the volume — or better, stop mounting a volume over build output.
  • Why do people mount an anonymous volume at `/app/node_modules` when bind-mounting their source tree for development?
    The source bind mount would otherwise mask the `node_modules` directory installed during the build, since bind mounts never copy up. Mounting a volume at the nested `node_modules` path takes precedence over the parent bind mount, and because volumes do copy up from image content on first use, the installed dependencies appear inside the container while the source stays live-editable from the host.

Copy-up is like moving into an unfurnished flat where the landlord leaves you a starter set of furniture — but only if the flat is genuinely empty on move-in day. Bring one box yourself and you get nothing; bring a whole van (a bind mount) and the landlord's furniture is locked in the basement, out of sight.

saying these in an interview costs you the question

  • Believing Docker merges image content with existing volume content on every start — it copies once, into an empty volume only.
  • Expecting a bind mount to be seeded from the image; it never is.
  • Concluding that mounting a volume deletes the image's files (they are masked, and reappear if you unmount).
  • Fixing stale-asset symptoms by rebuilding the image repeatedly instead of removing the volume.
  • Assuming copy-up re-runs on `docker restart` or on image upgrade.

context