Persisting data: volumes & bind mounts
Keep data when a container is gone.
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.
$ docker run --name note alpine sh -c 'echo "buy milk" > /reminder.txt; cat /reminder.txt'
buy milk
There it is, sitting inside the container. Now delete that container and start a brand-new one from the same image.
$ docker rm note$ docker run --name note alpine cat /reminder.txt
notecat: 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.
$ 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
noteskeepbuy 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.
$ 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
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.
$ docker volume ls$ docker volume inspect notes --format '{{.Mountpoint}}'
DRIVER VOLUME NAMElocal notes/var/lib/docker/volumes/notes/_data
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.
docker run --rm alpine:3.20 sh -c "echo gone > /tmp/x; cat /tmp/x"docker volume create keepdemodocker 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/xdocker volume rm keepdemo
gonekeepdemosurvives
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.