Why container data is ephemeral
A container has two file layers:
- Image layers โ read-only, shared between containers from the
same image.
- Container writable layer โ created on top when the container
starts, destroyed when the container is removed.
Demo the problem:
# Start a container and write data to the scratchpad layer
docker run -d --name eph alpine sh -c "echo hello > /data && sleep 600"
# Verify the data exists (prints: hello)
docker exec eph cat /data
# Remove the container (discarding its writable layer)
docker rm -f eph
# Start a fresh container from the same base image
docker run -d --name eph alpine sh -c "sleep 600"
# Verify the data is gone (prints: No such file or directory)
docker exec eph cat /data 2>&1
Data didn't survive the container's death. That's by design โ the
read-only base image is unchanged, the writable scratchpad is
discarded. For anything you want to keep (databases, uploaded
files, build caches) you need either a volume or a bind
mount.
Click Verify step when you've reproduced the disappearance.
Hint
Containers are immutable; their writable layer is deleted with the container.
Named volumes
A named volume is managed by Docker on the host filesystem. It
lives independently of any container โ containers come and go,
volumes stay.
Create one + mount it into a fresh nginx:
docker volume create app-data
docker volume ls
docker run -d --name web \
--mount source=app-data,target=/usr/share/nginx/html \
-p 8300:80 \
nginx:alpine
Write something into the volume via the container:
docker exec web sh -c 'echo "<h1>persisted</h1>" > /usr/share/nginx/html/index.html'
curl -s http://localhost:8300/
Now blow up the container โ but NOT the volume:
docker rm -f web
Start a brand-new container, mount the same volume:
docker run -d --name web2 \
--mount source=app-data,target=/usr/share/nginx/html \
-p 8301:80 \
nginx:alpine
curl -s http://localhost:8301/
You should see persisted โ the new container reads the same files
the previous one wrote. That's the volume earning its keep.
Volumes also support the older -v syntax โ same effect, terser:
# docker run -d -v app-data:/usr/share/nginx/html nginx:alpine
Click Verify step.
Hint
`docker volume create`, then `--mount source=<vol>,target=<path>` at run.
Bind mounts
A bind mount mounts a specific host directory into the
container. Unlike named volumes, the host path is literal and
visible โ useful for dev (edit host files, see them in the
container immediately).
Create a host directory with content:
mkdir -p ~/site
cat > ~/site/index.html <<'HTML'
<!doctype html>
<html><body><h1>served from a bind mount</h1></body></html>
HTML
Mount it at /usr/share/nginx/html:
docker run -d --name dev-web \
-v ~/site:/usr/share/nginx/html:ro \
-p 8310:80 \
nginx:alpine
curl -s http://localhost:8310/
Now edit the host file โ the container immediately serves the new
content:
echo "<h1>edited live</h1>" > ~/site/index.html
curl -s http://localhost:8310/
The :ro flag mounts read-only. That's the safer default for
shipping config into a container โ code in the container can't
accidentally overwrite your host files.
Volumes vs bind mounts โ when to use which:
| | Volume | Bind mount |
|--|--|--|
| Path | Docker manages | You pick |
| Portable | Yes | Tied to host paths |
| Backups | docker run --rm -v vol:/x alpine tar ... | Just tar the host dir |
| Best for | Databases, caches, app data | Local dev hot-reload, config files |
Click Verify step when :8310 serves "edited live".
Hint
`-v $(pwd):/app` mounts a host path. Read-only is `:ro`.
Inspect and clean up
A few daily-driver storage commands.
List + inspect:
docker volume ls
docker volume inspect app-data
docker volume inspect --format '{{ .Mountpoint }}' app-data
The mountpoint is the real directory on the host. For
operational debugging you can ls it directly:
ls $(docker volume inspect --format '{{.Mountpoint}}' app-data)
Tag volumes with labels so prune doesn't kill them:
docker volume create --label keep=true important-data
docker volume ls --filter label=keep
Prune โ remove unused volumes:
docker volume prune -f
docker volume ls
prune deletes only dangling volumes (no container is mounting
them). To wipe a specific one:
docker volume rm important-data
If you try to remove a volume that's still in use, you get an
explicit error naming the using container.
Click Verify step when app-data still exists and contains
the persisted content from step 2.
Hint
`docker volume ls`, `docker volume inspect`, `docker volume prune`.