Skip to main content
DevOps from zero · Lesson 20 of 25 · 14 min

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.

By Matthews WongReviewed on

Key takeaway

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.
Watch it work

Step through the diagram

Press Play, or step with the arrows. Every step is written out below the diagram.

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.

Step 1 of 7. 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.

Keys: ← → step · Home / End · K play or pause

  1. 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. 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/imagesPredict the next step 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. 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. 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/imagesPredict the next step 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. 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. 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. 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.

Read more (2)

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.

Read more (1)

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.

Read more (2)

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.

Read more (1)

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.

Read more (1)

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.

Read more (1)

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.

Read more (1)

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.

How this site does it

The real config behind this lesson

Excerpts from the repository that builds and deploys this website, quoted as they are.

docker-compose.yml:14-23
    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
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
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

Mistakes that cost real time

  • 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.
Named volumeDocker's storage area on the host, managed by DockerKept until you remove it or run down -vIf empty, a copy of the image's folder at that pathApp data, databases, caches that must survive deploys
Bind mountA host path you chooseKept: it is the host's own folderThe host folder as it is, hiding the image's filesSource code in development, config files edited on the host
tmpfsHost memoryGone: removed when the container stopsAn empty folderScratch files you do not need on disk
Try it yourself

Do it on your own machine

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.

    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.

    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.

    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.

    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.

    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.

    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.

    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.

    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.

    docker volume rm demo-data demo-html demo-home demo-missing demo-restored
    cd .. && rm -rf demo-volumes
Quick quiz

Three questions before you move on

Pick an answer to see why it is right or wrong. Nothing is graded, so guess freely; a miss is kept in this browser so the course can ask it again later.

  1. 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?
  2. 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?
  3. 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?
From earlier lessons

Two questions from earlier lessons

Recalling an older lesson after a gap is what makes it stick. Answer from memory before you look anything up.

  1. 1.Your service runs no task on node C. A client connects to node C on the published port. What happens?
  2. 2.You change an A record whose TTL is 3600. Twenty minutes later a colleague still gets the old address. What is happening?

From: 19. Docker Swarm Tutorial: Services, Routing Mesh, Rolling Updates · 17. DNS Records Explained: A, CNAME, MX, TXT, TTL and the Proxy

Think it through

Answer in your head, then check

  1. 1.Why does this site's image cache need a volume at all?Show the answer

    Answer: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.

  2. 2.When does Docker copy the image's files into a volume, and when does it not?Show the answer

    Answer: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.

  3. 3.Which commands delete a Compose project's named volumes?Show the answer

    Answer: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

Related deep dives on this site