Persist Data with Docker Volumes
Stop losing data when a container is removed. Learn Docker's three storage types — named volumes, bind mounts and tmpfs — by persisting a real PostgreSQL database, inspecting a volume, editing files live through a bind mount, and using volumes in Compose.
Problem
You run a database in a container, insert important rows, then remove the container to upgrade it — and every row is gone. This isn't a bug: a container's writable layer is **ephemeral**. It lives and dies with the container. When you `docker rm` a container, its writable layer — and everything your app wrote there — is deleted permanently. That's fine for a stateless web server, but catastrophic for a database, uploaded files, or any state you care about. The fix is to store data **outside** the container's lifecycle, in storage that Docker manages separately: **named volumes** (Docker-managed, the preferred choice), **bind mounts** (a folder on your host, great for development), and **tmpfs** (in-memory, for sensitive or throwaway data). In this lab you'll prove the problem is real — write data, destroy the container, and watch a *new* container find the same data still there. You'll inspect where Docker keeps a volume on disk, edit a website live through a bind mount, and declare volumes in a `docker-compose.yml`. By the end you'll know exactly which storage type to reach for and why.
Objectives
- Explain why a container's writable layer is ephemeral and what gets lost on
docker rm - Choose correctly between named volumes, bind mounts and tmpfs for a given use case
- Create a named volume, mount it into a container, and prove data survives container removal
- Inspect a volume with
docker volume lsanddocker volume inspect, reading its Mountpoint and driver - Mount a host folder as a bind mount and edit files that reflect live inside a running container
- Declare a named volume and a bind mount in
docker-compose.yml, and back up a volume withtar
Prerequisites
- Docker installed and running (Docker Desktop on Windows/macOS, or Docker Engine on Linux). Run
docker version— if it prints a Client and a Server section, you are ready. - Docker Compose available (
docker compose version— bundled with Docker Desktop and modern Docker Engine) - A terminal (PowerShell, bash or zsh) and a web browser
- Internet access to reach Docker Hub for the image pulls
- Git installed, to submit your deliverable at the end
- You've completed the earlier Docker labs (containers, images, Compose, networking) — this is Lab 5, the last free one
What you will build
You won't write much application code in this lab. Instead you'll run a real PostgreSQL database and
an nginx web server, and attach storage that outlives the container. You'll write data, destroy
the container, and watch a brand-new container find the data intact — the proof that volumes work.
Then you'll inspect a volume on disk, edit a live website through a bind mount, and declare storage in
Docker Compose.
The one mental model: the container layer is ephemeral
Every container gets a thin writable layer on top of its read-only image. Anything the app writes
— database files, uploads, logs — lands there by default. The catch:
When you remove the container, that writable layer is deleted with it. The data is gone.
That's by design: containers are meant to be disposable. So persistent data must live outside the
container, in storage Docker manages independently. Docker gives you three options:
| Type | Where it lives | Managed by | Best for |
|---|---|---|---|
| Named volume | Docker's own area on the host (/var/lib/docker/volumes/) |
Docker | Databases, persistent app data — the default choice |
| Bind mount | A specific folder you pick on the host | You | Development: editing source/config live |
| tmpfs | Host RAM, never written to disk | Docker (in memory) | Sensitive or throwaway data (secrets, caches) |
How to choose
- Need data to survive container removal, and you don't care where on disk it sits? → named volume.
Docker manages it, it's portable across containers, and it's easy to back up. - Need to edit files on your host and see them instantly inside the container (live-reloading a site
or app in dev)? → bind mount. You point at an exact host folder. - Need scratch space that must never touch disk (a secret, a temporary cache)? → tmpfs. It lives
in RAM and vanishes when the container stops.
How to work through this lab
Do the steps in order — each builds on the last. Run every command yourself and read its real output.
Along the way you'll collect the evidence (proof of persistence, volume inspect, live bind-mount edit)
you need for the submission in Step 6.
Steps
-
The problem: the writable layer dies with the container
Before touching volumes, feel the pain they solve. You'll write a file inside a container, remove
the container, and confirm the file is gone.Prove data is lost
Run a throwaway Alpine container, write a file into it, then look at it:
docker run --name temp -d alpine:3.20 sh -c "echo 'important data' > /data.txt && sleep 3600" docker exec temp cat /data.txt # prints: important dataThe file exists — it lives in the container's writable layer. Now destroy the container and try to
get it back:docker rm -f temp docker run --rm alpine:3.20 cat /data.txt # error: no such fileThe new container is a fresh instance of the same image. It never saw your
data.txt, because that
file lived only in the writable layer of the removed container — which was deleted with it.The three storage types (and when to use each)
Docker's answer is to keep data outside the container's lifecycle. There are three mechanisms —
commit these to memory, because every later step is one of them:-
Named volumes — fully managed by Docker, stored in Docker's own area on the host. The preferred
way to persist data. Isolated from the host filesystem, easy to back up, migrate and share between
containers. Use for databases and any persistent app data. -
Bind mounts — a file or directory on the host is mounted into the container. Less isolation, more
flexibility; edits on either side are instantly visible on the other. Use for development — editing
source or config live. -
tmpfs mounts — stored in the host's memory, never written to disk. Ephemeral by definition. Use
for sensitive data you don't want on disk, or scratch data that needn't survive a restart.
Checkpoint
- Where did
data.txtactually live? (In the writable layer of thetempcontainer.) - Why couldn't the second container read it? (That layer was deleted with
docker rm; the new container has its own empty layer.) - Which storage type would you use for a PostgreSQL database? (A named volume — persistent and Docker-managed.)
Deliverable for this step: the two outputs — the successful
catinsidetemp, and the "no such file" error afterward. -
Named volumes — fully managed by Docker, stored in Docker's own area on the host. The preferred
-
Named volumes: prove data survives container removal
Now solve the problem. You'll create a named volume, run a PostgreSQL database that stores its
files there, add data, destroy the container, and bring up a new container on the same volume —
with your data intact.Create a volume
docker volume create pgdatapgdatais a Docker-managed named volume. It exists independently of any container.Run PostgreSQL using the volume
docker run -d --name lab-postgres \ -e POSTGRES_PASSWORD=minhasenha \ -v pgdata:/var/lib/postgresql/data \ postgres:16-alpine-
-v pgdata:/var/lib/postgresql/datamounts the named volumepgdataat the exact path where
PostgreSQL keeps its database files. The format isVOLUME_NAME:CONTAINER_PATH. - Because the left side is a plain name (not a path starting with
/or./), Docker treats it as a
named volume, not a bind mount.
Give it a few seconds to initialize, then create a table and insert a row:
docker exec -it lab-postgres psql -U postgres -c \ "CREATE TABLE alunos (id serial, nome text); INSERT INTO alunos (nome) VALUES ('Ada');" docker exec -it lab-postgres psql -U postgres -c "SELECT * FROM alunos;"You should see one row:
Ada. That data now lives in thepgdatavolume, not in the container's
writable layer.Destroy the container — keep the volume
docker rm -f lab-postgres docker volume ls # pgdata is still listedThe container is gone, but
pgdatasurvives. This is the whole point: removing a container does not
remove its named volumes.Bring up a NEW container on the same volume
docker run -d --name lab-postgres-2 \ -e POSTGRES_PASSWORD=minhasenha \ -v pgdata:/var/lib/postgresql/data \ postgres:16-alpine docker exec -it lab-postgres-2 psql -U postgres -c "SELECT * FROM alunos;"The
Adarow is still there — read by a completely different container. That's persistence: the
data outlived the container that created it.Deliverable for this step: the
SELECT * FROM alunos;output fromlab-postgres-2(a different container) showing the row survived. -
-
Inspect a volume: ls, inspect, Mountpoint
A volume isn't a black box. Docker gives you commands to list volumes and see exactly where and how
each one is stored.List available volumes
docker volume lsThis shows every volume, with its driver (usually
local) and name. You should seepgdata
in the list.Inspect a specific volume
docker volume inspect pgdataThe output is JSON. The fields that matter:
-
Name — the volume's name (
pgdata). -
Driver — the volume driver;
localfor standard volumes. -
Mountpoint — the path on the host where the volume's data actually lives (e.g.
/var/lib/docker/volumes/pgdata/_data). This is Docker's own managed area — you generally don't
touch it directly. - Labels — any labels attached to the volume.
-
Scope —
local(this host) orglobal.
What the Mountpoint tells you
The
Mountpointis where Docker stores the bytes. On Docker Desktop (Windows/macOS) this path lives
inside Docker's Linux VM, so you won't find it in your normal file explorer — that's expected and
correct. Inspecting the Mountpoint is useful to:- confirm where a volume's data is stored,
- diagnose storage-related issues,
- gather info for backup or migration (you'll use this in Step 5).
A caution from the manual: editing files directly at the host Mountpoint can corrupt data a running
container is using. Let the container (or a backup command) be the one that touches it.Deliverable for this step: the
docker volume inspect pgdataoutput, with theMountpointline highlighted. -
Name — the volume's name (
-
Bind mounts: edit a file on the host, see it live in the container
A named volume is Docker-managed and lives in Docker's own area. A bind mount is different: you
point the container at an exact folder on your host, and both sides see the same files in real
time. This is the tool for local development.Create a site folder on the host
mkdir -p lab-volume/site echo "<h1>Version 1 — served from a bind mount</h1>" > lab-volume/site/index.htmlServe it with nginx via a bind mount
docker run -d --name site -p 8080:80 \ -v "$(pwd)/lab-volume/site:/usr/share/nginx/html:ro" \ nginx:1.27-alpine- The left side of
-vis now a path ($(pwd)/lab-volume/site), so Docker treats it as a bind
mount, not a named volume. - It's mounted at nginx's web root,
/usr/share/nginx/html. -
:romounts it read-only inside the container — nginx only needs to read the files. Dropping
:rowould let the container write back to your host folder.
On Windows PowerShell, replace
$(pwd)with${PWD}. On classic Windows CMD, use%cd%.Open http://localhost:8080 — you'll see "Version 1".
Edit on the host, refresh in the browser
Without touching the container, edit the file on your host:
echo "<h1>Version 2 — edited live on the host</h1>" > lab-volume/site/index.htmlRefresh http://localhost:8080 — it now says "Version 2". You never rebuilt or restarted the
container. The bind mount means the container reads the same file you just edited on the host.Named volume vs bind mount — the practical difference
Named volume Bind mount Location Docker-managed area (you don't pick it) An exact host path you choose Isolation High — isolated from host FS Low — it is a host folder Best for Databases, persistent data, backups Live-editing source/config in dev Portability Portable, easy to back up Tied to this host's path layout Rule of thumb: bind mounts for development (you want to edit files), named volumes for data you
must persist (you want Docker to manage it).Deliverable for this step: note that editing
index.htmlon the host changed the page from "Version 1" to "Version 2" with no container restart. - The left side of
-
Volumes with Docker Compose + tmpfs + backup
In real projects you don't type long
docker run -vcommands — you declare storage in
docker-compose.yml, where it's versioned and reproducible. You'll wire up a named volume and a bind
mount, meet tmpfs, and back up a volume.Declare a named volume in Compose
In
lab-volume/, createdocker-compose.yml:services: postgres: image: postgres:16-alpine environment: POSTGRES_PASSWORD: minhasenha volumes: - pgdata:/var/lib/postgresql/data # named volume (top-level, below) ports: - "5432:5432" web: image: nginx:1.27-alpine volumes: - ./site:/usr/share/nginx/html:ro # bind mount (host path) ports: - "8080:80" volumes: pgdata:Two kinds of storage in one file:
-
pgdata:/var/lib/...— the left side is a plain name, so it's a named volume. It must also
be declared in the top-levelvolumes:block, which is what tells Compose to manage it. -
./site:/usr/share/...— the left side is a path (./site), so it's a bind mount to the
folder you created in Step 4.
Bring it up, then prove persistence across a full teardown:
docker compose up -d # ... create a table / add data via psql as in Step 2 ... docker compose down # stops AND removes the containers docker compose up -d # fresh containers, same pgdata volume # ... query again: the data is still there ...docker compose downremoves the containers but not the named volume, so the data survives. (To
also delete volumes you'd have to saydocker compose down -v— a useful reset, and a dangerous typo.)tmpfs — for sensitive or throwaway data
When data must never touch disk (a secret, a scratch cache), use tmpfs — it lives in RAM and is
wiped when the container stops. In Compose:web: image: nginx:1.27-alpine tmpfs: - /tmp # in-memory, gone on container stopAnything written to
/tmpexists only while the container runs; stop it and the data is discarded.
That's a feature for secrets and caches, not a place for anything you need to keep.Back up a volume
Because a named volume is just a directory Docker manages, you back it up by tarring its contents from
a throwaway container:docker run --rm \ -v pgdata:/data \ -v "$(pwd):/backup" \ alpine:3.20 tar czf /backup/pgdata-backup.tar.gz -C /data .This mounts
pgdataread-side at/data, mounts your current host folder at/backup, and writes
pgdata-backup.tar.gzto your host. To restore, you'd run the reverse: extract the tar into a fresh
volume. This tar-through-a-container trick is the standard way to back up and migrate Docker volumes.Security notes
-
Bind mounts expose a real host folder to the container — mind file permissions, and prefer
:rowhen the container only needs to read (as with the nginx site above). -
Sensitive data belongs in tmpfs (in memory) or behind proper encryption — not in a plain
bind mount on disk. - Use separate volumes for separate services unless they genuinely need to share data.
Deliverable for this step: your
docker-compose.yml, and proof that data survived adocker compose down+upcycle. -
-
Submit: document your persistence proof
Prove you made data outlive its container by capturing the evidence in a small repository.
Clean up first
docker rm -f lab-postgres-2 site 2>/dev/null docker compose down # if you have Compose running # keep or remove the pgdata volume as you like: # docker volume rm pgdataBuild your submission
Create a folder, initialize git, and write a file named
SUBMISSION.md. Paste the real outputs
you collected — not invented text.# Docker Lab 5 — Persist Data with Volumes ## 1. The problem (ephemeral layer) <paste the successful `cat /data.txt` inside `temp`, then the "no such file" error after `docker rm`> ## 2. Persistence proof (named volume) <paste `SELECT * FROM alunos;` from `lab-postgres-2` — a DIFFERENT container — showing the row survived> ## 3. docker volume inspect pgdata <paste the JSON; point out the Mountpoint line> ## 4. Bind mount live edit Editing `site/index.html` on the host changed the page from "Version 1" to "Version 2" with no restart. (yes / no) ## 5. Compose + persistence across down/up <paste your docker-compose.yml, and evidence the data survived `docker compose down` then `up -d`> ## Reflection (2-3 sentences) In my own words: why the writable layer is ephemeral, and when I'd choose a named volume vs a bind mount vs tmpfs.Submit
git init git add SUBMISSION.md docker-compose.yml git commit -m "Docker lab 5 — persist data with volumes" # push to a public repo or create a gist, then submit that URLSubmit the repository or gist URL as your lab deliverable.
Submission criteria (self-check — no stubs)
- Step 1 shows the real "no such file" error proving the writable layer is ephemeral
- The persistence proof is from a different container (
lab-postgres-2), not the one that wrote the data -
docker volume inspectoutput is included, with theMountpointidentified - You confirmed a host-side edit reached the container live through the bind mount
- Your
docker-compose.ymldeclares both a top-level named volume and a bind mount - You showed data surviving a
docker compose down+up -dcycle - Your reflection correctly states, in your words, when to use volume vs bind mount vs tmpfs
- All outputs are real command output you ran — no placeholder or invented text
What's next
Congratulations — that was the last free lab of the Docker track. You can now run containers,
build images, use Compose, connect containers over networks, and persist data with volumes: the full
core of day-to-day Docker.From here the track continues with premium content that takes you to production:
- Docker Security — hardening images and containers, non-root users, secrets, scanning.
- Docker in Production — observability with Prometheus and Grafana, healthchecks, resource limits.
- Kubernetes track — orchestrating containers at scale beyond a single host.
You've built the foundation; premium is where you take it to production.