Skip to main content

Docker Volumes and Persistent Storage — Volumes, Bind Mounts, and tmpfs

Manage persistent data in Docker using named volumes, bind mounts, and tmpfs — and learn exactly which one to use for production vs development.

44 Terms

Overview and What You Will Learn

Containers are stateless by design. The moment you run docker rm on a container, every file written inside it is gone forever. For a stateless API server that is fine. For a PostgreSQL database holding Razorpay transaction records, that is a disaster waiting to happen.

Docker solves this with three storage mechanisms, and most engineers only learn the difference between them after losing data once in development. This lab makes sure that lesson does not happen to you in production.

By the end of this lab you will:

  • Understand exactly why containers lose data on removal and how each storage type avoids that
  • Create, inspect, back up, and restore named volumes
  • Use bind mounts correctly for local development workflows
  • Know when tmpfs is the right (and only) choice
  • Configure all three storage types inside docker-compose.yml
  • Avoid the most common volume permission and data-loss mistakes

Why This Matters in Production

At Zerodha, the trade ledger database runs inside a container, but the actual data lives on a named volume mounted at /var/lib/postgresql/data. When the container is upgraded to a new Postgres minor version, the container is destroyed and recreated — but the volume survives untouched. If that data had been written to the container's own writable layer instead of a volume, every deployment would wipe the ledger.

This is the single most important mental model in this lab: the container is disposable, the volume is not. Engineers who skip this lesson eventually run docker compose down thinking it only stops containers, not realizing docker compose down --volumes deletes data too. Understanding the three storage types prevents that 2 AM incident.

Core Principles

The three storage types compared:

Bash
+------------------------+ +------------------------+ +------------------------+
| Named Volume | | Bind Mount | | tmpfs |
| | | | | |
| Managed by Docker | | Managed by you (OS path) | | RAM only, never disk |
| /var/lib/docker/volumes| | Any host path you choose | | Mounted in memory |
| Survives container rm | | Survives container rm | | Lost on container stop |
| Best for: prod data | | Best for: dev hot reload | | Best for: secrets/cache|
+------------------------+ +------------------------+ +------------------------+

Why containers lose data without a volume:

Bash
+------------------------------------------+
| Container runs, writes file to /data | <- file lives in container's
+------------------------------------------+ writable layer only
|
v
+------------------------------------------+
| docker rm container_name runs |
+------------------------------------------+
|
v
+------------------------------------------+
| Writable layer destroyed, file gone | <- no volume = no recovery
+------------------------------------------+

With a volume attached, the data path changes:

Bash
+------------------------------------------+
| Container writes to /var/lib/postgresql |
| /data (mounted from named volume) |
+------------------------------------------+
|
v
+------------------------------------------+
| Write goes to host-managed volume | <- outside container's
| /var/lib/docker/volumes/pgdata/_data | writable layer
+------------------------------------------+
|
v
+------------------------------------------+
| docker rm container_name runs |
| Volume pgdata is untouched, data intact | <- survives removal
+------------------------------------------+

Detailed Step-by-Step Practical Lab

Milestone 1 — Create, inspect, and use a named volume

Bash
## Create a named volume explicitly (optional -- Docker can also auto-create)
docker volume create pgdata-mumbai-prod
## List all volumes on this host
docker volume ls
## Inspect a volume -- shows the real path on disk
docker volume inspect pgdata-mumbai-prod
Bash
[
{
"CreatedAt": "2026-06-10T09:12:00Z",
"Driver": "local",
"Mountpoint": "/var/lib/docker/volumes/pgdata-mumbai-prod/_data",
"Name": "pgdata-mumbai-prod",
"Scope": "local"
}
]
Bash
## Run Postgres with the named volume attached
docker run -d \
--name razorpay-ledger-db \
-e POSTGRES_PASSWORD=changeme_in_prod \
-v pgdata-mumbai-prod:/var/lib/postgresql/data \
postgres:16-alpine
## Confirm the volume is mounted correctly
docker inspect razorpay-ledger-db --format '{{ json .Mounts }}'
## Stop and remove ONLY the container -- volume survives
docker stop razorpay-ledger-db
docker rm razorpay-ledger-db
## Recreate the container pointing at the same volume -- data is back
docker run -d \
--name razorpay-ledger-db \
-e POSTGRES_PASSWORD=changeme_in_prod \
-v pgdata-mumbai-prod:/var/lib/postgresql/data \
postgres:16-alpine

Milestone 2 — Use a bind mount for local development hot reload

Bash
## Bind mount the current directory into the container
## Code changes on host are reflected instantly inside the container
docker run -d \
--name swiggy-api-dev \
-p 4000:4000 \
-v /home/rahul/projects/swiggy-api:/app \
-w /app \
node:20-alpine \
npm run dev
## Using the newer --mount syntax (more explicit, recommended for production scripts)
docker run -d \
--name swiggy-api-dev \
-p 4000:4000 \
--mount type=bind,source=/home/rahul/projects/swiggy-api,target=/app \
-w /app \
node:20-alpine \
npm run dev
## Verify the bind mount with docker inspect
docker inspect swiggy-api-dev --format '{{ json .Mounts }}'

Milestone 3 — Use tmpfs for in-memory, never-persisted data

Bash
## Mount tmpfs for a directory that should never touch disk
## Useful for session caches, temp upload buffers, secret material
docker run -d \
--name session-cache \
--tmpfs /app/cache:rw,size=128m \
phonepe/session-service:latest
## Confirm tmpfs is active inside the container
docker exec session-cache mount | grep tmpfs
## tmpfs on /app/cache type tmpfs (rw,size=131072k)
## Data written to /app/cache disappears the moment the container stops
docker stop session-cache
docker rm session-cache
## Cache content is gone -- this is expected and desired behaviour

Milestone 4 — Back up and restore a named volume

Bash
## Back up a volume's contents to a tar file on the host
## Uses a throwaway container that mounts both the volume and a host directory
docker run --rm \
-v pgdata-mumbai-prod:/source \
-v /home/rahul/backups:/backup \
alpine \
tar czf /backup/pgdata-backup-2026-06-18.tar.gz -C /source .
## Restore the backup into a fresh volume
docker volume create pgdata-mumbai-restored
docker run --rm \
-v pgdata-mumbai-restored:/target \
-v /home/rahul/backups:/backup \
alpine \
tar xzf /backup/pgdata-backup-2026-06-18.tar.gz -C /target
## Verify restored data by mounting it into a temporary inspection container
docker run --rm -v pgdata-mumbai-restored:/data alpine ls -la /data

Milestone 5 — Use volumes in docker-compose.yml

YAML
services:
ledger-db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: changeme_in_prod
volumes:
# Named volume -- managed by Docker, survives compose down
- pgdata:/var/lib/postgresql/data
api:
build: .
volumes:
# Bind mount -- source code for hot reload in development
- ./src:/app/src
# tmpfs -- in-memory only, never written to disk
tmpfs:
- /app/tmp
volumes:
# Declaring the named volume at the top level
pgdata:
driver: local
Bash
## Start the stack
docker compose up -d
## Stop containers but KEEP volumes (default, safe)
docker compose down
## Stop containers AND delete volumes (destructive -- use with caution)
docker compose down --volumes

Milestone 6 — Clean up unused volumes safely

Bash
## List volumes not currently used by any container
docker volume ls -f dangling=true
## Remove a specific unused volume
docker volume rm old-test-volume
## Remove ALL unused volumes -- review the dangling list first
docker volume prune
## Prune with a filter -- only volumes older than 72 hours
docker volume prune --filter "label!=keep" -a

Production Best Practices and Common Pitfalls

Scenario Wrong Correct
Database storage Write directly to container filesystem Mount a named volume at the data directory
Local dev code changes Rebuild image on every change Bind mount source directory for hot reload
Temporary secret material Write to a regular volume Use tmpfs so it never touches disk
Removing a stack docker compose down --volumes by habit docker compose down (keeps volumes by default)
Backing up data Hope the volume is fine Scheduled tar backup via throwaway container
Bind mount in production Bind mount application code into prod containers Bake code into the image, reserve bind mounts for dev only

Quick Reference and Troubleshooting Commands

Task Command
Create a named volume docker volume create name
List volumes docker volume ls
Inspect a volume docker volume inspect name
Remove a volume docker volume rm name
Remove unused volumes docker volume prune
Mount named volume docker run -v name:/path image
Mount bind path docker run -v /host/path:/path image
Mount tmpfs docker run --tmpfs /path:rw,size=128m image
Back up a volume docker run --rm -v name:/source -v $(pwd):/backup alpine tar czf /backup/out.tar.gz -C /source .
Tip

Use docker run --mount instead of -v when scripting backups or production deploys. The --mount syntax is more verbose but fails loudly on typos instead of silently creating an unexpected anonymous volume.

Remember

docker compose down does NOT delete named volumes by default. You must explicitly pass --volumes to remove them. This default exists specifically to protect production data.

Common Mistake

Forgetting the trailing slash difference between -v /data:/app/data and -v ./data:/app/data. The first is an absolute host path; the second is relative to the current directory. A typo here can silently create an anonymous volume instead of mounting the directory you intended.

Security

Never bind mount the Docker socket (-v /var/run/docker.sock:/var/run/docker.sock) into a container unless that container is a trusted, audited tool. Any process with access to the socket has root-equivalent control over the entire host.

Resources

Explore More in Docker Compose and Multi-Container Applications

All 6 Topics

Frequently Asked Questions

Is Docker Volumes and Persistent Storage — Volumes, Bind Mounts, and tmpfs free to learn on DevOps Network?

Yes - this topic, like everything on DevOps Network, is 100% free with no paywall or sign-up gate.

What does the Docker Volumes and Persistent Storage — Volumes, Bind Mounts, and tmpfs topic cover?

Manage persistent data in Docker using named volumes, bind mounts, and tmpfs — and learn exactly which one to use for production vs development.