👋 Hey there, I’m Dheeraj Choudhary an AI/ML educator, cloud enthusiast, and content creator on a mission to simplify tech for the world.
After years of building on YouTube and LinkedIn, I’ve finally launched TechInsight Neuron a no-fluff, insight-packed newsletter where I break down the latest in AI, Machine Learning, DevOps, and Cloud.
What to expect: actionable tutorials, tool breakdowns, industry trends, and career insights all crafted for engineers, builders, and the curious.
If you're someone who learns by doing and wants to stay ahead in the tech game you're in the right place.
Introduction
Containers are ephemeral by design. When you remove a container, every file it ever wrote, every database record it ever stored, every log it ever produced disappears with it. That's intentional and useful for stateless applications, but it's a problem for anything that needs to remember state: databases, uploaded files, application configuration, and logs.
Docker volumes solve this by decoupling data from the container lifecycle. The data lives outside the container, in storage that Docker manages independently. The container can be stopped, removed, upgraded, or replaced entirely, and the data sits untouched, ready for the next container to pick up exactly where the last one left off.
This guide covers the three Docker storage mechanisms, explains where volumes actually live on your host, walks through every volume management command, and explains when to use each storage type with real-world examples.
Why Containers Lose Data
Every Docker container has a writable layer on top of its read-only image layers. When a container writes a file, creates a database record, or logs output, that data goes into the writable layer. The image layers underneath never change.
When you delete a container with docker rm, that writable layer is destroyed. The image remains intact, but everything the container wrote is gone.
docker run -d --name db mysql:8
# MySQL creates databases, tables, and records
# All stored in the container's writable layer at /var/lib/mysql
docker rm -f db
# Container gone. Writable layer gone. All MySQL data: gone.This isn't a bug. Ephemerality is what makes containers replaceable, scalable, and consistent. But real applications need at least some data to persist. The solution is to move that data out of the writable layer entirely, into storage that exists independently of any container.

The Three Storage Options
Docker provides three mechanisms for persisting or temporarily storing data outside a container's writable layer:
Volumes are managed entirely by Docker. Docker creates and maintains them in its own storage area on the host. They're independent of any container and any specific host path. Volumes are the default recommended approach for production data.
Bind mounts map a specific directory or file on your host machine directly into the container. The container sees it as part of its filesystem. Changes from either side are immediately visible on the other. Best suited for development workflows.
tmpfs mounts (Linux only) store data in the host machine's RAM rather than on disk. Data never touches the filesystem and disappears the moment the container stops. Used for sensitive temporary data like tokens or session state that must not persist.
Volumes | Bind Mounts | tmpfs Mounts | |
|---|---|---|---|
Managed by | Docker | Host OS | Host OS (memory) |
Data survives container removal | Yes | Yes | No |
Works on Linux + Windows | Yes | Yes | Linux only |
Can share between containers | Yes | Yes | No |
Host can access directly | No (indirectly) | Yes | No |
Best for | Production data | Dev source code | Sensitive temp data |
Docker Volumes: The Recommended Approach
Volumes are the preferred mechanism for persisting data in Docker for several reasons, straight from the official documentation:
Volumes are often a better choice than writing data directly to a container, because a volume doesn't increase the size of the containers using it. Using a volume is also faster; writing into a container's writable layer requires a storage driver to manage the filesystem, which provides a union filesystem using the Linux kernel. This extra abstraction reduces performance as compared to using volumes, which write directly to the host filesystem.
Beyond performance, volumes have other advantages over bind mounts: they work consistently across Linux and Windows containers, they can be managed with Docker CLI commands, they can be safely shared among multiple containers simultaneously, and they support volume drivers for storing data on remote hosts or cloud storage providers.
Where Volumes Actually Live
When Docker creates a volume, it stores it in a dedicated directory on the host:
/var/lib/docker/volumes/<volume-name>/_dataFor example, a volume named mysql-data lives at:
/var/lib/docker/volumes/mysql-data/_dataYou can verify this with docker volume inspect:
docker volume inspect mysql-dataOutput:
[
{
"CreatedAt": "2024-01-15T10:23:45Z",
"Driver": "local",
"Labels": {},
"Mountpoint": "/var/lib/docker/volumes/mysql-data/_data",
"Name": "mysql-data",
"Options": {},
"Scope": "local"
}
]The Mountpoint field shows exactly where the data lives on the host. The key point: this path is managed by Docker, not by you. You shouldn't be writing to it directly. That's intentional. Volumes being Docker-managed is what makes them portable and safe to share between containers.
Creating and Using Volumes
There are two ways to create a volume: explicitly beforehand, or implicitly when you start a container.
Explicit creation
docker volume create my-dataThis creates the volume in Docker's storage area. No container is involved yet.
Implicit creation
If you reference a volume name that doesn't exist when starting a container, Docker creates it automatically:
docker run -d \
--name db \
-v my-data:/var/lib/mysql \
mysql:8The -v flag syntax is volume-name:container-path. Here my-data is the volume name and /var/lib/mysql is where MySQL expects to store its data inside the container. Docker mounts the volume at that path, so everything MySQL writes to /var/lib/mysql actually goes into the volume on the host.
The container can be deleted and recreated, and the data remains:
# Remove the container
docker rm -f db
# Start a fresh container using the same volume
docker run -d \
--name db-v2 \
-v my-data:/var/lib/mysql \
mysql:8
# All previous data is still there
Named vs Anonymous Volumes
The -v flag supports two forms that behave very differently.
Named volumes have an explicit name you choose:
docker run -v mysql-data:/var/lib/mysql mysql:8
# Volume name: "mysql-data"
# Easy to reference, manage, back up, and shareAnonymous volumes have no name. Docker generates a random identifier:
docker run -v /var/lib/mysql mysql:8
# Docker creates a volume with a name like: "a1b2c3d4e5f6..."
# Difficult to identify, difficult to manageThe behavior difference matters when you clean up containers. When you run
docker rm, anonymous volumes created by that container are removed by default (unless you use-vto explicitly remove named volumes too). Named volumes are never automatically removed.Always use named volumes for any data you care about. Anonymous volumes are acceptable only for truly throwaway scratch space. The inability to easily identify and reference them makes them risky for anything important.
The --mount Flag: The Modern Syntax
Docker has two syntaxes for attaching volumes and mounts: the older -v flag and the newer --mount flag. The official Docker documentation recommends --mount because it is explicit, verbose, and easier to read and reason about.
# -v syntax (concise but implicit)
docker run -v mysql-data:/var/lib/mysql mysql:8
# --mount syntax (verbose but explicit)
docker run --mount type=volume,source=mysql-data,target=/var/lib/mysql mysql:8The --mount flag takes comma-separated key-value pairs:
typeisvolume,bind, ortmpfssource(orsrc) is the volume name or host pathtarget(ordst) is the container pathreadonlymakes the mount read-only
# Read-only volume mount
docker run --mount type=volume,source=config-data,target=/etc/app,readonly nginx
# Bind mount using --mount
docker run --mount type=bind,source=/home/user/myapp,target=/app node:18
# Volume with --mount, explicit about everything
docker run -d \
--name mysql-db \
--mount type=volume,source=mysql-data,target=/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=secret \
mysql:8One important behavioral difference: if you use --mount with type=bind and the source path doesn't exist on the host, Docker returns an error. With -v, Docker automatically creates the directory. The --mount behavior is stricter and safer in production.
Managing Volumes
# List all volumes
docker volume ls
# Inspect a specific volume (shows mountpoint, driver, labels)
docker volume inspect my-data
# Remove a specific volume
docker volume rm my-data
# Only works if no container (running or stopped) is using the volume
# Remove all volumes not currently used by any container
docker volume prune
# Remove with confirmation bypass
docker volume prune -fdocker volume prune is powerful and destructive. It removes every volume not attached to at least one container, whether that container is running or stopped. If you have a stopped database container still attached to a volume, the volume is safe. If you removed the container and forgot about the volume, docker volume prune will delete it.
Before running docker volume prune, always check what volumes exist:
docker volume lsAnd check which containers are stopped but still reference volumes:
docker ps -aFor a full cleanup including stopped containers, dangling images, unused networks, and unused volumes together:
docker system prune --volumesThe --volumes flag is required to include volumes in docker system prune. Without it, volumes are left untouched.
Bind Mounts: Direct Host Filesystem Access
Bind mounts map a specific path on your host machine directly into the container. Unlike volumes, you control exactly where the data lives, because you're specifying the host path yourself.
# Using -v syntax
docker run -d \
--name webapp \
-v /home/user/myapp:/app \
-p 3000:3000 \
node:18
# Using --mount syntax (preferred)
docker run -d \
--name webapp \
--mount type=bind,source=/home/user/myapp,target=/app \
-p 3000:3000 \
node:18The host directory /home/user/myapp is mounted at /app inside the container. Every file change on the host immediately appears inside the container, and vice versa. This is what makes bind mounts the right tool for local development: you edit code in your editor on the host, and the running container sees the changes instantly without a rebuild.
Bind mounts are appropriate for sharing source code or build artifacts between a development environment on the Docker host and a container, when you want to create or generate files in a container and persist the files onto the host's filesystem, and for sharing configuration files from the host machine to containers.
The downside is portability. A bind mount hardcodes a host path into your docker run command. That path might not exist on another developer's machine or on a production server. This is why bind mounts are a development tool and volumes are the production choice.
A security note: never bind mount /, /etc, /var, or other sensitive host directories into untrusted containers. Bind mounts give the container direct access to that path with whatever permissions are configured. Mounting sensitive system directories into a container is a significant security risk.

tmpfs Mounts: In-Memory Temporary Storage
tmpfs mounts store data in the host machine's RAM. Nothing is written to disk. When the container stops, the data is gone with no trace, no cleanup required.
# Using --tmpfs flag (simplest)
docker run -d \
--name temp-app \
--tmpfs /tmp \
myapp
# Using --mount flag (more control)
docker run -d \
--name temp-app \
--mount type=tmpfs,destination=/tmp,tmpfs-size=100m \
myappAs opposed to volumes and bind mounts, a tmpfs mount is temporary, and only persisted in the host memory. When the container stops, the tmpfs mount is removed, and files written there won't be persisted. Unlike volumes and bind mounts, you can't share tmpfs mounts between containers. This functionality is only available if you're running Docker on Linux.
The practical use cases for tmpfs mounts are narrow but important. tmpfs mounts are useful for temporary data you don't want to persist, such as authentication tokens, session data, or cache files you'll rebuild anyway. When a container needs an API key or a password during runtime, storing it in a tmpfs mount means it never touches disk and leaves no trace after the container stops.
Tmpfs mounts are also useful for performance-sensitive scratch space. RAM access is orders of magnitude faster than disk access. If your application processes large amounts of temporary data, storing it in a tmpfs mount during processing and discarding it afterward is both faster and cleaner than writing to disk.
Choosing the Right Storage Type
Three questions determine which storage type to use:
Does the data need to survive the container? If yes, use a volume or bind mount. If no, the writable layer or a tmpfs mount is fine.
Does it need to be accessed from the host machine? If you need to edit it from your laptop or share it with non-Docker processes, use a bind mount. If only containers need it, use a volume.
Is it sensitive data that must never touch disk? Use a tmpfs mount.
In practice, the decision matrix simplifies to:
Database data, uploaded files, application state → Named volume
Source code during local development → Bind mount
Secrets, tokens, session data, temporary scratch space → tmpfs mount
Config files shared from host to container → Bind mount (read-only)
Logs you want to access from the host → Bind mount
Practical Example: MySQL with Persistent Data
This example demonstrates the core value of volumes: data survives container replacement.
# Step 1: Create a named volume
docker volume create mysql-data
# Step 2: Run MySQL with the volume mounted
docker run -d \
--name mysql-db \
-e MYSQL_ROOT_PASSWORD=secret \
-e MYSQL_DATABASE=myapp \
--mount type=volume,source=mysql-data,target=/var/lib/mysql \
mysql:8
# Step 3: Connect and create some data
docker exec -it mysql-db mysql -u root -psecret
# Inside MySQL:
# USE myapp;
# CREATE TABLE users (id INT, name VARCHAR(50));
# INSERT INTO users VALUES (1, 'Alice');
# EXIT;
# Step 4: Stop and remove the container entirely
docker stop mysql-db
docker rm mysql-db
# Step 5: Start a brand new container with the same volume
docker run -d \
--name mysql-db-v2 \
-e MYSQL_ROOT_PASSWORD=secret \
--mount type=volume,source=mysql-data,target=/var/lib/mysql \
mysql:8
# Step 6: Verify the data is still there
docker exec -it mysql-db-v2 mysql -u root -psecret
# SELECT * FROM myapp.users;
# Returns: 1 | Alice
# Data survived. Volume preserved everything.
Sharing a Volume Between Containers
A single named volume can be mounted into multiple containers simultaneously. This is how containers share data without going through a network connection.
# Container 1: Writer (application that generates reports)
docker run -d \
--name report-generator \
--mount type=volume,source=shared-reports,target=/output \
myapp
# Container 2: Reader (nginx serving those report files)
docker run -d \
--name report-server \
--mount type=volume,source=shared-reports,target=/usr/share/nginx/html,readonly \
-p 8080:80 \
nginxThe readonly option on the nginx mount ensures it can read files from the volume but cannot write to them. This is a good security practice whenever a container only needs read access.
Be mindful of write conflicts. If two containers write to the same files simultaneously, you can get data corruption. For most use cases, one container writes and others read. For cases where multiple containers need to write, you need application-level coordination or a database rather than a raw shared volume.
Backing Up and Restoring Volume Data
Volumes live in Docker's storage area, so you can't just copy them like regular directories (well, you can, but it requires finding the mountpoint). The proper way to back up a volume is to spin up a temporary container that mounts the volume and tars its contents to a file on your host.
Backup
# Create a compressed backup of the mysql-data volume to the current directory
docker run --rm \
--mount type=volume,source=mysql-data,target=/data \
--mount type=bind,source=$(pwd),target=/backup \
alpine \
tar czf /backup/mysql-data-backup.tar.gz -C /data .This runs a temporary Alpine container that mounts the mysql-data volume at /data and the current host directory at /backup, then creates a compressed tarball of the volume contents.
Restore
# Restore from backup into a volume (creates volume if it doesn't exist)
docker run --rm \
--mount type=volume,source=mysql-data-restored,target=/data \
--mount type=bind,source=$(pwd),target=/backup \
alpine \
tar xzf /backup/mysql-data-backup.tar.gz -C /dataThis pattern works for any volume and any data. The temporary container is removed after it finishes (--rm), leaving only the backup file and the restored volume behind.
Key Takeaways
Containers are ephemeral: when a container is removed, its writable layer and all data stored in it are destroyed. Volumes move data outside the container so it survives deletion
Docker has three storage mechanisms: volumes (Docker-managed, recommended for production), bind mounts (host-path-based, for development), and tmpfs mounts (RAM-based, Linux-only, for sensitive temporary data)
Volumes store data at
/var/lib/docker/volumes/<name>/_dataon the host. Docker manages this path; don't write to it directlyThe
-v name:/container/pathsyntax creates and mounts a named volume. The--mount type=volume,source=name,target=/pathsyntax is more explicit and is preferred in productionAlways use named volumes for important data. Anonymous volumes (created by specifying only a container path with no volume name) get random IDs and are difficult to manage, identify, or back up
docker volume pruneremoves all volumes not attached to any container, running or stopped. It's destructive. Always checkdocker volume lsbefore running itdocker system prune --volumesis required to include volumes in a full system cleanup. Without the--volumesflag, volumes are left untouched bydocker system pruneBind mounts are the right tool for local development (edit code on host, changes appear instantly in container), but depend on specific host paths and are not portable to other machines or production
Use the
readonlyoption when mounting a volume into a container that only needs to read the data:--mount type=volume,source=mydata,target=/data,readonlyBack up volumes using a temporary Alpine container that tars the volume contents to a bind-mounted host directory
Conclusion
Data persistence is what separates a toy Docker setup from a production-ready one. The distinction between what should live in a volume, what should use a bind mount, and what belongs in tmpfs directly shapes how reliable and maintainable your containers are in the long run.
The default answer for production data is always a named volume. Bind mounts are the right choice during development when you want code changes to reflect immediately without rebuilding images. tmpfs is a specialized tool for the narrow but important case of sensitive data that must never touch disk.
The natural progression from here is Docker Compose, which handles volume definitions declaratively alongside container definitions in a single file. Instead of wiring up volumes manually on the command line, Compose lets you define a postgres service with a postgres-data volume alongside your application service in a single docker-compose.yml, making multi-container setups with persistent data straightforward to share and reproduce.
🔗Let’s Stay Connected
📱 Join Our WhatsApp Community
Get early access to AI/ML, Cloud & Devops resources, behind-the-scenes updates, and connect with like-minded learners.
➡️ Join the WhatsApp Group
✅ Follow Me for Daily Tech Insights
➡️ LinkedIN
➡️ YouTube
➡️ X (Twitter)
➡️ Website

