Week 3: Volumes & Persisting Container Data

Delete a container and its writable layer goes with it — including any data your app wrote while it was running. That's a feature, not a bug, right up until you run a database that way and lose everything on the next redeploy. This week covers the three ways Docker persists data outside a container's disposable filesystem, and when to reach for each one.

Module 3 of 17 Week 3 of 18 ~3 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Explain why a container's filesystem disappears when the container is removed
  • Choose correctly between named volumes, bind mounts and tmpfs
  • Run a stateful database in a container that survives being recreated

1. The Ephemeral Filesystem

Every container gets a thin, writable layer on top of its (read-only) image layers. Anything the process writes — log files, an uploaded image, a SQLite database — lands in that writable layer. The moment you run docker rm, that layer is gone permanently.

watch it happen
docker run -it --name scratch alpine sh
# inside the container:
echo "important data" > /data.txt
exit

docker start -ai scratch    # restart the SAME container — data.txt is still there
docker rm -f scratch        # remove it entirely
docker run -it --name scratch alpine sh
cat /data.txt   # No such file — it's genuinely gone, this is a brand new container

Notice the distinction: stopping and restarting the same container preserves its writable layer — the container still exists, just paused. Removing the container destroys that layer for good. This is exactly the behavior you want for a stateless web server (throw it away, start a fresh one, no problem) and exactly the behavior that will delete your production database if you don't do something about it.

This is a deliberate design goal, not an oversight

Disposability is what makes containers so easy to reason about — you should be able to docker rm -f any stateless container and recreate it identically. The fix for stateful data isn't "avoid removing containers," it's "put the data somewhere that isn't the container's writable layer" — which is exactly what volumes are for.

2. Named Volumes

A named volume is storage managed entirely by Docker, living outside any single container's lifecycle. It's the recommended way to persist data for most stateful workloads, because Docker handles where it physically lives.

creating and using a named volume
docker volume create my-data
docker volume ls
docker volume inspect my-data

docker run -d --name app1 -v my-data:/app/data alpine sleep 3600
# -v my-data:/app/data   mount the "my-data" volume at /app/data inside the container

docker rm -f app1                                    # remove the container...
docker run -d --name app2 -v my-data:/app/data alpine sleep 3600
# ...the volume, and its data, survives and is now attached to app2

You can also mount a volume into multiple containers simultaneously — a common pattern for sharing files between a web server and a background worker, or for a sidecar container that processes logs written by the main one.

volume lifecycle commands
docker volume ls                 # list all volumes
docker volume rm my-data         # delete a volume (fails if it's in use by a container)
docker volume prune              # delete ALL unused volumes — use carefully
Volumes outlive containers, but not your disk

A named volume isn't backed up automatically — it's still just a directory on the host (or a volume driver's backend). Losing the host or accidentally running docker volume prune loses the data. Real production databases still need a separate backup strategy on top of volumes.

3. Bind Mounts & tmpfs

A bind mount maps a specific path on your host directly into the container — unlike a named volume, Docker doesn't manage it, you point at an exact host directory. This is the tool you reach for during local development, so code edits on your host show up instantly inside the running container:

bind mount for live-reload development
docker run -d --name dev-app \
  -v $(pwd)/src:/app/src \
  -p 3000:3000 \
  my-app:dev

# Edit a file in ./src on your host — the running container sees the change
# immediately, since it's literally the same files on disk, not a copy.
the third option: tmpfs
docker run -d --tmpfs /app/cache my-app:1.0
# tmpfs mounts live ONLY in memory — never written to disk, gone the instant
# the container stops. Useful for secrets you don't want touching disk, or
# scratch space that needs to be fast and doesn't need to survive a restart.
choosing between the three
Named volume  # production data (databases, uploads) — Docker-managed, portable
Bind mount    # local development — live host-file editing, host-path-specific
tmpfs         # ephemeral, sensitive, or performance-critical scratch data — memory only
Bind mounts are a portability trap

A bind mount path like C:\Users\you\project\src or /home/you/project/src only exists on your exact machine — it can't be part of a portable, shareable image or Compose file that a teammate or a production server can just run. That's exactly why named volumes, not bind mounts, are the production answer.

4. A Real Database, Persisted

Let's run PostgreSQL correctly — the way that survives a container restart, a container removal, and an image upgrade:

postgres with a named volume
docker volume create pg-data

docker run -d \
  --name my-postgres \
  -e POSTGRES_PASSWORD=devpassword \
  -e POSTGRES_DB=appdb \
  -v pg-data:/var/lib/postgresql/data \
  -p 5432:5432 \
  postgres:16

# Connect and create some data
docker exec -it my-postgres psql -U postgres -d appdb -c \
  "CREATE TABLE notes (id serial PRIMARY KEY, body text); INSERT INTO notes (body) VALUES ('hello');"

# Prove persistence: remove and recreate the container against the SAME volume
docker rm -f my-postgres
docker run -d --name my-postgres -e POSTGRES_PASSWORD=devpassword \
  -v pg-data:/var/lib/postgresql/data -p 5432:5432 postgres:16

docker exec -it my-postgres psql -U postgres -d appdb -c "SELECT * FROM notes;"
# -> your row is still there

The key detail is knowing which path inside the image actually needs to be persisted — for Postgres it's /var/lib/postgresql/data, documented on every official database image's Docker Hub page. Mount a volume anywhere else and you'll lose data exactly the same as if you'd used no volume at all.

Looking ahead to Kubernetes

This same problem — "how does a Pod, which is just as disposable as a container, keep its data?" — reappears in Week 11 as PersistentVolumes and PersistentVolumeClaims. The concept transfers directly: name the storage, mount it at the right path, and it survives the workload being recreated.

5. Hands-on Exercise

Hands-on

Run PostgreSQL two ways, and prove which one survives

Deliberately reproduce a data-loss bug, then fix it with a named volume, so the difference is something you've seen rather than just read about.

Part 1 — Reproduce the data loss:

  1. Run docker run -d --name broken-db -e POSTGRES_PASSWORD=devpw postgres:16 — deliberately with no volume mounted.
  2. Connect with docker exec -it broken-db psql -U postgres and create a table with a few rows of data.
  3. Remove the container: docker rm -f broken-db, then start a brand new one with the exact same command.
  4. Try to query your table again — confirm it's gone, and write one sentence explaining exactly why, in terms of what actually got deleted.
Hint

Use \dt inside the psql prompt to list tables quickly, and \q to exit back to your shell.

Part 2 — Fix it, and prove the fix under a harder scenario:

  1. Create a named volume and rerun Postgres correctly, mounted at /var/lib/postgresql/data, recreating your table and data.
  2. Remove the container and recreate it against the same volume — confirm the data survives, same as the walkthrough above.
  3. Now go further: run docker volume inspect on your volume and find the actual host path where Docker is storing the data (the "Mountpoint" field). Note it down.
  4. Finally, simulate an image upgrade: remove the container and recreate it using postgres:15 instead of postgres:16, still pointed at the same volume. Does your data still show up? What does this tell you about how tightly (or loosely) a named volume is coupled to a specific image version?
Hint

A volume is just files on disk — it doesn't know or care which image wrote them, which is exactly why real database version downgrades can be dangerous even though the volume "just works" for an upgrade. Data format compatibility across versions is your problem, not Docker's.

6. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

Why does data written inside a container disappear after docker rm, but not after docker stop?

docker stop pauses the container but keeps its writable layer intact on disk — starting it again with docker start resumes with the same data. docker rm deletes the container object entirely, including its writable layer, so any data that lived only there is gone permanently.

Q2

You're setting up local development with live-reload on code edits. Named volume or bind mount?

Bind mount. It maps an exact host directory into the container, so edits made in your editor on the host appear inside the running container immediately — exactly what live-reload needs. Named volumes are Docker-managed and better suited to portable, production data rather than pointing at a specific local source folder.

Q3

What's the defining property of a tmpfs mount?

It exists only in memory and is never written to disk — the data vanishes the instant the container stops. It's useful for sensitive scratch data you don't want persisted anywhere, or for performance-sensitive temporary files, but it offers zero persistence, even across a simple restart.

Q4

Why does the exact mount path matter when persisting a database's data with a volume?

The database only writes its actual data files to one specific directory inside the image (for Postgres, /var/lib/postgresql/data). Mounting a volume anywhere else persists nothing useful — the database still writes its real data to the container's disposable writable layer, and it's lost exactly as if no volume existed at all.