---
title: "06 — Data Persistence: Why Container Files Vanish (and How to Stop Them)"
uid: data-persistence-in-docker
tags: ["ephemeral", "volumes", "roadmap:docker", "docker", "storage", "bind-mounts"]
excerpt: "A file saved inside a container is gone by morning because containers are ephemeral by default. Persistence means attaching storage from outside the writable layer."
date: 2026-08-13T03:28:14+0000
source: https://www.aveshina.my.id/en/blog/data-persistence-in-docker
---

A file I saved inside a container was gone the next morning, and I had no vocabulary for why. The one sentence that explained it: **containers are ephemeral by default, so anything I want to keep has to live in storage attached from outside the container's writable layer.** [1][2]

The framing worth holding onto is that "container storage" is not one thing — it's three modes with different lifetimes, owners, and use cases. The bug I kept hitting (data disappearing) was me using the wrong one by default. Once I could name the three modes, the choice became obvious [1].

## Why the default loses data

By default, everything a container writes lands in its **writable layer** — a thin scratchpad Docker stacks on top of the image's read-only layers [1][2]. That scratchpad exists for the lifetime of *the container*, not the image. The moment I docker rm the container, the writable layer is discarded, and every file the process wrote disappears with it.

```figure
<svg viewBox="0 0 720 280" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="Two scenarios. Left: a running container with a writable layer holding a file 'data.db'. Arrow: docker rm. Result: the writable layer is gone, the file is lost. Right: the same container, but a named volume is mounted at the data path. Arrow: docker rm. Result: the container is gone but the volume (and data.db) survives, ready for a new container to attach.">
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">

    <!-- LEFT scenario -->
    <text x="160" y="22" font-size="12" font-weight="700" fill="#7f1d1d" text-anchor="middle">No volume — data lost</text>

    <rect x="60" y="40" width="200" height="40" rx="6" fill="#fee2e2" stroke="#dc2626" stroke-width="1.4"/>
    <text x="160" y="64" font-size="10.5" font-weight="700" fill="#7f1d1d" text-anchor="middle">writable layer — data.db</text>
    <rect x="60" y="86" width="200" height="14" rx="3" fill="#fef9c3" stroke="#ca8a04" stroke-width="1"/>
    <rect x="60" y="104" width="200" height="14" rx="3" fill="#fef9c3" stroke="#ca8a04" stroke-width="1"/>
    <rect x="60" y="122" width="200" height="14" rx="3" fill="#fef9c3" stroke="#ca8a04" stroke-width="1"/>
    <text x="160" y="158" font-size="10" font-style="italic" fill="#64748b" text-anchor="middle">docker rm → writable layer gone</text>

    <rect x="60" y="180" width="200" height="40" rx="6" fill="#e2e8f0" stroke="#94a3b8" stroke-width="1.2" stroke-dasharray="4 3"/>
    <text x="160" y="204" font-size="10.5" fill="#64748b" text-anchor="middle">data.db — gone</text>

    <!-- RIGHT scenario -->
    <text x="560" y="22" font-size="12" font-weight="700" fill="#052e16" text-anchor="middle">Volume — data survives</text>

    <rect x="460" y="40" width="200" height="40" rx="6" fill="#dcfce7" stroke="#16a34a" stroke-width="1.4"/>
    <text x="560" y="64" font-size="10.5" font-weight="700" fill="#052e16" text-anchor="middle">writable layer (empty)</text>
    <rect x="460" y="86" width="200" height="14" rx="3" fill="#fef9c3" stroke="#ca8a04" stroke-width="1"/>
    <rect x="460" y="104" width="200" height="14" rx="3" fill="#fef9c3" stroke="#ca8a04" stroke-width="1"/>
    <rect x="460" y="122" width="200" height="14" rx="3" fill="#fef9c3" stroke="#ca8a04" stroke-width="1"/>
    <text x="560" y="158" font-size="10" font-style="italic" fill="#64748b" text-anchor="middle">docker rm → container gone</text>

    <!-- volume outside -->
    <rect x="460" y="180" width="200" height="40" rx="6" fill="#bbf7d0" stroke="#16a34a" stroke-width="1.5"/>
    <text x="560" y="197" font-size="10.5" font-weight="700" fill="#052e16" text-anchor="middle">named volume: pgdata</text>
    <text x="560" y="212" font-size="10" fill="#052e16" text-anchor="middle">data.db — survives</text>
    <path d="M560,124 L560,180" fill="none" stroke="#16a34a" stroke-width="1.4" stroke-dasharray="3 3"/>
  </g>
</svg>
```

This is the "ephemeral container filesystem" behavior, and it's not a bug — it's the design. Stateless-by-default is what lets me treat containers as disposable: rebuild, replace, scale up, scale down, all without worrying about leftover state. The cost is that anything I *do* want to keep has to be opted into persistence explicitly [1][2].

## The three storage modes

Docker gives me three ways to attach storage, and they differ in who manages the data and where it physically lives.

**1. The writable layer (default, ephemeral).** Every container gets one. Fast, isolated, and thrown away with the container. Use it only for scratch files I'm happy to lose.

**2. Volumes (managed by Docker, persistent).** A volume is a directory Docker creates and manages on the host, in its own storage area, and mounts into the container at a path I choose [3]. The container writes to /var/lib/postgresql/data; underneath, Docker is writing to a managed directory on the host. The container can be removed and recreated; the volume — and its data — stays.

```
docker volume create pgdata
docker run -d -v pgdata:/var/lib/postgresql/data postgres:16
```

Volumes are the recommended default for persistent app data. They're independent of the host's directory layout (so they work the same on Linux, Mac, and Windows), Docker manages their lifecycle, and they can be shared between containers [3].

**3. Bind mounts (a host path, mounted directly).** A bind mount wires a specific absolute path on the host into the container. The container is now writing to a real directory I can see from my shell, outside Docker's management [4][5].

```
docker run -d -v "$PWD:/app" -w /app node:20 node server.js
```

The trade-off: bind mounts are tethered to the host's filesystem layout (so they don't port across machines cleanly), and the container can write anywhere in that host path, which is a real footgun. Their superpower is *live editing* — I mount my source directory, edit a file on the host, and the container sees the change instantly. That's the backbone of hot-reloading dev workflows [4].

## Choosing between them

The decision collapses to two questions. *Who needs to manage the data?* and *Does it need to be portable?*

- **Application data that must survive container recreation** (database files, uploads, logs I care about) → **volume**. Docker manages it, it survives removals, and it's portable [3].
- **Source code I want to live-edit during development** → **bind mount**. The container sees my host files change instantly, which is what makes dev loops fast [4].
- **Throwaway scratch** → **writable layer**. Don't pay the cost of a volume for data I want gone anyway.

## Sharing data between containers

One detail I underused at first: volumes aren't tied to a single container. Two containers can mount the same volume and read/write the same files, which is how one container writes data and another reads it. A common pattern is a "data-only" helper container, or just attaching the same named volume to several services in a Compose file. Bind mounts share the same trick — mount the same host path into multiple containers and they're looking at the same files [3][4].

## How I use this

The habit I built is to ask, before I docker run anything stateful: "what path holds the data I'd be sad to lose?" Then I attach a volume at exactly that path and nothing else. For databases that's the data directory; for an upload service it's the uploads folder; for a CI runner it's the workspace cache. For dev work where I'm editing code, I reach for a bind mount of my source directory and accept the portability trade-off because the live-reload payoff is worth it. The single question — "is this path ephemeral, and if not, where does the real storage live?" — is what stopped me from losing data to the writable layer.

## References

[1] Docker, Inc., "Persisting container data," Docker Docs, 2024. [Online]. Available: [https://docs.docker.com/get-started/docker-concepts/running-containers/persisting-container-data/](https://docs.docker.com/get-started/docker-concepts/running-containers/persisting-container-data/)

[2] Docker, Inc., "Concepts — Persisting container data (video)," YouTube, 2024. [Video]. Available: [https://www.youtube.com/watch?v=10_2BjqB_Ls](https://www.youtube.com/watch?v=10_2BjqB_Ls)

[3] Docker, Inc., "Volumes," Docker Docs, 2024. [Online]. Available: [https://docs.docker.com/storage/volumes/](https://docs.docker.com/storage/volumes/)

[4] Docker, Inc., "Bind mounts," Docker Docs, 2024. [Online]. Available: [https://docs.docker.com/storage/bind-mounts/](https://docs.docker.com/storage/bind-mounts/)

[5] Docker, Inc., "Choose the -v or --mount flag," Docker Docs, 2024. [Online]. Available: [https://docs.docker.com/storage/bind-mounts/#choose-the--v-or---mount-flag](https://docs.docker.com/storage/bind-mounts/#choose-the--v-or---mount-flag)

```quiz
Q: A container writes a file to /tmp/data.db in its writable layer, then is removed with docker rm. What happens to the file?
- It is committed into the image automatically
- It is discarded — the writable layer is deleted with the container
correct: 1
explain: The writable layer only exists for the lifetime of the container. Removing the container deletes the layer and everything in it.

Q: Which storage mode is the recommended default for persistent application data like database files?
- The container's writable layer
- A named volume, mounted at the data path
correct: 1
explain: Volumes are Docker-managed, independent of host layout, survive container recreation, and can be shared between containers. That makes them the right choice for data that must persist.

Q: What is the main advantage of a bind mount for development?
- It works identically across all host operating systems
- It mounts a host directory directly, so file changes on the host are seen by the container instantly — enabling live reload
correct: 1
explain: Bind mounts wire a host path into the container, so editing on the host is reflected immediately. The trade-off is host-layout coupling and a larger attack surface for the container.

Q: Two containers mount the same named volume. This means…
- they each get their own private copy of the data
- they read and write the same underlying files
correct: 1
explain: A volume is shared storage. Multiple containers mounting it see and modify the same files, which is how producer/consumer and sidecar patterns pass data.

Q: Why is "stateless by default" considered a feature of containers, not a limitation?
- Because it makes images larger
- Because disposable, replaceable containers are only safe to throw around when their state lives elsewhere
correct: 1
explain: Ephemeral writable layers are what let me rebuild, replace, and scale containers freely. Persistence is opt-in, layered on via volumes or backing services where it's actually needed.
```
