Persistent Data #
One of the most common misconceptions when starting with Docker is treating containers as a place to store data. By default, containers are ephemeral: when a container is deleted, all data inside its filesystem disappears without a trace. This isn’t a bug — it’s fundamental design.
But real applications need data that survives. Databases must store transactions, applications must store file uploads, and application logs must be accessible after a container restart. This is where the persistent data concept becomes crucial.
Persistent data is a mechanism for storing data outside the container lifecycle — somewhere that doesn’t disappear when a container dies, restarts, or is recreated. This article thoroughly covers Docker’s three official mechanisms: volumes, bind mounts, and tmpfs. Each has different characteristics, use cases, and trade-offs.
Choosing the right mechanism is an architectural decision that will determine your application’s future stability, portability, and security. Let’s start from the foundation.
Why Containers Can’t Store Data #
Before jumping into solutions, it’s important to understand why this problem exists. It’s not a Docker weakness — it’s the direct consequence of how containers work.
Containers Are Layers, Not Disks #
As discussed in the previous article, every container is a writable layer on top of read-only image layers. This writable layer:
- Lives on the host, but is managed by Docker.
- Is tied to the container — when the container is deleted, the layer disappears with it.
- Operates with copy-on-write — files are only copied into this layer when they change.
flowchart TB
subgraph IMG["Image Layers (read-only)"]
L1[Base]
L2[RUN]
L3[COPY]
end
subgraph CONT["Container Writable Layer"]
W[Runtime changes]
end
IMG --> CONT
RM[docker rm container] -.->|deletes| CONT
style CONT fill:#ffd8a8
style RM fill:#ff6b6b,color:#fffWhen you run docker rm container, Docker deletes this writable layer. All the data the application wrote to the filesystem during the container’s life — database files, logs, uploads — vanishes with it. There’s no way to recover it.
The Impact on Real Applications #
Imagine running MySQL in a container without any persistence mechanism:
docker run -d --name mysql -e MYSQL_ROOT_PASSWORD=secret mysql:8
The application runs. You create a database, fill tables, store data. Everything works as usual. Then one day, you restart the container (say, for an image update):
docker rm -f mysql
docker run -d --name mysql -e MYSQL_ROOT_PASSWORD=secret mysql:8
A new database, empty. All the data you stored is gone. No error, no warning — the container works perfectly, but the data is destroyed.
This isn’t a hypothetical scenario. It’s the most common source of data loss in Docker, and the cause is always the same: data stored in the writable layer instead of somewhere persistent.
Docker’s golden rule: Containers = stateless, data = stateful outside the container. Always. Without exception. This isn’t a negotiable best practice — it’s how Docker works.
The Three Persistent Data Mechanisms in Docker #
Docker provides three official mechanisms for storing data outside the container lifecycle. Each has different characteristics, advantages, and trade-offs.
flowchart TB
P[Persistent Data in Docker] --> V[Volume<br/>managed by Docker]
P --> B[Bind Mount<br/>manual host path]
P --> T[tmpfs<br/>in RAM]
V --> V1[Production]
V --> V2[Databases]
V --> V3[Multi-container sharing]
B --> B1[Development]
B --> B2[Live reload]
B --> B3[Config files]
T --> T1[Sensitive caches]
T --> T2[Sessions]
T --> T3[Ephemeral secrets]| Mechanism | Physical Location | Persistent? | Managed By | Use Case |
|---|---|---|---|---|
| Volume | /var/lib/docker/volumes/ | Yes | Docker | Production, databases, multi-container |
| Bind Mount | Host path per arguments | Yes (depends on host) | User / OS | Development, live reload, config |
| tmpfs | RAM | No | OS | Sensitive caches, ephemeral secrets |
All three are attached to containers with the -v or --mount flag on docker run, or in the volumes section of docker-compose.yml. Let’s discuss each one.
Volumes — the Official Mechanism for Production #
Volumes are the persistent data mechanism managed directly by the Docker Engine. They’re the recommended choice for almost every production scenario.
Volume Characteristics #
- Stored on the host in a Docker-managed directory (default
/var/lib/docker/volumes/). - Volume names can be user-chosen (named volumes) or random (anonymous volumes).
- Lifecycle independent of the container — volumes survive container deletion.
- Can be mounted into many containers at once (sharing).
- Support volume drivers for integration with external storage (NFS, EBS, cloud storage).
- Portable — can be backed up, restored, and migrated to another host.
How It Works #
flowchart LR
subgraph HOST["Host Machine"]
HV[/var/lib/docker/volumes/<br/>volume-name/_data/]
end
subgraph C1["Container A"]
CA[/data/]
end
subgraph C2["Container B"]
CB[/var/lib/mysql/]
end
HV --> CA
HV --> CBThe container never knows the volume’s physical location on the host. All it sees is the mount point in its internal filesystem. This decoupling is what makes volumes portable and safe.
Creating and Using Volumes #
The most explicit way:
# Create a volume
docker volume create mysql-data
# Run a container with the volume
docker run -d \
--name mysql \
-v mysql-data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=secret \
mysql:8
The implicit way (Docker creates it automatically):
# Docker automatically creates the "app-data" volume because it doesn't exist
docker run -d \
--name app \
-v app-data:/app/data \
my-app
Verifying volumes:
# List all volumes
docker volume ls
# Inspect volume details
docker volume inspect mysql-data
inspect output:
[
{
"Name": "mysql-data",
"Driver": "local",
"Mountpoint": "/var/lib/docker/volumes/mysql-data/_data",
"CreatedAt": "2026-06-06T12:00:00Z",
"Labels": {},
"Scope": "local"
}
]
Volumes in Docker Compose #
In Compose, volumes are declared declaratively:
version: "3.9"
services:
db:
image: mysql:8
environment:
MYSQL_ROOT_PASSWORD: secret
volumes:
- mysql-data:/var/lib/mysql
backup:
image: alpine
volumes:
- mysql-data:/source:ro
- ./backups:/backup
command: tar czf /backup/db.tar.gz -C /source .
volumes:
mysql-data:
The declarative advantages:
- Reproducible —
docker compose upalways creates volumes with the same configuration. - Self-documenting — read
docker-compose.ymland immediately know which volumes are used. - Multi-service sharing — volumes defined once, used by many services.
Best practice: Always use named volumes (mysql-data,app-uploads,cache-data) rather than anonymous volumes (random hashes). Named volumes are easier to identify, back up, and manage.
Bind Mounts — Host Paths into Containers #
Bind mounts connect specific files or directories on the host directly to paths in the container. There’s no abstraction — what’s at the host path is what’s at the container path.
Bind Mount Characteristics #
- The host path is set manually (an absolute path, or relative to
docker-compose.yml). - Not managed by Docker — host files can be accessed directly without Docker.
- Depends on the host’s filesystem structure — not portable.
- Host changes are immediately visible in the container (no copy).
- Great for development (live reload) and accessing configuration files.
How It Works #
flowchart LR
subgraph HOST["Host Machine"]
HP[/home/dev/project/src/]
HC["/etc/nginx/nginx.conf"]
end
subgraph CONT["Container"]
CP[/app/src/]
CC["/etc/nginx/nginx.conf"]
end
HP <--> CP
HC <--> CC
style HP fill:#a8d8a8
style CP fill:#ffd8a8The difference from volumes:
- Volume → Docker picks the path on the host; the container doesn’t know it.
- Bind mount → The user picks the path on the host; it must be consistent across environments.
How to Use #
CLI with the -v format:
docker run -d \
--name dev-app \
-v /home/dev/project/src:/app/src \
my-app:dev
CLI with the --mount format (more explicit):
docker run -d \
--name dev-app \
--mount type=bind,source=/home/dev/project/src,target=/app/src \
my-app:dev
The --mount format is more verbose but clearer — good for scripts and CI.
Docker Compose (paths relative to the compose file):
services:
app:
image: my-app:dev
volumes:
- ./src:/app/src # bind mount, relative path
- ./nginx.conf:/etc/nginx/nginx.conf:ro # read-only
Read-Only Bind Mounts #
For configuration files the container must not modify, add :ro:
docker run -d \
--name nginx \
-v ./nginx.conf:/etc/nginx/nginx.conf:ro \
nginx
The container can read but not write. This is a common pattern for configuration — the host is the source of truth, the container is only a consumer.
Technical detail: :ro on a bind mount is deep read-only — every file inside the path becomes read-only and can’t be written by the container. This differs from a regular volume, which is read-write.tmpfs — Storage in Memory #
tmpfs stores data directly in RAM (memory) rather than on disk. This gives the highest access speed, but data is lost when the container stops.
tmpfs Characteristics #
- Stored in host memory (RAM).
- Very fast — speeds close to native memory access.
- Not persistent — lost when the container stops.
- Never touches the host’s disk filesystem.
- Good for sensitive data (never written to disk) or temporary data.
Use Cases #
- Caches — data that may be lost, but must be accessed fast.
- Session storage — web sessions that don’t need to last long.
- Ephemeral secrets — tokens, API keys loaded at startup but not needing persistence.
- Sensitive data — information that must never leave memory (certain compliance requirements).
How to Use #
docker run -d \
--name app \
--tmpfs /app/cache:size=100m \
my-app
The --tmpfs flag accepts size (memory limit) and mode (permissions) options.
In Docker Compose:
services:
app:
image: my-app
tmpfs:
- /app/cache:size=100m
When NOT to Use tmpfs #
- Databases — data must persist, and memory can run out.
- User file uploads — must survive long-term.
- Production caches that are expensive to rebuild — if regeneration takes a long time, store it in a volume.
Full Comparison of the Three Mechanisms #
| Aspect | Volume | Bind Mount | tmpfs |
|---|---|---|---|
| Location | /var/lib/docker/volumes/ | Manual host path | RAM |
| Persistent | Yes | Yes (depends on host) | No |
| Managed by | Docker | User / OS | OS |
| Portability | High | Low | N/A |
| Performance | Fast (native FS) | Very fast (no abstraction) | Fastest (RAM) |
| Sharing between containers | Yes | Yes | Yes |
| Backup/migration | Easy | Must copy manually | Not needed |
| Good for production | Yes | Rarely | Cache only |
| Good for development | Yes | Yes | Sometimes |
| Host access | Must go through Docker | Yes, direct | No |
Decision Tree #
flowchart TD
A{Does the data need<br/>to survive<br/>container death?}
A -- No --> B[tmpfs]
A -- Yes --> C{Will the container<br/>be deployed<br/>on many hosts?}
C -- Yes --> D[Volume]
C -- No --> E{Using a specific<br/>host path for live<br/>reload or config?}
E -- Yes --> F[Bind Mount]
E -- No --> D[Volume]The Separation of Concerns Principle #
The core of all the mechanisms above is one architectural principle: separating application from data.
flowchart TB
subgraph APP["Application Layer (Stateless)"]
A1[Container A]
A2[Container B]
A3[Container C]
end
subgraph DATA["Data Layer (Stateful)"]
D1[Volume]
D2[Database Service]
D3[Object Storage]
end
APP -->|mount / access| DATA
style APP fill:#a8d8a8
style DATA fill:#ffd8a8- Application layer = containers, images, code. Stateless, replaceable at any time.
- Data layer = volumes, databases, object storage. Persistent, managed separately.
This principle isn’t just about Docker — it’s the foundation of cloud-native architecture in general. Microservices, twelve-factor apps, and cloud-native architectures all start from here.
Twelve-Factor App, Factor VI: “Execute the app as one or more stateless processes.” This means the application must not store state in its process. State must live in a backing service — which in the Docker context is a volume or an external database.
When Persistent Data Is NOT Needed #
Not every workload needs persistent data. There are situations where ephemeral is exactly what you want:
- CI/CD build jobs — build results are usually pushed to a registry, no need to persist in the container.
- Unit test containers — pass/fail output is enough on stdout, no data to store.
- One-off batch processing — output goes to stdout or the host via volumes, then the container is deleted.
- Stateless microservices — services that only process requests without storing state.
For workloads like these, no volume or bind mount is needed. Just let the container live briefly and die.
When Persistent Data Is Mandatory #
Some workloads can’t live without persistent data:
- Databases (MySQL, PostgreSQL, MongoDB) — data must survive.
- User file uploads — images, documents, attachments.
- Persistent message queues — Kafka, RabbitMQ with durable queues.
- Search indexes — Elasticsearch, OpenSearch.
- Expensive-to-rebuild caches — Redis with persistence (RDB/AOF), or computed caches.
- Application logs — especially when aggregated from many containers.
- ML model artifacts — trained models loaded at container start.
For all of these, without persistent data you’ll lose data every time the container restarts. That’s not an acceptable scenario in production.
Common Usage Patterns #
1. Database with a Volume #
services:
postgres:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
The most standard pattern. Postgres data is mounted to a volume; the container can be restarted without losing data.
2. Multi-Container with a Shared Volume #
services:
app:
image: my-app
volumes:
- uploads:/app/uploads
processor:
image: image-processor
volumes:
- uploads:/input
depends_on:
- app
volumes:
uploads:
app writes files to /app/uploads, processor reads from /input. Both mount the same volume.
3. Development with a Hybrid Bind Mount + Volume Setup #
services:
app:
build: .
volumes:
- ./src:/app/src # bind mount for live reload
- node_modules:/app/node_modules # volume for dependencies
- ./.env:/app/.env:ro # bind mount for config
ports:
- "3000:3000"
volumes:
node_modules:
Bind mount for source code (needs live reload), a volume for node_modules (so the host doesn’t overwrite it), and a read-only bind mount for .env (the container must not modify config).
4. tmpfs for Secrets #
services:
app:
image: my-app
environment:
- API_KEY_FILE=/run/secrets/api_key
secrets:
- api_key
tmpfs:
- /run/secrets:size=1m,mode=0700
secrets:
api_key:
file: ./secrets/api_key.txt
The secret is loaded into tmpfs at startup, stored in memory, never written to disk.
Persistent Data Best Practices #
1. Use Volumes for Production #
Volumes are the default choice for almost every production scenario. Managed by Docker, portable, safe.
2. Use Bind Mounts Only for Development #
Bind mounts fit source-code live reload and config access. For production data, use volumes.
3. Don’t Store Important Data in the Container #
This is already the golden rule, but it’s still often violated. Always ask: “if the container is deleted right now, what data would be lost?”
4. Separate Data, Config, and Logs #
volumes:
- app-data:/app/data # application data
- app-config:/app/config # configuration
- app-logs:/app/logs # logs (or use a logging driver)
Don’t lump everything into one volume — it becomes hard to back up and migrate.
5. Back Up Volumes Regularly #
A volume isn’t a backup. It only moves the data lifecycle to the host. For important data, you still need backups to another location (S3, NFS, tape).
6. Use tmpfs for Sensitive Data #
API keys, session tokens, and data that must never leave memory should live in tmpfs.
7. Monitor Disk Usage #
Volumes can bloat. Watch with docker system df and docker volume ls -f dangling=true.
8. Document Mount Points #
Every volume should have a clear name and documentation. New developers joining the team should immediately know which volume holds what.
Summary #
- Docker containers are ephemeral — all data in the writable layer is lost when the container is deleted. This isn’t a bug, it’s design. Persistent data is the mechanism for storing data outside the container lifecycle.
- Three official mechanisms: Volumes (managed by Docker, for production), Bind mounts (manual host paths, for development), tmpfs (in RAM, for sensitive caches). Each has different trade-offs.
- Volumes are the default production choice. Managed by Docker, portable, support volume drivers for external storage integration (NFS, EBS, cloud), and can be shared between containers.
- Bind mounts connect host paths directly to containers. Good for development (source-code live reload) and config files. Not portable because they depend on the host structure.
- tmpfs stores data in memory. Very fast but not persistent. Good for caches, sessions, and ephemeral secrets that must never be written to disk.
- The core principle: separation of concerns — the application layer is stateless, the data layer is stateful outside the container. This is the foundation of cloud-native architecture and twelve-factor apps.
- Persistent data is mandatory for: databases, file uploads, search indexes, durable message queues, application logs, and ML model artifacts. Not needed for: CI/CD builds, unit tests, one-off batch processing.
- Best practices: named volumes for production, bind mounts for development, tmpfs for sensitive data, separate data/config/logs, back up volumes regularly, and monitor disk usage.
- The golden rule: Containers = stateless, data = stateful outside the container. Always.