# Docker Volume vs Bind Mount: Where Container Data Lives

> Docker volume vs bind mount explained: where container data lives, how a new volume is filled from the image, who owns the files, and what down -v deletes.

URL: https://www.matthewswong.com/en/learn/devops-from-zero/docker-volumes-vs-bind-mounts/

DevOps from zero · Lesson 20 of 25 · Reviewed on 18 October 2026 · By Matthews Wong (https://www.matthewswong.com/en/about/)

> A Docker volume is storage that Docker creates and manages outside any container, so its data survives when the container is removed. A bind mount shows a host folder instead, with the host's files and owners. A tmpfs mount keeps files in memory only. With none of them, writes land in the container's writable layer and vanish with it.

## You will be able to

- Choose a named volume, a bind mount or tmpfs for a piece of data, and say what each one survives.
- Explain when Docker copies an image's files and owners into a new volume, and fix a mount a non-root app cannot write to.
- Back up and restore a named volume with a throwaway container, and know which Compose command deletes it.

## Where a container's data lives

A container's writes go to its writable layer and vanish when a deploy recreates it; a named volume outlives the container and is filled from the image only while empty; a bind mount shows a host folder with the host's owner; and docker compose down -v deletes the volume.

1. **Meet the parts.** A container running as uid 1001, with its own writable layer. Outside it: the image it was made from, a named volume Docker manages, and a folder on the host.
2. **Writes land in the layer.** With no mount, everything the app writes goes to the container's writable layer. next/image saves each optimized cover under .next/cache/images, so that is where the cache lives.

   ```
   /app/.next/cache/images
   ```

   *Predict:* A deploy pulls a new image and recreates the container. What happens to the covers cached in the old writable layer? (The new container inherits them / They are deleted with the old container)
3. **A deploy throws it away.** docker compose up -d with a new image recreates the container. The old writable layer is removed with the old container, so every cover gets optimized again.

   ```
   docker compose up -d
   ```
4. **A volume, filled once.** Mount an empty named volume there and Docker first copies in what the image holds at that path, owners included. If the image has no folder there, the volume stays root-owned.

   ```
   - next-image-cache:/app/.next/cache/images
   ```

   *Predict:* The next deploy recreates the container, and the cache now lives in the named volume. What does the new container find there? (The same cached covers / An empty cache again / A fresh copy from the new image)
5. **Recreate keeps the volume.** The volume lives outside every container. Compose recreates the container, mounts the same volume, and the cached covers are still there. Nothing is copied again, because the volume is not empty.

   ```
   docker compose pull && docker compose up -d
   ```
6. **A bind mount: the host wins.** Swap in a bind mount and the container sees the host folder, owner and all, with nothing copied. A folder -v creates on Linux is root-owned, so uid 1001 cannot write.

   ```
   - ./cache:/app/.next/cache/images
   ```
7. **down -v deletes it.** docker compose down removes containers and networks but keeps named volumes. Add -v and Compose also deletes the volumes the file declares, and everything in them.

   ```
   docker compose down -v
   ```

## What is a Docker volume?

A Docker volume is storage that Docker creates and manages for containers, outside any one of them. It matters because of where data goes otherwise: everything a container writes lands in its writable layer, which belongs to that container alone, is hard for another process to get data out of, and, as Docker's docs put it, doesn't persist when the container is destroyed. A container is replaced on every deploy, so anything worth keeping has to live somewhere else.

Docker offers three places to put it: volumes, bind mounts and tmpfs mounts. A volume's data sits in Docker's own storage directory on the host, and docker volume inspect shows where. Its contents exist outside the lifecycle of any container: remove the container and the volume stays, even when nothing uses it. Docker calls volumes the preferred mechanism for persisting data that containers generate and use, and easier to back up or migrate than bind mounts.

A named volume has a name you choose, like this site's next-image-cache, so every new container can mount the same one. An anonymous volume gets a random name instead; start its container with --rm and Docker removes the anonymous volume when the container exits.

## How is a bind mount different?

A bind mount puts a file or folder from the host into the container. You choose the path, and the host and the container read and write the same files. That is what you want for source code during development, and Docker points to bind mounts whenever both sides need the files. It is also a risk: a bind mount has write access to the host's files by default, so add ro when the container only needs to read.

A bind mount brings the host folder as it is, owners and permissions included. If the host path does not exist, -v creates it, always as a folder, while --mount stops with an error; Compose creates it too unless create_host_path is turned off. In a test against a Linux Docker Engine 29.8.1, the folder -v created was owned by root with mode 755, and a process running as uid 1001 got Permission denied. On Docker Desktop for Mac, with the folder shared from macOS, the same write succeeded, so try that case on a Linux machine.

## What happens when a mount lands on files the image already has?

Mount anything over a folder the image fills and the image's files there are hidden for as long as the mount exists. A bind mount always hides them, and so does a volume that already holds files. Docker's docs add that there is no straightforward way to reveal the hidden files again; recreate the container without the mount.

The exception is an empty volume, which includes every brand-new one. By default Docker first copies the folder's existing files into it, which its docs call a good way to pre-populate data. The Dockerfile reference says the same of VOLUME: docker run initializes the new volume with whatever exists at that location in the image. The volume-nocopy option, nocopy in Compose, turns the copy off, and a bind mount is never filled this way.

Owners come along with the copy. Docker's pages do not spell this out, so it was tested on Docker Engine 29.8.1: a new volume mounted where the image had a folder owned by uid 1001 came up owned by 1001. Mounted at a path the image does not contain, the same kind of volume came up owned by root, and uid 1001 could not write to it. The copy also runs only while the volume is empty: one that already holds files keeps them, and their owners, when a new image arrives.

## When is tmpfs the right choice?

A tmpfs mount is temporary and lives only in the host's memory. When the container stops, the mount is removed and its files with it, and containers cannot share one. Docker documents it for Docker on Linux; Docker Desktop runs containers in a Linux VM, which is where the try-it below tested it.

Use it for scratch files you never need on disk. Two limits from Docker's docs: what you write there counts against the container's memory limit, and the kernel may still swap it to disk. With --mount and no size set, it can grow to half of the host's RAM.

## Why does this site keep its image cache in a named volume?

next/image optimizes each cover the first time it is requested and caches the result in the cache/images folder of Next.js's build directory, here .next/cache/images. Without a mount, that folder sits in the writable layer, and every deploy runs docker compose up -d with a new image, which stops and recreates the container. Docker's reference says that recreate preserves mounted volumes, so a named volume carries the cache from one container to the next.

The compose file's comment also says why not a bind mount: a host folder Docker creates would be root-owned, while a named volume would take its owner from the image's uid 1001. Both rules are right, but the second needs a folder at that path in the image, and the runner stage does not create one: it copies Next's standalone output, which has no cache folder, and the static files. As the files stand, the volume most likely starts out owned by root, as lesson 5 also found. The fix is one RUN line in the runner, before USER nextjs, that creates .next/cache/images and gives it to nextjs.

## What deletes a volume?

Removing a container never removes a named volume; Docker keeps it until you remove it. docker compose down removes the project's containers and networks and leaves named volumes, and even anonymous ones, in place. docker compose down -v also removes the named volumes declared in the file's volumes section and the anonymous volumes attached to its containers. Volumes marked external are never removed. This site's deploy runs docker compose pull and up -d, never down -v, so the cache volume outlives every release.

docker volume rm and docker volume prune delete volumes directly; lesson 15 covers which prune flags reach named volumes.

## How do you back up a named volume?

With a throwaway container. Docker's recipe starts a container with --rm that mounts the volume and a host folder, runs tar to write an archive into the host folder, and exits; restoring is the same in reverse, into a new volume. Docker's example reaches the volume through --volumes-from; mounting it by name, as the try-it below does, works the same way.

A tar archive is only as consistent as the files were while tar read them. PostgreSQL's docs say a file-system-level backup needs the server shut down, because tar does not take an atomic snapshot; for a database, stop it first or use its own dump tool. This site's volume holds only a cache it can rebuild, so it needs no backup at all.

## The real config behind this lesson

`docker-compose.yml:14-23`

```yaml
    volumes:
      # Persist next/image's optimised-variant cache across deploys.
      # next.config.ts sets minimumCacheTTL to a year, but the cache lived in the
      # container's writable layer and every deploy replaces the container — so
      # the first request for each cover after a deploy paid a full JPEG decode
      # plus an AVIF encode again, on 1 vCPU under a 512 MB cap. A named volume
      # rather than a bind mount, so Docker seeds its ownership from the image's
      # uid 1001 (Dockerfile:35) instead of creating a root-owned host dir the
      # container cannot write to.
      - next-image-cache:/app/.next/cache/images
```

The comment carries both reasons for the design. A deploy replaces the container, and Compose keeps a mounted volume when it recreates one, so the cache survives. The ownership half holds only if the image has a folder at /app/.next/cache/images for Docker to copy, and nothing in the runner stage creates one. Its line reference has drifted too: the uid 1001 user is created at Dockerfile line 63 now.

`docker-compose.yml:49-50`

```yaml
volumes:
  next-image-cache:
```

The top-level entry declares the volume with no options, so Docker creates it with the engine's defaults the first time docker compose up runs, and reuses it after that. Compose prefixes the name with the project's, which is why docker volume ls shows a longer name than the one in the file. Only removing it on purpose, or docker compose down -v, deletes it.

`Dockerfile:66-67`

```dockerfile
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
```

These two lines give uid 1001 the standalone server, its .next folder and the static files. Next's standalone output has no cache folder, which a local build confirms, so the image holds nothing at .next/cache/images for Docker to copy into a new volume. Creating that folder in the runner, owned by nextjs, is the step the compose comment assumes.

## What breaks

- **Permission denied on a fresh volume or bind mount.** The app runs as a non-root user and the mount point is owned by root: a host folder that -v created, or a new volume at a path the image never had. Create the folder in the Dockerfile and give it to the app user before USER, or chown the host folder to the app's uid. Check with stat inside the container: the owner is a number, and 0 means root.
- **The database is empty after docker compose down -v.** -v reads like a cleanup flag, but it removes the named volumes the file declares, and the data in them, not just the containers. Use plain down to stop a project. When you do mean to remove a volume, back it up first with a throwaway container.
- **A bind mount hides what the image put there.** Mount a host folder over a path the image fills, such as the app's own code or nginx's html folder, and those files disappear inside the container, because the mount covers them. Mount at a separate path, or use a named volume, which an empty start fills from the image.
- **A new image, an old volume.** Docker copies the image's files only into an empty volume. Ship new default files in a new image and an existing volume keeps the old ones, because its content hides the image's. Keep defaults out of the mounted path, or update the volume yourself.

### Named volume, bind mount or tmpfs: where the data lives and what it survives, as Docker documents them.

| Mount | Where the data lives | After the container is removed | At first mount | Good for |
| --- | --- | --- | --- | --- |
| Named volume | Docker's storage area on the host, managed by Docker | Kept until you remove it or run down -v | If empty, a copy of the image's folder at that path | App data, databases, caches that must survive deploys |
| Bind mount | A host path you choose | Kept: it is the host's own folder | The host folder as it is, hiding the image's files | Source code in development, config files edited on the host |
| tmpfs | Host memory | Gone: removed when the container stops | An empty folder | Scratch files you do not need on disk |

## Try it yourself

You need Docker on a laptop and a terminal (macOS, Linux or WSL). Everything happens in one empty folder, every name starts with demo-, and the last step removes it all.

1. Make a working folder, create a named volume and inspect it. Mountpoint is a path in Docker's own storage area; on Docker Desktop that sits inside its Linux VM, not on your laptop's disk.

   ```bash
   mkdir demo-volumes && cd demo-volumes
   docker volume create demo-data
   docker volume inspect demo-data
   ```
2. Write a file from one throwaway container, then read it from another. The first container is already gone; the file is still there and prints hello.

   ```bash
   docker run --rm -v demo-data:/data alpine:3 sh -c 'echo hello > /data/note.txt'
   docker run --rm -v demo-data:/data alpine:3 cat /data/note.txt
   ```
3. Mount a brand-new volume over nginx's html folder, then an empty host folder over the same path. The volume lists 50x.html and index.html, copied from the image; the bind mount lists nothing, because it hides them.

   ```bash
   docker run --rm -v demo-html:/usr/share/nginx/html nginx:alpine ls /usr/share/nginx/html
   mkdir site
   docker run --rm -v "$PWD/site":/usr/share/nginx/html nginx:alpine ls /usr/share/nginx/html
   ```
4. Owners come along with the copy. node:22-alpine has a node user, uid 1000, that owns /home/node, so a new volume there prints 1000. At a path the image does not have, the volume prints 0, which is root, and the node user gets Permission denied.

   ```bash
   docker run --rm -v demo-home:/home/node node:22-alpine stat -c '%u %n' /home/node
   docker run --rm -u node -v demo-missing:/home/node/cache node:22-alpine \
     sh -c 'stat -c "%u %n" /home/node/cache; touch /home/node/cache/x'
   ```
5. A tmpfs mount lives in memory: df reports tmpfs as the filesystem, and nothing written there outlives the container.

   ```bash
   docker run --rm --tmpfs /scratch alpine:3 df -h /scratch
   ```
6. Back up the volume with a throwaway container that writes a tar archive into this folder, then restore it into a new volume and read the file back.

   ```bash
   docker run --rm -v demo-data:/data:ro -v "$PWD":/backup alpine:3 tar czf /backup/demo-data.tgz -C /data .
   docker volume create demo-restored
   docker run --rm -v demo-restored:/data -v "$PWD":/backup alpine:3 tar xzf /backup/demo-data.tgz -C /data
   docker run --rm -v demo-restored:/data alpine:3 cat /data/note.txt
   ```
7. Now with Compose: one service and a named volume. Write a file, force a brand-new container, and read the file again. docker compose ps -q prints a different container ID the second time; the file still says kept.

   ```bash
   printf 'services:\n  app:\n    image: nginx:alpine\n    volumes:\n      - data:/data\nvolumes:\n  data:\n' > compose.yaml
   docker compose up -d
   docker compose exec app sh -c 'echo kept > /data/note.txt'
   docker compose ps -q
   docker compose up -d --force-recreate
   docker compose ps -q
   docker compose exec app cat /data/note.txt
   ```
8. docker compose down keeps the volume, listed as demo-volumes_data, so the file survives another up. down -v deletes it, and the last list comes back empty.

   ```bash
   docker compose down
   docker volume ls --filter name=demo-volumes
   docker compose up -d
   docker compose exec app cat /data/note.txt
   docker compose down -v
   docker volume ls --filter name=demo-volumes
   ```
9. Clean up the volumes and the folder.

   ```bash
   docker volume rm demo-data demo-html demo-home demo-missing demo-restored
   cd .. && rm -rf demo-volumes
   ```

## Quick quiz

1. **A Postgres service keeps its data in a named volume. You run docker compose down, then docker compose up -d. What happened to the data?**
   A. Deleted, because down removes everything in the project
   B. Kept, because down leaves named volumes alone
   C. Kept only if the image did not change
   Answer: B. down removes containers and networks. Named volumes declared in the file survive it, whatever the image, unless you add -v.

2. **A new named volume is mounted at /app/uploads, a path the image does not contain. The app runs as uid 1001. What happens when it writes there?**
   A. Permission denied: the volume starts out owned by root
   B. It works: Docker gives a new volume to the container's user
   C. It works: volumes are writable by everyone
   Answer: A. Docker copies files and owners into an empty volume only from a folder the image has at that path. With none, the volume stays owned by root. Create the folder in the image, owned by uid 1001, before USER.

3. **You bind-mount an empty host folder over /usr/share/nginx/html in the nginx image. What is in that folder inside the container?**
   A. The image's index.html, copied into the host folder
   B. The image's files merged with the host folder's
   C. Nothing: the empty host folder hides the image's files
   Answer: C. A bind mount never copies. It shows the host folder and hides whatever the image had at that path until the mount is gone. Only an empty volume gets a copy.

## Think it through

- **Why does this site's image cache need a volume at all?** Every deploy recreates the container, and the writable layer is removed with the old one. A named volume lives outside the container, so Compose mounts the same one into each new container.
- **When does Docker copy the image's files into a volume, and when does it not?** Only when the volume is empty at mount time and the image has something at that path. A volume that already holds files keeps them, new image or not, and a bind mount is never filled.
- **Which commands delete a Compose project's named volumes?** docker compose down -v, docker volume rm, and volume prune with the flag that includes named volumes. A plain down, a recreate and removing a container all leave named volumes alone.

## Go deeper

- [Learn Docker from Scratch: First Container in 6 Steps](https://www.matthewswong.com/en/blog/belajar-docker-dari-nol-panduan-pemula/)
- [PostgreSQL Backup Strategy for VPS: From pg_dump to WAL Archiving](https://www.matthewswong.com/en/blog/postgresql-backup-strategy/)
- [restic vs BorgBackup: Encrypted, Deduplicated Backups](https://www.matthewswong.com/en/blog/restic-vs-borg-encrypted-backups/)

## Sources

- [Docker docs: Storage overview (writable layer, mount types)](https://docs.docker.com/engine/storage/)
- [Docker docs: Volumes (lifecycle, populating, backup and restore)](https://docs.docker.com/engine/storage/volumes/)
- [Docker docs: Bind mounts](https://docs.docker.com/engine/storage/bind-mounts/)
- [Docker docs: tmpfs mounts](https://docs.docker.com/engine/storage/tmpfs/)
- [Docker docs: Dockerfile reference, VOLUME](https://docs.docker.com/reference/dockerfile/#volume)
- [Docker docs: Compose top-level volumes](https://docs.docker.com/reference/compose-file/volumes/)
- [Docker docs: Compose services reference, volumes](https://docs.docker.com/reference/compose-file/services/#volumes)
- [Docker docs: docker compose up (recreate preserves volumes)](https://docs.docker.com/reference/cli/docker/compose/up/)
- [Docker docs: docker compose down (-v, --volumes)](https://docs.docker.com/reference/cli/docker/compose/down/)
- [Next.js docs: Image component (where optimized images are cached)](https://nextjs.org/docs/app/api-reference/components/image)
- [PostgreSQL docs: File system level backup](https://www.postgresql.org/docs/current/backup-file.html)

---

- HTML version: https://www.matthewswong.com/en/learn/devops-from-zero/docker-volumes-vs-bind-mounts/
- Bahasa Indonesia: https://www.matthewswong.com/id/learn/devops-from-zero/docker-volumes-vs-bind-mounts/
- Site index for agents: https://www.matthewswong.com/llms.txt
- Sitemap: https://www.matthewswong.com/sitemap.xml
- API description (OpenAPI): https://www.matthewswong.com/openapi.json
