Docker · commonly confused
Docker volume vs bind mount
Short answer
First published
On this page · 10 sections
In simple terms
Both solve the same problem. Anything a container writes to its own filesystem goes into a writable layer that disappears when the container is removed, so data that must survive has to live somewhere else. A volume and a bind mount are the two ordinary places to put it.
A volume is a storage area Docker creates and looks after for you. You refer to it by name and do not care where it physically lives. A bind mount is a specific directory on your machine that you point the container at — the container sees your real files, and you see whatever it writes.
That makes the choice mostly about who needs to see the files. If only containers need the data — a database's files, an upload directory — a volume is the documented choice, which Docker calls the preferred mechanism for data generated and used by containers, and the Docker documentation says volumes are not a good choice when you need to reach the files from the host. If you need to edit files on your machine and have the container see the change immediately — source code during development, a config file you tune by hand — that is what a bind mount is for.
The part that catches people is what happens to files the image already had at that path. Mount an empty volume there and Docker copies those files into the volume first, so everything still looks present. Bind-mount a directory there instead, and the image's files are hidden behind yours — so a setup that worked with a volume suddenly cannot find its own files when someone switches to a bind mount for local development.
Words used on this page
- mount
- Attaching storage at a path inside the container, so reads and writes there go to that storage instead of the container's writable layer.
- named volume
- Storage Docker creates and manages, referred to by a name such as
app-config. It lives in Docker's storage area and survives the containers that use it. - anonymous volume
- A volume created without a name, which gets a random one. It persists after its container is removed, unless the container was started with
--rm. - bind mount
- A file or directory from the host placed into the container at a path you choose. The container and the host see the same files.
- obscured files
- Files the image already had at a mount path. The mount hides them rather than deleting or merging them, and they stay hidden until the container is recreated without the mount.
- pre-population
- When an empty volume is mounted where the image has files, Docker copies those files into it first. The
volume-nocopyoption turns this off; it never happens for a bind mount. - -v and --mount
- Two ways to write the same mounts. In
-v, a path such as/tmpor./configmeans a bind mount and a plain name means a volume.--mountspells outtype=volumeortype=bindand supports every option. - read-only (ro)
- A mount option that lets the container read the mount but not write to it.
What actually happens
By default every file a container creates goes into a writable container layer on top of the read-only image layers. The Docker documentation is direct that data written there does not persist when the container is destroyed, is unique to that container, and is hard to get out. Volumes and bind mounts both mount external storage into the container's filesystem namespace so the data outlives it.
A volume is created and managed by Docker and stored in a directory inside Docker's own storage area on the host. A bind mount maps a file or directory you choose on the host machine straight into the container. The documentation recommends bind mounts when files must be reached from both the container and the host, and volumes when they need not be — a volume is completely managed by Docker, and using one rather than the writable layer does not grow the container.
The population rules are the mechanism behind most confusion. If you mount an empty volume into a directory where the image already has files, those files are copied into the volume by default. If the volume is not empty, or if you bind mount anything onto that path, the image's existing files are obscured by the mount instead — the documentation compares it to mounting a USB drive over /mnt — and there is no straightforward way to reveal them again short of recreating the container without the mount.
Bind mounts have one more property worth stating plainly: they give the container write access to files on the host by default. A process in the container can create, modify or delete files in that host directory, which the documentation notes can have security implications and can affect non-Docker processes. The readonly option removes that write access when the container only needs to read.
Same path, two mounts, two very different results
# The image ships default config at /app/config. # 1. Named volume. First run, the volume is empty, so Docker copies the # image's /app/config into it. The app finds its defaults. docker run -d --name web -v app-config:/app/config myapp:1.0 # 2. Bind mount. Your host directory is placed over /app/config, and the # image's defaults are hidden. If ./config is empty, the app sees nothing. docker run -d --name web-dev -v "$(pwd)/config:/app/config" myapp:1.0 # 3. The quiet trap: upgrade the image, reuse the old volume. # The volume is no longer empty, so it is NOT repopulated — v2's new # defaults are obscured by the files copied from v1. docker run -d --name web2 -v app-config:/app/config myapp:2.0 # Read-only bind mount: the container can read your files, not change them. docker run -d -v "$(pwd)/config:/app/config:ro" myapp:1.0
Case 3 is the one that produces "the new version didn't take effect". The copy-in only happens while the volume is empty, so a volume populated by an older image keeps the older files at that path.
Try it yourself
Before you start
- Docker Engine or Docker Desktop. Bind mounts refer to paths on the machine where the Docker daemon runs, not where you type the command: with a remote daemon you cannot bind-mount files from your own machine. Docker Desktop shares host paths with its Linux VM for you.
- For a bind mount, a host path that exists.
--mountstops with an error if it is missing, while-vsilently creates it as an empty directory. Relative paths such as./configwork with-vfrom Engine 23, and../parent paths from Engine 28.2.0. - What the image already has at the path you will mount over, because the mount can hide it. If the image has
lsand no ENTRYPOINT in the way,docker run --rm <image> ls -la <path>lists it. - A copy of anything you cannot lose. Removing a volume, or deleting a bind-mounted host directory, deletes the data for every container that uses it.
A bind mount hides the image's own files
Quoted from the Mount into a non-empty directory example in Docker's Bind mounts page, not reproduced: Docker is not installed on the machine this page was checked on, and the page does not say which Docker version produced the message. Docker calls the example contrived to be extreme: it puts the host's /tmp over the image's whole /usr directory. Do not try it with a host directory you care about, because a bind mount is writable by default.
Command
docker run -d \ -it \ --name broken-container \ -v /tmp:/usr \ nginx:latest
Output · as shown in Docker docs: Bind mounts, mount into a non-empty directory
$ docker run -d \ -it \ --name broken-container \ -v /tmp:/usr \ nginx:latest docker: Error response from daemon: oci runtime error: container_linux.go:262: starting container process caused "exec: \"nginx\": executable file not found in $PATH".
How to read it
-v /tmp:/usris a bind mount, because the source is a host path. The host's/tmpnow sits at/usrin the container, and everything the image had under/usris obscured rather than merged.- The nginx binary is one of those hidden files, so the container could not start:
executable file not found in $PATHis the part to look for. The wrapping text,oci runtime error: container_linux.go:262, depends on the runtime version. - Docker's page notes that the container is created but does not start, so it still has to be removed with
docker container rm broken-container. - A new, empty named volume in the same place would have behaved differently: Docker would first copy the image's
/usrinto it, so nginx would be found. A volume that already held data would obscure/usrjust as this bind mount did. - The fix is to mount at a path the image does not use for its own files, or to bind-mount just the one file you mean to replace.
The mistake this causes
Switching a working named volume to a bind mount, or upgrading an image while reusing a populated volume, and expecting the image's files at that path to still appear.
Why people do it An empty volume is filled from the image on first mount, which makes it look as if the image's files are always there. They are not: a bind mount, or any volume that already has data, hides the image's files at that path rather than merging with them.
What you see A container that worked starts failing with missing-file errors for config or assets it ships with — or, after an upgrade, behaves like the old version because the volume is still serving the previous release's files. The image is fine; the mount is covering it.
If it goes wrong
invalid mount config for type "bind": bind source path does not exist: <path>--mountdoes not create missing host paths. Check the spelling, create the directory first, or, on Docker 29.3.0 and later, add thebind-create-srcoption.- A path you meant to mount as a file shows up in the container as an empty directory
- With
-v, Docker created the missing host path, and it always creates a directory. Fix the host path, delete the directory Docker made, and switch to--mountso a missing path fails loudly next time. executable file not found in $PATH, or missing config or assets, right after adding a mount- The mount is covering the image's files at that path. Mount somewhere else, mount a single file instead of the directory, or use an empty named volume if you want the image's files copied in.
- After upgrading the image, the container still serves the old files from a volume path
- Docker fills a volume from the image only while it is empty, so the old files obscure the new ones. Back up the volume, then either copy the changed files in deliberately or remove it with
docker volume rmand let the new image populate a fresh one. docker volume rmrefuses to remove a volume because a container is using it- Docker will not remove a volume that is in use, and stopped containers count. Find them with
docker ps -a --filter volume=<name>, remove those containers, then remove the volume.
How it shows up in production
A service runs in production with a named volume over its config directory. It works, because the first run copied the image's default config into the empty volume.
A developer reproduces it locally with a bind mount of an empty directory, to edit config by hand. The service immediately fails to find its own default files, and a long investigation goes into the image build — which is correct.
Separately, the team ships a release that changes a default in that config directory. Production does not change behaviour, because the existing volume is not empty and so is never repopulated. The new defaults are sitting in the image, hidden behind the old files.
Both incidents are the same rule: only an empty volume is filled from the image. Anything else covers the image's files at that path.
How to tell which one you are hitting
- Do you need to see or edit the files from the host?
- Then a bind mount — that is its purpose. If only containers need the data, Docker's documentation prefers a volume, and the documentation recommends against volumes when host access is needed.
- Does the image already have files at the path you are mounting over?
- If so, only an empty volume preserves them, by copying them in on first use. A bind mount or a non-empty volume hides them.
- Did files go missing right after changing a mount, or did an upgrade seem to change nothing?
- That is a mount covering the image's files, not a broken image. Inspect what is mounted where before rebuilding anything.
- Does the container need to write to the host directory at all?
- If not, mount it read-only. A bind mount grants write access to host files by default, which the documentation flags as a security consideration.
Security considerations
- A bind mount is writable by default. A process in the container can create, change or delete files in that host directory, and Docker warns this can affect non-Docker processes on the host. Add
ro(-v "$(pwd)"/config:/app/config:ro) whenever the container only needs to read. - Bind-mounting broad host paths gives the container that much of the host. Docker's security page notes that a container given the host's
/can alter the host filesystem without any restriction, which is why only trusted users should control the Docker daemon. - On SELinux hosts, the
zandZoptions relabel the host files themselves. Docker warns that bind-mounting a system directory such as/homeor/usrwithZrenders the host inoperable, so use them only on directories that belong to the container. - Volumes outlive their containers, and so does whatever they hold. Remove a volume that held secrets with
docker volume rm;docker volume pruneremoves only unused anonymous volumes unless you add--all. - When several containers share a volume, each read-write mount can change data the others rely on. Mount it read-only in the containers that only read, which Docker supports on the same volume at the same time.
In an interview
How this gets asked in an interviewpreparation
Usually phrased as When would you use a Docker volume instead of a bind mount?
What a strong answer contains Volumes for data only containers need — databases, uploads — because Docker manages them and they avoid tying the container to the host's directory layout. Bind mounts when the host needs the files too, typically development. The answer that shows experience names the population rule: an empty volume is filled with the image's existing files at that path, while a bind mount or a non-empty volume obscures them — which explains both missing files after switching to a bind mount and upgrades that seem not to apply.
The follow-up "You upgraded the image but the container still uses the old default config. Why?" The config directory is on a volume that was populated by the old image. A non-empty volume is not repopulated, so the old files obscure the new image's.
Sources
The behaviour this page describes, as documented by the projects themselves.
- Docker docs: docker container run, mount volume (-v)
- Docker docs: docker volume rm
- Docker docs: docker volume prune
- Docker docs: docker container ls (volume filter)
- Docker docs: Docker Engine security
- Docker Engine 28 release notes (relative parent paths in 28.2.0)
- Docker Engine 29 release notes (bind-create-src in 29.3.0)
- Docker — Storage
- Docker — Volumes
- Docker — Bind mounts
Did this get you to an answer?
No text box on purpose — please do not paste production logs anywhere
Something wrong or out of date? Report it