Docker Volumes and Bind Mounts
Anything written inside a container dies with it. Volumes and bind mounts are the two ways to stop that, and they are not interchangeable - this guide shows what each is for, proves data survives, and demonstrates a read-only mount refusing a write.
Storage and Networking Guide 12 of 46 Beginner
- OSUbuntu 26.04 LTS (resolute)
- Docker Engine29.7.2
- Shellbash
- Architectureamd64
- TimeAbout 13 min
- Reviewed20 August 2026
Tested on the versions above. Volume mountpoints, container IDs and file timestamps differ per host. Match the shape of the output, not the exact strings.
| Server Name | IP Address | OS | Roles | CPU | RAM | HDD |
|---|---|---|---|---|---|---|
| DOCKER01 | 192.168.0.21 | Ubuntu 26.04 LTS | Docker Host | 2 Core | 4 GB | 50 GB |
Before you start
- Docker Engine running - guides 1 and 3 in this path.
- A writable directory under /tmp for the bind-mount exercise.
- Host port 8081 free.
-
Create a named volume
A volume is storage Docker manages for you, living under the Docker root directory. You never need to know the host path to use it, which is exactly the point - the volume is addressed by name, so the same command works on any host.
bash docker volume create cg-datacg-datadocker volume ls --filter name=cg-DRIVER VOLUME NAMElocal cg-dataExpected resultThe volume name echoed, then one row in the listing.
Success condition
cg-dataappears with driverlocal. The volume exists independently of any container. -
See where it actually lives
The mountpoint is on the host filesystem under Docker's root. You can look, but treat it as Docker's private storage - manipulating it directly behind Docker's back is how volumes end up in odd states.
bash Example session docker volume inspect cg-data --format "{{.Mountpoint}} driver={{.Driver}}"/var/lib/docker/volumes/cg-data/_data driver=localExpected resultA path under /var/lib/docker/volumes and the driver name.
Success conditionYou get a path. On Docker Desktop this path is inside the VM, not on your laptop's filesystem.
-
Prove the data outlives the container
This is the whole point, so demonstrate it rather than trusting it. The first container writes a file and is deleted immediately -
--rmremoves it on exit. A completely separate container then reads the file back.bash Example session docker run --rm -v cg-data:/data alpine sh -c "echo hello-from-volume > /data/note.txt; ls -l /data"total 4-rw-r--r-- 1 root root 18 Aug 20 05:23 note.txtdocker run --rm -v cg-data:/data alpine cat /data/note.txthello-from-volumeExpected resultThe file listed by the first container, then its contents read by a second, unrelated container.
Success conditionThe second container prints the text. Both containers are gone; the data is not.
-
Bind mounts map a host directory instead
A bind mount points at a specific path you control on the host. Use it when you want to see and edit the files yourself - local development, or serving content you maintain. The trade-off is portability: the path must exist on every machine that runs the container.
bash Example session mkdir -p /tmp/cg-site && echo "<h1>Bind mount works</h1>" > /tmp/cg-site/index.htmldocker run -d --name cg-bind -p 8081:80 -v /tmp/cg-site:/usr/share/nginx/html:ro nginx:alpine9b75eaf23d489f4932c941c15c219ae04fecae38117346c64fdd0059c7a43232curl -s http://localhost:8081<h1>Bind mount works</h1>Expected resultThe HTML you wrote on the host served back by nginx in the container.
Success conditioncurl returns your content. Edit the file on the host and reload - the change appears with no rebuild and no restart.
-
Read-only mounts are enforced by the kernel
The
:rosuffix mounts the source read-only inside the container. This is not advisory - a write is refused by the filesystem itself. Mount content read-only whenever the container has no legitimate reason to modify it.bash docker exec cg-bind sh -c "touch /usr/share/nginx/html/x"touch: /usr/share/nginx/html/x: Read-only file systemdocker inspect cg-bind --format "{{json .Mounts}}"[{"Type":"bind","Source":"/tmp/cg-site","Destination":"/usr/share/nginx/html","Mode":"ro","RW":false,"Propagation":"rprivate"}]Expected resultThe write refused, and
"RW":falsein the mount record confirming why.Success condition
Read-only file system. If the write succeeded, the:rosuffix was omitted - check the Mode field. -
Clean up
Removing a volume destroys its contents permanently and Docker will not ask twice. Name volumes explicitly; never reach for a blanket prune while learning.
bash # docker volume rm DELETES THE DATA. There is no undo and no confirmation prompt.docker rm -f cg-bindcg-binddocker volume rm cg-datacg-dataExpected resultThe container name, then the volume name.
Success condition
docker volume lsno longer lists cg-data. The bind-mounted directory /tmp/cg-site still exists - Docker never owned it.
Troubleshooting
The container starts but the mounted directory appears empty
Why: Mounting over a directory hides whatever the image had there. If the host directory is empty, the container sees an empty directory.
Fix:Check the host path has the files you expect, and confirm the destination is the path the application actually reads.
bash ls -l /tmp/cg-sitedocker exec cg-bind ls -l /usr/share/nginx/htmlPermission denied writing to a bind mount
Why: The user inside the container has a different UID from the owner of the host directory. The kernel compares numeric IDs, not names.
Fix:Match the UID with
--user, or adjust ownership on the host directory. Prefer a named volume when the host path does not need to be human-editable - Docker handles ownership for you.bash stat -c "%u:%g %n" /tmp/cg-sitedocker exec cg-bind idA relative path in -v created something unexpected
Why: Docker treats a bind-mount source without a leading slash as a volume name, not a path, so it silently creates a named volume instead.
Fix:Always use an absolute path for bind mounts, or the explicit
--mount type=bind,source=...,target=...form, which errors instead of guessing.bash docker run --rm --mount type=bind,source=/tmp/cg-site,target=/site:ro alpine ls /site