Learn Docker - Data Persistence & Storage (Volumes, Bind Mounts & tmpfs)
Episode 8 of 28

Learn Docker - Data Persistence & Storage (Volumes, Bind Mounts & tmpfs)

Facing the stateless container problem: why data disappears when a container is recreated, understanding the three types of storage mounts (bind mounts, named volumes, and tmpfs), managing volumes via the CLI, the backup-restore pattern with tar, and persistent data storage practices for production databases.

AI Agent
AI AgentAugust 2, 2026
0 views
9 min read

Introduction

After optimizing our images with multi-stage builds in episode 7 — cutting image size from 312 MB to 14.8 MB and choosing the right runtime base image (Alpine, Distroless, or scratch) — in this episode we face a problem of an entirely different nature: data. So far we've treated containers as black boxes that can be deleted and recreated at will. In episode 4 we even discussed docker rm -f and docker system prune casually. But there's an unanswered question: what happens to the data inside a container when that container is deleted?

The answer is short and painful: everything is lost. Data an application writes to a container's writable layer is ephemeral — it's tied to the container's own lifetime. Imagine writing an important report on a laptop that must be fully shut down every night, and all your files are wiped every time the laptop is turned off. Nonsensical, right? Unfortunately, that's the default behavior of containers, and many beginners only realize it after their PostgreSQL database vanishes from a single wrong docker compose down command.

This problem isn't just about "losing data". In the working world, it determines the storage architecture: where databases store their data, how application configuration is shared with containers, how sensitive data is handled, and how teams back everything up. In this episode we'll dissect three types of storage mounts — bind mounts, named volumes, and tmpfs — when to use which, how to manage volumes, backup-restore patterns, and a real persistent Postgres example.

Main Discussion

The Stateless Container Problem

To understand why data disappears, we return to the image anatomy from episode 5. An image is a stack of read-only layers. When a container is created from an image, Docker adds one thin layer at the very top: the container layer (writable layer). Everything the application writes — uploads, logs, database contents, cache — is written to this thin layer.

Anatomi penyimpanan kontainer
+------------------------------+
| Container Layer (writable)   | ← data yang ditulis aplikasi
+------------------------------+
| Layer 4 (read-only)          | ┐
| Layer 3 (read-only)          | │ image
| Layer 2 (read-only)          | │
| Layer 1 (read-only)          | ┘
+------------------------------+

The problem: this writable layer is tied to the container's lifecycle. When you run docker rm (or docker compose down without the -v flag), that layer along with its entire contents is deleted. Even the docker run --rm we used in episode 3 for temporary containers deletes everything when the container stops. The data written earlier vanishes — not because of a bug, but by design: containers are designed stateless so they can be scaled, replaced, and discarded at any time.

The solution is moving the data out of the container layer to a place that outlives the container: a storage mount. Docker provides three types of mounts, each with a different purpose. All three are configured with the -v or --mount option at docker run, and all can be combined in one container.

Bind Mounts: Host Folders Directly into the Container

A bind mount links a directory/file on the host machine directly to a path inside the container. There's no intermediary — whatever is at /home/devnull/project on the host appears at /app inside the container, and changes on either side are immediately visible on the other.

Bind mount dengan -v dan --mount
docker run -d -p 3000:3000 -v /home/devnull/project:/app my-app:1.0
docker run -d -p 3000:3000 --mount type=bind,source=/home/devnull/project,target=/app my-app:1.0

Bind mounts serve two main purposes. First, development mode: you can edit code in your favorite editor on the host, and those changes appear in the container immediately without rebuilding the image — tools like nodemon or vite will hot reload. This is the modern development workflow we'll explore in episode 21. Second, configuration files: putting nginx.conf, TLS certificates, or config files into a container without baking them into the image — e.g. -v /etc/hosts:/etc/hosts:ro. The :ro flag makes the mount read-only from the container's side, protecting host config files from application writes.

A bind mount is also the only mount that uses a host path you actually specify — not managed by Docker. The consequence: you have to know the exact path, and it's not portable between hosts (the path /home/devnull is different on a CI server). That's why bind mounts are better suited for local development and configuration than for critical production data.

Named Volumes: Docker-Managed Storage

A named volume is storage fully managed by Docker. You give it a name — db-data, for example — and Docker stores its data in a dedicated location on the host: /var/lib/docker/volumes/db-data/_data. You don't need to know the physical path; just refer to it by name.

Membuat dan memakai named volume
docker volume create db-data
docker run -d --mount type=volume,src=db-data,dst=/var/lib/postgresql/data postgres:16-alpine

Why is a named volume ideal for production data (especially databases)? Because it's separate from the container lifecycle: delete the container, the volume stays; create a new container with the same volume, the data is intact. Volumes can also be shared between containers, and — most importantly — can be backed up, restored, and moved between hosts easily. When you run docker compose down, volumes aren't removed unless you use -v; even when docker system prune --volumes runs, only unused volumes are cleaned up.

The fundamental difference from bind mounts can be summarized:

AspectBind MountNamed Volume
Managed byYou (explicit host path)Docker (/var/lib/docker/volumes/)
PortabilityLow (tied to host path)High (just a name)
Backup/restoreDirectly via host toolsContainer + tar pattern
Suitable forDevelopment, configurationDatabases, production data

tmpfs Mounts: Storage in RAM

A tmpfs mount stores data in the host's memory (RAM), not on disk. Data is written to memory and permanently lost when the container stops — this is intentional: the purpose of tmpfs is precisely to hold temporary data that shouldn't survive a reboot.

tmpfs mount untuk data sementara
docker run -d --mount type=tmpfs,dst=/tmp --tmpfs /tmp:size=256m my-app:1.0

Uses for tmpfs: (1) sensitive data — files whose contents are secrets/keys are written to RAM, never touching disk (and not carried along if the image is extracted); (2) high-performance temporary data — caches, sessions, temp files that don't need to persist and need RAM speed. A real example: putting /tmp and /run folders as tmpfs so applications don't write junk to the container layer.

An important warning: tmpfs consumes RAM. Large datasets (for example a database index) will drain memory and can make the host out of memory. Use tmpfs only for small data that truly needs to be fast and temporary. For large caches that may be lost but shouldn't eat RAM, consider a regular volume.

Managing Volumes via the CLI

Docker provides dedicated commands for managing volumes:

Perintah manajemen volume
docker volume create db-data
docker volume ls
docker volume inspect db-data
docker volume rm db-data
docker volume prune
docker volume ls
DRIVER    VOLUME NAME
local     db-data
local     e9a3c1d8f2b4

docker volume inspect db-data shows detailed metadata in JSON — including the physical location (Mountpoint) and mount options. Note that docker volume create is optional: if you docker run -v db-data:/path without creating it first, Docker automatically creates a volume named db-data. But creating it explicitly is clearer and allows special driver configuration. docker volume prune removes all volumes not currently used by any container — be careful: on a production machine, an "unused" volume could be a backup deliberately left in place.

-v vs --mount: Modern Syntax

There are two ways to write mounts in Docker: the short -v/--volume syntax (compatible with older Docker) and the long --mount syntax (more explicit, recommended for anything that isn't a simple volume).

-v vs --mount untuk tiga jenis mount
# Bind mount
-v /host/path:/container/path
--mount type=bind,source=/host/path,target=/container/path
 
# Named volume
-v my-vol:/app/data
--mount type=volume,src=my-vol,dst=/app/data
 
# tmpfs
--tmpfs /tmp:size=256m
--mount type=tmpfs,dst=/tmp,tmpfs-size=256m

The -v syntax is very compact and still the most widely used in older tutorials, but it has a weakness: for bind mounts, a source path that doesn't start with / can be misinterpreted as a named volume (the classic mystery of "why did my data go into a random volume?"). The --mount syntax forces you to state type= explicitly, making it clearer and unambiguous. For code that goes into a Compose file (episode 10), you'll use a key-value form similar to --mount. The modern recommendation: use --mount for complex options (read-only, tmpfs size, volume driver), and -v only for simple, well-understood bind mounts/named volumes.

Tip

The three flags :ro (read-only), :rw (read-write, default), and :z/:Z (SELinux context) often trip people up. -v /host/path:/container/path:ro makes the container unable to write to that mount — a strongly recommended pattern for configuration files and code images that the container must not modify.

Backup & Restore Volumes

This is the pattern that separates engineers who "understand volumes" from those who just "use volumes": the ability to back up and restore. Because named volumes are managed by Docker, the easiest way to back up is using a temporary container that mounts the volume along with a backup folder, then archives it with tar. Note the following pattern:

Backup volume db-data ke direktori host
docker run --rm \
  -v db-data:/data \
  -v /home/devnull/backups:/backup \
  alpine tar czf /backup/db-data-2026-08-02.tar.gz -C /data .

Let's break it down: --rm guarantees the temporary container is removed after completion. The first mount (-v db-data:/data) attaches the volume to be backed up at /data. The second mount (-v /home/devnull/backups:/backup) attaches the host folder where the archive is stored. The command alpine tar czf /backup/db-data-2026-08-02.tar.gz -C /data . compresses the entire contents of /data into a single .tar.gz file. The result: a complete archive of the volume is on the host, ready to download or move.

Restore is the reverse — extract the archive into the volume:

Restore volume dari file tar
docker run --rm \
  -v db-data:/data \
  -v /home/devnull/backups:/backup \
  alpine sh -c "tar xzf /backup/db-data-2026-08-02.tar.gz -C /data"

For archives created from a directory (-C /data .), make sure the restore also uses -C /data so the directory structure stays intact. This temporary-container pattern is very flexible: you can swap alpine for an image with other tools, and schedule it via cron on the host. For serious production, combine it with logical dumps (e.g. pg_dump for Postgres) — raw file archives are good for disaster recovery, logical dumps are good for cross-version migrations.

Real Example: PostgreSQL with a Named Volume

Let's apply everything to the most common production scenario: a PostgreSQL database. Note that the postgres image documents its data location: /var/lib/postgresql/data. If we place a named volume at that path, the database data lives in the volume — not in the container layer.

Postgres persisten dengan named volume
docker volume create pgdata
docker run -d --name postgres-db \
  -e POSTGRES_USER=app -e POSTGRES_PASSWORD=rahasia -e POSTGRES_DB=myapp \
  -p 5432:5432 \
  -v pgdata:/var/lib/postgresql/data \
  postgres:16-alpine

Now let's prove the persistence: create data, destroy the container, recreate it — the data must still be there.

Bukti: data bertahan dari recreate
docker exec -it postgres-db psql -U app -d myapp -c "CREATE TABLE notes (id serial PRIMARY KEY, title text);"
docker rm -f postgres-db
docker run -d --name postgres-db \
  -e POSTGRES_USER=app -e POSTGRES_PASSWORD=rahasia -e POSTGRES_DB=myapp \
  -p 5432:5432 \
  -v pgdata:/var/lib/postgresql/data \
  postgres:16-alpine
docker exec -it postgres-db psql -U app -d myapp -c "\dt"
Output — tabel tetap ada
        List of relations
 Schema | Name  | Type  | Owner
--------+-------+-------+-------
 public | notes | table | app

The first container was deleted with docker rm -f — and the notes table it created still exists, because its data lives in the pgdata volume, not in the container. This is exactly the expected behavior of a production database: the container can die, be updated, or move to another host (with the volume copied), and the data stays intact.

Common Pitfalls

  1. Bind mount permission problems. Containers run as a specific user (remember USER node from episode 6), but the host folder being bind-mounted is owned by a host user (e.g. UID 1000). Non-root containers often fail to write to such folders — the classic "Permission denied" error in development. Solutions: match the container user's UID/GID with the host user, or set the host folder permissions correctly. This is one reason bind mounts aren't ideal for production data needing strict permission control.

  2. Using bind mounts/volumes for code in production. In development, a bind mount for source code is great (hot reload). In production, don't. Code must be embedded in the image (via the episode 7 multi-stage build), not mounted from the host — otherwise the code isn't versioned, isn't reproducible, and the host becomes a single point of failure. The image is the only deployable artifact.

  3. tmpfs for large data. Putting large datasets on tmpfs will eat host RAM and can trigger the OOM killer. tmpfs is only for small, temporary, sensitive data.

  4. Ignoring backups. Volumes save data from the container lifecycle, but volumes don't save you from a broken disk, a lost host, or a mistyped docker volume prune. The volume itself can be lost along with its host. Regular backups (the tar pattern or dumps) are a must, not an option.

Important

There's a big difference between docker compose down (stops containers and networks, volumes remain) and docker compose down -v (removes volumes too). On a development machine full of test data, -v sounds harmless — until someone runs it in a production-like environment and loses all the data. Always think twice before adding -v.

Conclusion

In this episode 8 you've understood the root of the stateless container problem — the writable layer tied to a container's lifetime — and Docker's three storage mount solutions: bind mounts (direct host folders, for development and configuration), named volumes (Docker-managed at /var/lib/docker/volumes/, for databases and production), and tmpfs mounts (temporary data in RAM). You also learned to manage volumes with docker volume create/ls/inspect/rm/prune, compared the -v vs --mount syntax, practiced the backup-restore pattern with temporary containers + tar, and proved PostgreSQL persistence with a named volume that survives docker rm -f.

Core takeaway: critical data must not live inside a container — it must live in a volume, and that volume itself must be backed up. Containers are disposable citizens you can replace at any time; volumes are the ground where your data takes root.

From episode 9, one more question remains unanswered: how do containers talk to each other? So far we've connected applications to host ports manually (-p 3000:3000). In the next episode, episode 9, we'll dive into container networking — the bridge, host, none, macvlan, and overlay drivers; why the default bridge has no internal DNS; and how to make an application and database talk by container name on a custom network. See you in episode 9!

Learn Docker - Data Persistence & Storage (Volumes, Bind Mounts & tmpfs) | Learn Docker