| |

Docker 26 🐳 Persistent Storage Overview: Bind Mounts vs Docker Volumes vs tmpfs

A container’s writable layer is ephemeral. When the container is removed, everything written inside it is gone. This is by design—it makes containers disposable and reproducible. But real applications need persistence: a database must survive a restart, a log file must be available after the container stops, a configuration file must be shared with the host. Docker provides three storage mechanisms for this: volumes, bind mounts, and tmpfs mounts. They look the same from inside the container—each appears as a directory or file—but they differ in where the data lives, who manages it, and whether it survives.

This chapter covers the three mount types in full. You will learn what each one is, how it is created, where the data is stored, and the situations each one addresses. You will also see the trade-offs between them and the decision framework that makes the choice straightforward.

Key point: Volumes are managed by Docker and stored under /var/lib/docker/volumes/. Bind mounts map a specific host path into the container and are managed by you. tmpfs mounts store data in the host’s memory and are never written to disk. Volumes are the recommended default for persistent data. Bind mounts are for host-authored content like configuration files and source code. tmpfs is for sensitive, temporary data that should never touch the disk.


Why three storage mechanisms exist

The persistence problem. A container’s filesystem is layered: the image layers are read-only, and a thin writable layer sits on top. Files written to the writable layer are lost when the container is removed. For data that must survive—database files, uploaded content, application logs—the writable layer is the wrong place. A mount point redirects writes to storage that exists outside the container’s lifecycle.

The host-access problem. Sometimes the container needs to read or write files that live on the host. A development container should see the source code as it is edited on the host. A monitoring container should read the host’s log files. A bind mount provides this access by mapping a host path directly into the container.

The security problem. Some data is sensitive: credentials, tokens, decrypted secrets. Writing it to disk—even to a volume—leaves it on the filesystem, where it can be read by anyone with access to the host or to a volume backup. A tmpfs mount keeps the data in memory. When the container stops, the memory is reclaimed, and nothing remains on disk.

The management problem. Docker-managed volumes are portable and easy to back up. They are referenced by name, not by a host path, so the container’s definition does not depend on the host’s directory structure. Bind mounts depend on the host’s filesystem layout, which makes them less portable and more fragile. The three mechanisms represent different trade-offs between convenience, control, and security.

The performance problem. Writes to a container’s writable layer go through the storage driver—OverlayFS on most Linux systems—which adds overhead. Volumes and bind mounts bypass the storage driver entirely. They are mounted directly into the container’s filesystem, which makes them faster for write-heavy workloads like databases .


a. Docker volumes: managed persistence

A volume is a storage location managed by Docker. It is created with docker volume create or automatically when a container is started with a volume mount. The data is stored under /var/lib/docker/volumes/ on the host, but Docker manages the directory and the volume’s lifecycle .

docker volume create mydata
docker run -d --name db -v mydata:/var/lib/postgresql/data postgres:18

The -v mydata:/var/lib/postgresql/data option mounts the volume at the specified path in the container. Files written to that path are stored in the volume. If the container is removed and a new container is started with the same volume, the files are still there .

A volume can be mounted into multiple containers simultaneously. This is useful for sharing data between containers—log aggregation, shared configuration, or a pipeline where one container writes and another reads . Docker does not remove a volume when the container using it is removed. The volume persists until it is explicitly deleted with docker volume rm .

Volumes support volume drivers, which allow the data to be stored on remote hosts or cloud storage instead of the local filesystem. The default driver is local, which stores the data on the Docker host . A volume driver plugin can back the volume with NFS, Amazon S3, or another storage system, abstracting the storage location from the container’s definition .

The --mount flag is the preferred syntax for volumes because it is more explicit. The older -v flag is shorter but less clear about the mount type .

docker run -d --mount source=mydata,target=/var/lib/postgresql/data postgres:18

b. Bind mounts: direct host access

A bind mount maps a specific path on the host machine into the container. The path is referenced by its full host location, and the file or directory does not need to exist beforehand—Docker creates it on demand if it is missing .

docker run -d --name dev -v /home/user/app:/app node:22

The host directory /home/user/app is mounted at /app in the container. Changes made in the container are visible on the host, and changes made on the host are visible in the container. This is the mechanism for sharing source code during development, where the container sees the editor’s changes immediately .

Bind mounts have limitations compared to volumes. They depend on the host’s directory structure, which makes them less portable. They are not managed by Docker—docker volume ls does not list them, and docker volume prune does not remove them. They can access sensitive host files, including system directories, which is a security consideration .

The :ro flag mounts the bind read-only, preventing the container from modifying the host files. This is the recommended pattern for configuration files and TLS certificates, where the container should only read the data .

docker run -d -v /host/config:/app/config:ro myapp

Bind mounts are the right choice when the data is authored on the host and consumed in the container. Source code in development, configuration files, TLS certificates, and host log files are the common cases. They are not the right choice for application-generated data that should be managed by Docker .


c. tmpfs mounts: memory-only storage

A tmpfs mount stores data in the host’s memory. Nothing is written to the host’s filesystem or to the container’s writable layer. When the container stops, the tmpfs mount is removed, and the data is lost .

docker run -d --tmpfs /run/secrets myapp

The --tmpfs flag is the short form. The --mount flag is the preferred form because it is more explicit and supports more options .

docker run -d --mount type=tmpfs,dst=/run/secrets,tmpfs-size=64m,tmpfs-mode=1770 myapp

tmpfs mounts are available only on Linux. They cannot be shared between containers. The data counts toward the container’s memory limit—a large tmpfs-size does not grant the container additional RAM outside its limit .

The primary use cases are sensitive data and high-churn temporary files. Credentials, session tokens, and decrypted secrets can be stored in a tmpfs mount so they are never written to disk. A tmpfs mount combined with a read-only container filesystem provides writable scratch space for applications that need temporary storage without persisting it .

A subtle point: Docker’s tmpfs maps directly to the Linux kernel’s tmpfs. If the host uses swap, the data may be written to the swap file, which is a disk-backed store. The data is not guaranteed to stay in RAM .


Complete Example Session

# ============================================
# PART 1: CREATE A VOLUME
# ============================================
docker volume create postgres_data
docker volume ls
# ============================================
# PART 2: RUN A CONTAINER WITH A VOLUME
# ============================================
docker run -d --name db \
  -e POSTGRES_PASSWORD=secret \
  -v postgres_data:/var/lib/postgresql/data \
  postgres:18
# ============================================
# PART 3: VERIFY DATA PERSISTS
# ============================================
docker exec -it db psql -U postgres -c "CREATE TABLE tasks (id SERIAL, description TEXT);"
docker exec -it db psql -U postgres -c "INSERT INTO tasks (description) VALUES ('test');"
docker stop db && docker rm db
docker run -d --name db2 \
  -v postgres_data:/var/lib/postgresql/data \
  postgres:18
docker exec -it db2 psql -U postgres -c "SELECT * FROM tasks;"
# The data is still there
# ============================================
# PART 4: BIND MOUNT FOR DEVELOPMENT
# ============================================
docker run -d --name dev \
  -v $(pwd):/app \
  -w /app \
  node:22 \
  npm run dev
# Changes to files on the host are visible in the container
# ============================================
# PART 5: BIND MOUNT READ-ONLY
# ============================================
docker run -d --name web \
  -v /host/nginx.conf:/etc/nginx/nginx.conf:ro \
  nginx:alpine
# The container cannot modify the host's nginx.conf
# ============================================
# PART 6: TMPFS MOUNT
# ============================================
docker run -d --name app \
  --mount type=tmpfs,dst=/run/secrets,tmpfs-size=64m,tmpfs-mode=1770 \
  myapp
# Data in /run/secrets is in memory only
# ============================================
# PART 7: TMPFS WITH READ-ONLY ROOT
# ============================================
docker run -d --name secure \
  --read-only \
  --tmpfs /tmp \
  --tmpfs /run \
  myapp
# The container's root filesystem is read-only
# /tmp and /run are writable in memory
# ============================================
# PART 8: LIST AND INSPECT VOLUMES
# ============================================
docker volume ls
docker volume inspect postgres_data
# ============================================
# PART 9: BACKUP A VOLUME
# ============================================
docker run --rm \
  -v postgres_data:/source:ro \
  -v $(pwd)/backup:/backup \
  alpine tar czf /backup/postgres_data.tar.gz -C /source .
# ============================================
# PART 10: CLEAN UP
# ============================================
docker stop db2 && docker rm db2
docker volume rm postgres_data
# Volume is removed only when explicitly deleted

The ten parts covered creating a volume, running a container with a volume, verifying persistence, bind mounts for development, read-only bind mounts, tmpfs mounts, tmpfs with read-only root, listing and inspecting volumes, backing up a volume, and cleanup.


Quick Reference

Mount Type Comparison

AspectVolumeBind Mounttmpfs
Managed byDockerYouKernel
Host location/var/lib/docker/volumes/Any host pathMemory
Survives container removalYesYesNo
Survives host rebootYesYesNo
Shareable between containersYesYesNo
Portable across hostsYesNoN/A
PerformanceFast (bypasses overlay)Fast (bypasses overlay)Fastest (RAM)
SecurityIsolatedCan access host filesIn-memory only
Backed up by DockerYesNoNo

Syntax

Mount Type-v Syntax--mount Syntax
Volume-v mydata:/path--mount source=mydata,target=/path
Bind-v /host:/path--mount type=bind,source=/host,target=/path
tmpfs--tmpfs /path--mount type=tmpfs,dst=/path
Read-only-v /host:/path:ro--mount ...,readonly

When to Use Each

Use CaseMount Type
Database filesVolume
Application stateVolume
Uploaded contentVolume
Logs (persistent)Volume
Source code (development)Bind mount
Configuration filesBind mount (:ro)
TLS certificatesBind mount (:ro)
Host log files (read)Bind mount (:ro)
Credentialstmpfs
Session tokenstmpfs
Scratch space (read-only root)tmpfs
Cachestmpfs

Volume Commands

CommandPurpose
docker volume create nameCreate a volume
docker volume lsList volumes
docker volume inspect nameShow volume details
docker volume rm nameRemove a volume
docker volume pruneRemove unused volumes

Best Practices

✅ Do This:

# Use named volumes for persistent data
docker run -v pgdata:/var/lib/postgresql/data postgres:18      # ✅

# Use bind mounts for development source code
docker run -v $(pwd):/app -w /app node:22 npm run dev           # ✅

# Use :ro for configuration files
docker run -v /host/config.yml:/app/config.yml:ro myapp         # ✅

# Use tmpfs for secrets
docker run --tmpfs /run/secrets myapp                          # ✅

# Use --mount for explicit syntax
docker run --mount source=mydata,target=/data myapp             # ✅

# Back up volumes with a temporary container
docker run --rm -v mydata:/source:ro -v $(pwd):/backup alpine tar czf /backup/data.tar.gz -C /source .  # ✅

# Combine tmpfs with read-only root
docker run --read-only --tmpfs /tmp myapp                       # ✅

❌ Don’t Do This:

# Don't store databases in the container writable layer
docker run postgres:18  # data is lost when container is removed  # ❌

# Don't use bind mounts for application-generated data
docker run -v /host/data:/var/lib/postgresql/data postgres       # ⚠️

# Don't forget :ro for config files the container should not modify
docker run -v /host/nginx.conf:/etc/nginx/nginx.conf nginx      # ⚠️

# Don't expect tmpfs data to survive container restart
docker run --tmpfs /data myapp  # data lost on stop              # ❌

# Don't share tmpfs between containers
# It is not possible                                                # ❌

# Don't mount the Docker socket into a container
docker run -v /var/run/docker.sock:/var/run/docker.sock myapp    # ❌ security risk

# Don't use docker volume prune without checking
docker volume prune  # may delete volumes still needed           # ⚠️

Common Pitfalls

PitfallWhy It HappensFix
Data lost after container removalStored in writable layerUse a volume or bind mount
Volume not removedDocker manages itdocker volume rm explicitly
Bind mount path wrongHost path does not existDocker creates it, but verify
tmpfs data goneContainer stoppedExpected behavior
Permission deniedHost file ownershipMatch UID/GID
Volume not sharedDocker requires same volume nameUse the same volume
Backup incompleteVolume still in useStop container first

Real-World Examples

1. Database Persistence

docker run -d --name db \
  -v postgres_data:/var/lib/postgresql/data \
  postgres:18

2. Development Source Code

docker run -d -v $(pwd):/app -w /app node:22 npm run dev

3. Read-Only Configuration

docker run -d -v /host/nginx.conf:/etc/nginx/nginx.conf:ro nginx

4. Secrets in Memory

docker run -d --tmpfs /run/secrets myapp

5. Shared Volume Between Containers

docker run -d --name writer -v shared_logs:/logs app1
docker run -d --name reader -v shared_logs:/logs app2

6. Backup a Volume

docker run --rm \
  -v postgres_data:/source:ro \
  -v $(pwd):/backup \
  alpine tar czf /backup/db.tar.gz -C /source .

7. Read-Only Root with tmpfs

docker run --read-only --tmpfs /tmp --tmpfs /run myapp

8. NFS Volume

docker volume create --driver local \
  --opt type=nfs \
  --opt o=addr=10.0.0.10,rw \
  --opt device=:/exports/data \
  nfs_data

9. Bind Mount for Logs

docker run -d -v /var/log/myapp:/logs:ro myapp

10. Inspect a Volume

docker volume inspect postgres_data

Visual

Where the Data Lives

┌─────────────────────────────────────────────────────────────┐
│  DOCKER HOST                                                │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  /var/lib/docker/volumes/                           │    │
│  │  └── postgres_data/                                 │    │
│  │      └── _data/                                     │    │
│  │          └── (database files)                       │    │
│  │                                                     │    │
│  │  VOLUME: managed by Docker                          │    │
│  └─────────────────────────────────────────────────────┘    │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  /home/user/app/                                    │    │
│  │  └── (source code)                                  │    │
│  │                                                     │    │
│  │  BIND MOUNT: managed by you                         │    │
│  └─────────────────────────────────────────────────────┘    │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  RAM (host memory)                                  │    │
│  │  └── (tmpfs data)                                   │    │
│  │                                                     │    │
│  │  TMPFS: never touches disk                          │    │
│  └─────────────────────────────────────────────────────┘    │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Mount Type Decision Tree

┌─────────────────────────────────────────────────────────────┐
│  Does the data need to persist?                             │
│    │                                                        │
│    ├── NO ──▶ tmpfs                                         │
│    │         (credentials, scratch, caches)                 │
│    │                                                        │
│    └── YES                                                  │
│          │                                                  │
│          ▼                                                  │
│  Is the data authored on the host?                          │
│    │                                                        │
│    ├── YES ──▶ Bind mount                                   │
│    │         (source code, config, certs)                   │
│    │                                                        │
│    └── NO ──▶ Volume                                        │
│               (database, uploads, app state)                │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Container Filesystem Layers

┌─────────────────────────────────────────────────────────────┐
│  CONTAINER FILESYSTEM                                       │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  Writable layer (ephemeral)                         │    │
│  │  Lost when container is removed                     │    │
│  └─────────────────────────────────────────────────────┘    │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  Image layers (read-only)                           │    │
│  │  From the image, never modified                     │    │
│  └─────────────────────────────────────────────────────┘    │
│                                                             │
│  MOUNTS (overlay on top):                                   │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  Volume  │  Bind mount  │  tmpfs                    │    │
│  │  /data   │  /app        │  /run/secrets             │    │
│  └─────────────────────────────────────────────────────┘    │
│                                                             │
│  Mounts bypass the writable layer.                          │
│  Data goes directly to the host or memory.                  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

tmpfs Memory Accounting

┌─────────────────────────────────────────────────────────────┐
│  tmpfs data counts toward the container's memory limit.     │
│                                                             │
│  Container memory limit: 512 MB                             │
│  tmpfs-size: 256 MB                                         │
│  Application memory: 200 MB                                 │
│                                                             │
│  Total: 200 + 256 = 456 MB                                  │
│  Remaining: 56 MB                                           │
│                                                             │
│  If tmpfs fills, the container can OOM.                     │
│  A large tmpfs-size does not add RAM outside the limit.     │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Summary

ItemValue
VolumeManaged by Docker, /var/lib/docker/volumes/
Bind mountHost path mapped into container
tmpfsMemory-only, never on disk
Volume persistenceSurvives container removal
Bind mount persistenceSurvives container removal
tmpfs persistenceLost when container stops
Volume sharingYes, between containers
Bind mount sharingYes, host path
tmpfs sharingNo
Volume driversRemote storage, cloud
Best for volumesDatabases, app state, uploads
Best for bind mountsSource code, config, certs
Best for tmpfsSecrets, scratch, caches

Key takeaways:

  • Volumes are the recommended default for persistent data. They are managed by Docker, stored under /var/lib/docker/volumes/, and survive container removal. They can be shared between containers and backed up easily.
  • Bind mounts map a specific host path into the container. They are useful for host-authored content: source code during development, configuration files, and TLS certificates. Use :ro for files the container should not modify.
  • tmpfs mounts store data in memory only. The data is lost when the container stops and never written to disk. Use them for credentials, session tokens, and other sensitive temporary data.
  • The choice depends on the data’s origin and persistence needs. Application-generated data that must persist goes in a volume. Host-authored content goes in a bind mount. Temporary sensitive data goes in tmpfs.
  • Volumes bypass the storage driver. Writes to a volume or bind mount are faster than writes to the container’s writable layer because they skip the OverlayFS overhead. This matters for databases and other write-heavy workloads.
  • --mount is preferred over -v for clarity. The --mount syntax is more explicit about the mount type and options. The -v syntax is shorter but less clear.
  • tmpfs data counts toward the container’s memory limit. A large tmpfs-size does not grant additional RAM outside the limit. Filling the mount can cause the container to be killed by the OOM killer.

Remember: The three mount types are not interchangeable. Each one answers a different question. A volume answers “where should Docker store this data?” A bind mount answers “how does the container see this host file?” A tmpfs mount answers “how do I keep this data in memory only?” The answers determine the choice. Use volumes for the data the application owns. Use bind mounts for the data the host owns. Use tmpfs for the data that should not survive the container. The container’s writable layer is for nothing that needs to persist. The mount is the boundary between the ephemeral container and the persistent world.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!