Chapter 5
Volumes & Data — Persisting State Beyond the Container
Every container starts with a fresh copy of its image filesystem. Writes go into a thin writable layer on top. When the container is removed, that layer is gone.
docker run -d --name db postgres:16
# write some data to the database...
docker rm -f db
# all data is gone — the writable layer was deleted with the containerFor stateless apps (most web servers), this is fine. For anything stateful — databases, file uploads, caches — you need storage that survives container restarts and deletions.
Three Ways to Mount Storage
Host filesystem
│
┌───┴────────────────────────────────────────────┐
│ Named Volumes (Docker-managed path) │
│ Bind Mounts (specific host path) │
│ tmpfs Mounts (in-memory only, never on disk) │
└────────────────────────────────────────────────┘
Named Volumes — Production Default
Docker manages the storage path. You don't need to know where it lives on the host.
docker volume create pgdata
docker run -d \
--name postgres \
-v pgdata:/var/lib/postgresql/data \
postgres:16The data lives at /var/lib/docker/volumes/pgdata/_data on the host. It persists across container restarts, docker stop, and even docker rm. Only docker volume rm pgdata or docker volume prune removes it.
docker volume ls # list all volumes
docker volume inspect pgdata # mountpoint, creation date
docker volume rm pgdata # delete the volume (fails if in use)
docker volume prune # delete all unused volumes (careful!)Bind Mounts — Local Development
Mount a specific host directory into the container. Changes on either side are reflected immediately — this is the standard local dev pattern.
docker run -d \
-v $(pwd):/app \
-p 3000:3000 \
my-app:devThe host's current directory is mounted at /app inside the container. Edit a file on your machine — the container sees it instantly. No rebuild needed.
The node_modules trick:
docker run -d \
-v $(pwd):/app \ # mount source code
-v /app/node_modules \ # anonymous volume: keeps container's node_modules intact
my-app:devWithout the second -v, the host's node_modules (or lack of it) would overwrite what was installed inside the container. The anonymous volume shadows /app/node_modules with the container's own version.
tmpfs Mounts — Sensitive or Ephemeral Data
In-memory only. Not written to disk. Disappears when the container stops.
docker run -d \
--tmpfs /tmp \
my-app:latestUse for:
- Sensitive data that should never touch disk (tokens, intermediate crypto material)
- High-speed temporary scratch space
Named Volumes vs Bind Mounts — When to Use Which
| Named Volume | Bind Mount | |
|---|---|---|
| Location | Docker-managed | Specific host path |
| Best for | Production, databases | Local dev, config injection |
| Portability | High — works on any Docker host | Low — tied to host directory |
| Performance | Optimized for containers | Slower on Mac/Windows (fsevents overhead) |
Use named volumes in production. Use bind mounts for local development. Avoid anonymous volumes (no name) in production — they're hard to track and easy to accidentally prune.
Practical Patterns
Database with Persistent Storage + Custom Network
docker network create app-net
docker run -d \
--name postgres \
--network app-net \
-v pgdata:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=secret \
-e POSTGRES_DB=myapp \
postgres:16
docker run -d \
--name api \
--network app-net \
-p 3000:3000 \
-e DATABASE_URL=postgresql://postgres:secret@postgres:5432/myapp \
my-api:latestapi reaches the database by name (postgres:5432 via DNS). Data survives container restarts via the pgdata volume.
Read-Only Mount
docker run -d \
-v $(pwd)/config:/app/config:ro \
my-app:latest:ro = read-only. The container can read config files but cannot modify them.
Sharing a Volume Between Containers
docker volume create shared-uploads
docker run -d --name app -v shared-uploads:/app/uploads my-app:latest
docker run -d --name nginx -v shared-uploads:/usr/share/nginx/html/uploads nginx:alpineBoth containers read and write the same volume. nginx can serve files that app wrote.
Backing Up and Restoring Volumes
Docker volumes live on the host. Back them up with a temporary container:
# backup: tar the volume contents to the host
docker run --rm \
-v pgdata:/data \
-v $(pwd):/backup \
alpine tar czf /backup/pgdata-backup.tar.gz -C /data .
# restore: extract into a new volume
docker volume create pgdata-restored
docker run --rm \
-v pgdata-restored:/data \
-v $(pwd):/backup \
alpine tar xzf /backup/pgdata-backup.tar.gz -C /dataSummary
Named volume: docker run -v myvolume:/data/path ← production
Bind mount: docker run -v $(pwd):/app ← local dev
tmpfs: docker run --tmpfs /tmp ← sensitive/ephemeral
Volumes survive: container stop, restart, rm
Volumes die on: docker volume rm, docker volume prune
Next: Docker Compose — replace all these docker run commands with a single YAML file.