Bind Mounts in Development
A bind mount maps a host directory straight into the container, so a file you save on the host is instantly the file the container runs — no rebuild, no copy. This is what makes containerized development bearable: edit Driftwood's Python source in your editor, and the gunicorn reloader inside the container picks it up. The same directness that makes it good in development is what makes it a footgun — most infamously when the mount clobbers a directory the image already populated.
This page is about using bind mounts where they belong — the inner development loop — and about the three ways they bite: hidden dependencies, accidental writes, and UID mismatches. Each is a predictable consequence of what a bind mount actually does, which is replace the container's view of a path with the host's.
Live-Editing Source on the Host
Bind-mount the project directory over the app's working directory — -v /home/dev/driftwood:/app — and host edits appear in the container immediately. Pair that with a hot-reloader (gunicorn's --reload, a framework's dev server) and you get an edit-save-refresh loop with no image rebuild between changes. The container holds the runtime and the installed dependencies from the image; the bind mount supplies the source you're actively editing. That split is the whole appeal — you iterate on code at host speed while the container provides the environment.
The Clobber Footgun
A bind mount does not merge with what the image put at that path — it replaces the container's view of it entirely. Mount your host source over /app and the container now sees your host directory there, and only your host directory. If the image had installed something into /app at build time, it's gone from view. The classic break: a Python image ran pip install into a site-packages or .venv under the working directory, or a Node image baked node_modules into /app/node_modules — bind-mount your host source over /app and those installed dependencies vanish, because your host tree doesn't contain them. The container starts and then dies with "module not found."
/app — a file you save on the host is instantly the file the container runs, so edits reflect instantly with no rebuild.node_modules or site-packages vanish.Working Around the Clobber
There are two standard fixes. The clean one is to keep dependencies out of the path the bind mount covers — install a Python virtualenv at /opt/venv instead of inside /app, so mounting source over /app never touches the packages. The other is to shadow the dependency subdirectory with an anonymous volume layered on top of the bind mount — -v /home/dev/driftwood:/app -v /app/node_modules — so the image's node_modules survives under the host mount because the anonymous volume re-covers that one subpath.
# Python — venv lives outside the mounted path, so it's never clobbered docker run -d --name driftwood-web \ -v /home/dev/driftwood:/app \ -e PATH=/opt/venv/bin:$PATH \ driftwood:dev # image installed deps into /opt/venv, not /app # Node — anonymous volume re-covers node_modules under the host mount docker run -d --name driftwood-web \ -v /home/dev/driftwood:/app \ -v /app/node_modules \ driftwood:dev
In the Python case the host source at /app and the dependencies at /opt/venv never overlap, so there's nothing to clobber. In the Node case the second mount tells Docker to put an anonymous volume at /app/node_modules, which sits on top of the bind mount and preserves the image's installed modules at that one subpath while the rest of /app reflects your host edits.
Read-Only Mounts
Append :ro when the container should read a file but never write it. Driftwood's proxy takes its nginx config exactly this way — -v /home/dev/driftwood/nginx.conf:/etc/nginx/conf.d/default.conf:ro — so the container reads the config at startup and cannot corrupt the host's source-of-truth copy. Read-only is both a safety rail and a statement of intent: it documents that this mount is input to the container, not a place it writes back to. For any config or static asset the container only consumes, :ro should be the default.
Ownership and UID Mismatch
A bind-mounted file carries its host UID, not a username — the kernel only stores the numeric owner. If the container's process runs as a different UID than the host file's owner, it hits permission-denied on files it appears to "own" by name. The sharpest version is the reverse: the container runs as root (UID 0), writes files into the bind-mounted directory, and now the host directory is littered with root-owned files the developer's own account can't edit or delete without sudo. The fix is to align UIDs — run the container as a user matching the host owner, or set ownership deliberately — so the host directory stays manageable from both sides.
- Bind-mounting host source over the image's working directory and wiping the dependencies the image installed there — the container starts but fails on import because
node_modulesorsite-packagesis now the empty host view. - Using a bind mount for Driftwood's database in development "to see the files" and inheriting host ownership and filesystem-specific behavior that diverges from the named-volume production setup, hiding bugs until deploy.
- Mounting a config file read-write when read-only was intended, so a misbehaving container can overwrite or truncate the host's source-of-truth config.
- Running the container as root with a bind mount and littering the host directory with root-owned files the developer's own account can't edit or delete without
sudo. - Bind-mounting on Docker Desktop and blaming the app for slow I/O — bind-mount reads and writes cross the macOS/Windows-to-Linux-VM boundary (topic 02, topic 06), which is the latency, not the code.
- Use bind mounts for source in development and named volumes for data in production, and keep the two setups close enough that dev still catches real bugs.
- Install dependencies into a path your source bind mount doesn't cover, or shadow the dependency directory with an anonymous volume, so a host mount never hides the image's installed packages.
- Mark every bind mount the container only needs to read with
:ro, including Driftwood's nginx config, so the container can't mutate host files. - Align UIDs — run the container as a user matching the host owner, or set ownership deliberately — so bind-mounted files don't become root-owned and unmanageable on the host.
:z/:Z) that matter on Fedora/RHEL hosts
Kubernetes a host bind is a hostPath volume, explicitly discouraged for portability (Ch12 topic 76)
NFS/Samba share · rsync-on-save the pre-container sync workflow a bind mount replaces
Knowledge Check
Why does bind-mounting host source over the image's working directory break the container with "module not found"?
- A bind mount replaces the container's view of that path, hiding the dependencies the image installed there
- It permanently deletes the installed dependencies from the image's read-only layers on disk, rewriting the layer so the modules are gone for every future container started from that same image
- It merges the host and image directories, and the merge corrupts the dependency files
- It mounts the path read-only, so the runtime is blocked from loading the modules
What does layering an anonymous volume at /app/node_modules over a source bind mount accomplish?
- It re-covers that one subpath so the image's installed node_modules survives under the host mount
- It copies the host's own
node_modulesdirectory up into the container at startup and keeps the two trees synced, so the container always runs whatever packages the host happens to have installed locally - It makes the entire bind mount read-only to protect the installed dependencies
- It reinstalls all of the dependencies from scratch on every container start
When is a bind mount the right tool, and when is it the wrong one?
- Right for live-editing source or sharing a host config in dev; wrong for production database data, where a named volume belongs
- Right for the production database's data directory because a fixed host path is easy to locate and back up, wrong for live-editing source in development where a rebuild is needed for every change anyway
- Right for keeping a decrypted secret off disk, wrong for any on-disk source file
- Right whenever you want one mount to stay portable across many different hosts
Why does a bind mount with the container running as root leave files the host developer can't delete?
- The container writes files owned by root's UID, and a non-root host user can't modify UID-0 files without sudo
- The files are locked inside the container's mount namespace and never actually appear on the host disk, so the developer sees them only from within the running container and they disappear the moment it stops
- Docker encrypts every file written through a bind mount so that only root can later read them
- The kernel stores the owner as a plain username that the host then can't resolve
You got correct