CoursesDocker for beginnersPersisting data: volumes & bind mounts

Persisting data: volumes & bind mounts

Keep data when a container is gone.

Beginner12 min · lesson 12 of 16
In plain terms
A container’s own storage is like a hotel room — housekeeping wipes it the moment you check out. A volume is the storage locker down the hall that stays yours between visits. Keep anything you care about in the locker, not the room.

Every container starts life as a copy of an image. An image is a read-only template, meaning nothing inside it can be changed, a bit like a printed recipe card; a container is one meal cooked from that card. While it runs, each container gets a thin scratch space of its own to write files into. Docker calls that the writable layer. Here is the part that bites people. Docker throws the scratch space away the second you delete the container. A hotel room works the same way. Housekeeping strips it the moment you check out, and the notepad you scribbled on goes in the bin. For a web server holding nothing worth keeping, fine. For a database, that is a very bad day.

To keep data, you park it outside the container and ask Docker to wire that outside storage to a path inside. Two tools do that job, and beginners swap them constantly. A named volume is storage Docker creates and looks after for you, tucked into its own corner of the disk. A bind mount points at a real folder on your own machine (the host, meaning the computer Docker itself runs on) and shares that folder into the container. Same idea, different owner. Docker owns the volume. You own the folder behind a bind mount.

Watch a file vanish

The fastest way to believe any of this is to break it yourself. You will start a tiny container, write a file inside it, delete the container, then start a fresh one and go hunting for that file. Alpine does the job here: a stripped-down version of Linux, only a few megabytes, ideal for throwaway tests.

terminal
$ docker run --name note alpine sh -c 'echo "buy milk" > /reminder.txt; cat /reminder.txt'
output
buy milk

There it is, sitting inside the container. Now delete that container and start a brand-new one from the same image.

terminal
$ docker rm note
$ docker run --name note alpine cat /reminder.txt
output
note
cat: can't open '/reminder.txt': No such file or directory

Read that output slowly. The first line, note, is docker rm reporting back the name of what it removed. The second line is the new container failing to find /reminder.txt. Nothing malfunctioned. Your text went into the first container's writable layer, and docker rm binned that layer along with the container. The replacement booted from the same untouched image, so it never had your file to begin with.

Keep it with a named volume

Now give the data a home outside the container. A named volume is the storage locker down the corridor from your hotel room. It stays yours between visits, and housekeeping has no key. You create the locker once, then tell each container to mount it, which means attach it at a chosen path. Everything written under that path lands in the locker instead of the throwaway layer.

terminal
$ docker volume create notes
$ docker run --name keep -v notes:/data alpine sh -c 'echo "buy milk" > /data/reminder.txt'
$ docker rm keep
$ docker run --rm -v notes:/data alpine cat /data/reminder.txt
output
notes
keep
buy milk

Same story as before, one extra flag. The -v notes:/data part attaches the volume called notes to /data inside the container. You wrote the file under /data, deleted the container, then started a second container pointing at that same volume, and the file was sitting there waiting. The data outlived the container because it never lived inside the container at all. (The --rm on the last command cleans up that short-lived container once it has printed; the volume stays exactly where it is.)

Bind mounts: share a folder from your machine

A bind mount is a shared drive you leave plugged in. Save a file on your side and the container sees the new version straight away, with no rebuild. That is why bind mounts run local development almost everywhere. Here you write a web page on your machine and hand it to an Nginx container to serve. Nginx is a very widely used web server, and it looks for pages in /usr/share/nginx/html.

terminal
$ echo "<h1>hi</h1>" > index.html
$ docker run -d --name site -p 8080:80 -v "$(pwd)":/usr/share/nginx/html:ro nginx:1.27
$ curl -s localhost:8080
output
3f9a1c7b2e8d4a6f0b1c2d3e4f5a6b7c8d9e0a1b2c3d4e5f6a7b8c9d0e1f2a3b
<h1>hi</h1>

The "$(pwd)" bit expands to your current folder, and Docker shares that exact folder into the container. The -d flag runs the container in the background, which is why the long string of hex characters comes back: that is the container's ID. The -p 8080:80 part publishes it at localhost:8080 so a browser can reach it, and curl (a command-line tool that fetches a web page) prints what a browser would render. The :ro on the end means read-only, so the container can serve your files but cannot change them, a sensible default for a mount like this. Edit index.html on your machine, hit refresh, and the new text appears with no restart. That live link is the whole point of a bind mount.

Rule of thumb: reach for a named volume when Docker should own real data, like a database's files or user uploads that have to survive a restart. Reach for a bind mount when you want your machine and the container staring at the same folder on purpose, which is nearly always development work. There is a third kind, tmpfs, short for temporary filesystem, which lives in memory and never touches the disk, so it disappears the moment the container stops. Handy for scratch files, or a secret you would rather never write to a drive. To see which volumes exist and where one actually sits on disk, ask Docker.

terminal
$ docker volume ls
$ docker volume inspect notes --format '{{.Mountpoint}}'
output
DRIVER VOLUME NAME
local notes
/var/lib/docker/volumes/notes/_data
A bind mount covers up whatever was already there
Mount a folder onto a path that already holds files from the image and your folder wins. The image's own files drop out of view for as long as the mount is attached. Put an empty folder over /usr/share/nginx/html and the page comes back blank. Nothing crashed. Nginx is looking into your empty folder and finding nothing to serve. Named volumes play by a different rule: an empty named volume gets seeded with a copy of the image's files the first time it is used. Check your paths before you go blaming the app.
Same delete, different fate
Same delete, different fate
One delete, three outcomes. Whether your data survives docker rm comes down to where the container was told to write.

That is the entire decision, and you make it at the moment you start the container. A write goes to the writable layer by default, and the layer dies when the container is removed. Point the same write at a volume or a bind mount and it lands somewhere docker rm cannot reach. Database files, uploaded images, build caches: all of them need that second route.

Bind mounts have sharper edges in production. The host path you type has to exist on every machine that runs the container, with the same contents and the same permissions, and that stops being true the minute a colleague's laptop or a server with a different disk layout runs it. Named volumes carry no such assumption, because Docker picks the location itself and creates it if it is missing.

Give each kind one job and the confusion goes away. Application code belongs in the image, baked in when you build. Durable data belongs in a named volume. Source files you are actively editing belong in a bind mount, and only while you are developing. tmpfs is for scratch space you actively want to lose. Blur those roles and you write the opening line of an "I deleted the container and lost the database" story.

Permissions catch people out on bind mounts. A process running as a non-root user inside the container often cannot write into a folder owned by your desktop account on the host, and the reverse trips people up too. Named volumes tend to start with ownership that matches what the image expects, so they stay quiet. When a write fails with Permission denied, compare the user ID (the number Linux uses to identify an account) inside the container against the one that owns the folder outside, before you start rewriting the app.

Run the vanishing-file demo once with your own hands, at a moment when nothing is at stake. Write a file in a plain container, remove the container, start a new one, watch the file be missing. Then run the same thing with -v mydata:/data and watch it survive. The muscle memory from those two runs is what stops you mounting nothing underneath a database six months from now.

Try this

Prove the loss first, then prove the fix. One file dies with its container; the other outlives two of them.

terminal
docker run --rm alpine:3.20 sh -c "echo gone > /tmp/x; cat /tmp/x"
docker volume create keepdemo
docker run --rm -v keepdemo:/data alpine:3.20 sh -c "echo survives > /data/x"
docker run --rm -v keepdemo:/data alpine:3.20 cat /data/x
docker volume rm keepdemo
output
gone
keepdemo
survives

Takeaway

A container's own filesystem is scratch paper. Name a volume (-v notes:/data) for anything you would be upset to lose, bind mount a host folder when you want your edits to show up live, and treat every other write as something docker rm will take with it.

Quick check
01You wrote /reminder.txt inside a plain Alpine container with no volume attached, ran docker rm, then started a fresh container. Why did the new one come up empty?
Correct — With no volume attached, the write went into throwaway scratch space, and docker rm took it away with the container.
Incorrect — docker rm removes a container and leaves the image alone. The very same image was reused for the new container.
Incorrect — Networking has nothing to do with where files sit on disk. An IP address cannot explain a missing file.
Incorrect — Alpine stores files perfectly well. The file went missing because it lived in the container's writable layer, not because the image is small.
02The lesson uses both a named volume (docker volume create notes) and a bind mount (-v "$(pwd)":/usr/share/nginx/html). What separates the two?
Correct — Ownership is the whole difference. Docker owns the volume; you own the folder behind a bind mount.
Incorrect — Both outlive the container. A bind mount is a folder on your own machine, and docker rm never touches it.
Incorrect — Read-only is a choice you make by adding :ro, and it works on either kind. It is not what separates them.
Incorrect — That describes tmpfs. Named volume data sits on disk under /var/lib/docker/volumes.
03You bind-mount an empty folder from your machine onto /usr/share/nginx/html, a path that already held Nginx's default files in the image. What loads in the browser, and why?
Incorrect — Nothing merges. Your folder covers the path, and the image's own files drop out of view while the mount is attached.
Incorrect — Nginx keeps running happily. It is serving from an empty directory, which is not a crash.
Correct — A bind mount covers whatever already sat at that path, so Nginx can see only your empty folder.
Incorrect — That seeding behavior belongs to an empty named volume, not to a bind mount.

Related