Docker Storage and Volumes: The Real Deal on Data Persistence in Containers
Docker storage and volumes deep dive: understand container layers, bind mounts, tmpfs, and production patterns.
20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.
- ✓Basic Docker commands (docker run, docker build)
- ✓Understanding of container lifecycle
Use volumes for persistent data Docker manages, bind mounts for direct host access, and tmpfs for in-memory scratch. Volumes are the safest default for production — they're portable, backup-friendly, and don't depend on host filesystem structure.
Think of a container like a food truck. The truck itself (the container) has a built-in fridge (the writable layer) that's wiped clean when you park it. Volumes are like a separate cooler you can plug into any truck — your ingredients survive even if you swap trucks. Bind mounts are like handing someone your personal fridge key — convenient but risky. tmpfs is like a whiteboard that disappears when you close the truck door.
Here's a truth that'll save your weekend: containers are designed to be disposable. That's the whole point — you kill them, you replace them, you scale them. But data doesn't scale. And when your database container restarts and all your customer orders vanish, you'll learn the hard way that Docker's default storage is a lie.
The problem is simple: every container starts with a fresh filesystem. Write to it, and that data lives only as long as the container. The moment it dies — crash, update, reschedule — your data dies too. That's fine for stateless apps. For anything else, you need storage that outlives the container.
By the end of this, you'll know exactly when to use volumes, bind mounts, and tmpfs. You'll understand the performance trade-offs, the backup strategies, and the gotchas that have burned teams in production. No fluff. Just the stuff that matters when your container goes down at 3 AM.
The Three Storage Options: When Each One Saves Your Bacon
Docker gives you three ways to persist data: volumes, bind mounts, and tmpfs. Each solves a different problem. Volumes are Docker-managed — they live in /var/lib/docker/volumes/ and are the safest default. Bind mounts map any host directory into the container — great for development, dangerous in production because they depend on host filesystem structure. tmpfs stores data in memory — fast but volatile, perfect for secrets or cache that must never hit disk.
Here's the rule: if you're storing data that must survive container restarts, use a volume. If you need to share config files from the host, use a bind mount. If you need a scratch space that's fast and disposable, use tmpfs. Anything else is asking for trouble.
How Docker's Layered Filesystem Actually Works (And Why It Lies to You)
Every Docker image is a stack of read-only layers. When you run a container, Docker adds a thin writable layer on top. This is the container's writable layer — it's where all your runtime changes go. But here's the catch: that layer is ephemeral. Delete the container, and the layer is gone. Worse, it's slow for write-heavy workloads because of copy-on-write semantics.
When you write to a file in the container, Docker copies it up from the image layer into the writable layer first. That's the copy-on-write penalty. For databases or any write-intensive app, this kills performance. That's why you mount a volume — it bypasses the layered filesystem entirely. The volume is a direct mount, no COW overhead.
This is also why docker commit is a terrible idea for backups. It captures the writable layer, but it's a snapshot, not a backup strategy. Use volumes and backup those instead.
docker history image:tag to see layer sizes. If you see a 500MB layer from a RUN apt-get install followed by a RUN rm -rf /var/lib/apt/lists/*, that 500MB is still in the layer. Combine commands in a single RUN to avoid wasted space.Volumes: The Production Workhorse — How to Create, Manage, and Backup
Volumes are Docker's first-class citizen for data persistence. They're managed by Docker, stored in /var/lib/docker/volumes/, and can be shared across containers. They work on any host, regardless of filesystem structure. They support volume drivers for NFS, cloud storage, and encryption.
Creating a volume is simple: docker volume create myvolume. But in production, you'll want to use the --mount syntax for clarity: docker run --mount type=volume,source=myvolume,target=/data. This makes it explicit that you're using a volume, not a bind mount.
Backup strategy: volumes are just directories on the host. You can tar them up, rsync them, or use a backup tool. For databases, always use the database's native backup tool (pg_dump, mysqldump) rather than copying volume files — you'll get consistent backups without locking issues.
Bind Mounts: The Double-Edged Sword for Development
Bind mounts map a host directory directly into the container. They're perfect for development — you can edit code on your host and see changes instantly in the container. But they're a security risk in production: the container can modify any file on the host, and the host filesystem structure becomes a dependency.
The classic rookie mistake: using a bind mount for a database data directory in production. When the host path changes or the directory is accidentally deleted, your database is gone. Volumes don't have this problem — they're managed by Docker and survive host directory changes.
For development, bind mounts are fine. Just use the :ro flag to mount read-only if you don't need write access: -v /host/path:/container/path:ro. This prevents accidental modifications from the container.
tmpfs: The In-Memory Scratch Space You're Not Using Enough
tmpfs mounts store data in the container's memory. They're fast — RAM speed — and they disappear when the container stops. Perfect for secrets, temporary files, or cache that must never persist to disk. But they count against the container's memory limit, so don't use them for large datasets.
Use tmpfs when you need a scratch directory that's automatically cleaned up. For example, a CI build container that downloads dependencies, compiles, and then exits — the tmpfs ensures no leftover files. Or a web server that writes session files — tmpfs keeps them fast and ephemeral.
One gotcha: tmpfs is not shared between containers. Each container gets its own tmpfs mount. If you need shared memory, use a volume or bind mount.
Volume Drivers: When Local Storage Isn't Enough
Docker's volume plugin architecture lets you use remote storage backends: NFS, cloud block storage (EBS, Azure Disk), or distributed filesystems (GlusterFS, Ceph). This is essential for multi-host setups like Docker Swarm or Kubernetes (though K8s has its own volume system).
To use an NFS volume, install the docker-volume-netshare plugin or use the built-in local driver with NFS options. For cloud volumes, use the respective plugin (e.g., rexray for EBS).
The trade-off: network storage adds latency. For databases, local volumes with replication are usually better than network volumes. But for shared config or logs, network volumes simplify management.
Storage Drivers: Overlay2, Devicemapper, and When to Care
Docker uses a storage driver to manage the layered filesystem. The default on modern Linux is overlay2 — it's fast, stable, and uses copy-on-write efficiently. On older systems, you might see devicemapper or aufs. If you're still on devicemapper, upgrade — it's slow and has known issues with space reclamation.
You can check your storage driver with docker info | grep 'Storage Driver'. If it's not overlay2, consider migrating. Overlay2 supports page cache sharing between containers — if two containers use the same base image, they share the same cached pages, saving memory.
One gotcha: overlay2 has a limit on the number of lower layers (usually 128). If you have a deep image history, you might hit this limit. Keep your Dockerfiles lean — combine RUN commands and use multi-stage builds.
Backup and Restore Strategies That Won't Fail You at 3 AM
Backing up Docker volumes is straightforward, but there are traps. Never backup a volume while the container is writing to it — you'll get inconsistent data. For databases, use the database's native backup tool. For application data, stop the container or use a filesystem snapshot.
The simplest backup: docker run --rm -v volume_name:/data -v $(pwd):/backup alpine tar czf /backup/backup.tar.gz -C /data .. This spins up a temporary container that mounts the volume and creates a tar. For restore, reverse the process.
For automated backups, use a cron job or a backup tool like Duplicati or restic. Restic supports Docker volumes directly with restic backup /var/lib/docker/volumes/volume_name/_data. Just make sure to stop the container first or use a snapshot.
volumes_from or mount the same volume. Example: a container with cron and restic that backs up volumes nightly.Performance Benchmarks: Volumes vs Bind Mounts vs tmpfs
Performance varies significantly between storage options. tmpfs is fastest (RAM speed), followed by volumes (direct mount, no COW), then bind mounts (similar to volumes but with host filesystem overhead), and finally the container's writable layer (slowest due to COW).
For write-heavy workloads like databases, always use a volume. The writable layer's COW penalty can be 10-100x slower for random writes. Bind mounts are comparable to volumes for reads, but writes go through the host's VFS layer, which can add latency on some filesystems.
tmpfs is ideal for ephemeral high-speed I/O, but it consumes RAM. If your container has a 512MB memory limit, a 256MB tmpfs leaves only 256MB for the application. Plan accordingly.
When Not to Use Docker Volumes: The Overkill Scenarios
Volumes are great, but they're not always the answer. If you're running a stateless app that doesn't persist data, don't add a volume — you're adding complexity for no benefit. If you need to share data between containers on the same host, a volume works, but consider if a shared tmpfs or a simple bind mount is simpler.
For one-off scripts or CI jobs that process data and exit, tmpfs is often better — no cleanup needed. For development, bind mounts are more convenient than volumes because you can edit files on the host.
And if you're using Kubernetes, don't use Docker volumes directly — use PersistentVolumeClaims. Docker volumes are a Swarm-era concept; K8s has its own storage abstraction.
volumes: db_data: and services: db: volumes: - db_data:/var/lib/mysql.The 4GB Container That Kept Dying
docker run -v pgdata:/var/lib/postgresql/data postgres. Also set --storage-opt size=50G on the container if using devicemapper.- Always mount a volume for database data directories.
- The container's writable layer is tiny and ephemeral — treat it like a scratch pad, not a hard drive.
docker system df. 2. Check if volume is mounted: docker inspect container | grep Mounts. 3. If no volume, add one and restart. 4. If volume is full, prune unused data: docker system prune.docker exec container id. 2. Check volume ownership: docker run --rm -v volume:/data alpine ls -la /data. 3. Fix with docker run --rm -v volume:/data alpine chown -R UID:GID /data.docker volume ls. 2. Check mount path: docker inspect container | jq '.[].Mounts'. 3. Ensure target path is not pre-populated by image (volumes override image contents). 4. If using bind mount, ensure host path exists and is absolute.docker inspect container_name --format '{{json .Mounts}}'docker volume ls-v volume_name:/data| File | Command / Code | Purpose |
|---|---|---|
| StorageOptionsDemo.devops | docker volume create myapp_data | The Three Storage Options |
| LayerInspection.devops | docker pull alpine:latest | How Docker's Layered Filesystem Actually Works (And Why It L |
| VolumeManagement.devops | docker volume create app_data | Volumes: The Production Workhorse |
| BindMountDev.devops | docker run -d -p 3000:3000 -v $(pwd)/src:/app/src:ro node:18 npm run dev | Bind Mounts |
| TmpfsUsage.devops | docker run --rm --tmpfs /scratch:size=100M alpine sh -c "dd if=/dev/zero of=/scr... | tmpfs |
| VolumeDriverNFS.devops | docker volume create --driver local --opt type=nfs --opt o=addr=192.168.1.100,rw... | Volume Drivers |
| StorageDriverCheck.devops | docker info | grep -i 'storage driver' | Storage Drivers |
| BackupRestore.devops | docker stop myapp | Backup and Restore Strategies That Won't Fail You at 3 AM |
| PerformanceTest.devops | docker run --rm alpine sh -c "dd if=/dev/zero of=/tmp/test bs=1M count=100" | Performance Benchmarks |
| WhenNotToUseVolumes.devops | docker run -d --name stateless nginx:alpine | When Not to Use Docker Volumes |
Key takeaways
Interview Questions on This Topic
How does Docker's copy-on-write affect write performance in the writable layer, and how do volumes mitigate this?
Frequently Asked Questions
20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.
That's Docker. Mark it forged?
5 min read · try the examples if you haven't