Server

Docker Volumes vs Bind Mounts vs tmpfs

By Prabath Thalangama· October 24, 2026· 3 min read
#docker#storage#volumes

Introduction

Containers are ephemeral; their writable layer vanishes on docker rm. To keep data — or share files with the host — you mount storage. There are three kinds and they behave differently.

The three types

Type Where it lives Use for
Named volume /var/lib/docker/volumes/<name> (Docker-managed) databases, uploads, anything that must survive
Bind mount any host path you choose source code in dev, config files, host logs
tmpfs host RAM, never on disk secrets, scratch space, caches you don't want persisted
# Named volume
docker run -v pgdata:/var/lib/postgresql/data postgres:16

# Bind mount (absolute host path, or ./ in compose)
docker run -v /home/me/project:/app node:20

# tmpfs
docker run --tmpfs /run:rw,size=64m myapp

# Explicit long form (clearer, recommended)
docker run --mount type=volume,source=pgdata,target=/var/lib/postgresql/data postgres:16
docker run --mount type=bind,source="$(pwd)",target=/app node:20

Named volumes

Docker creates and owns them. Benefits: portable (no host path assumptions), work identically on Linux/macOS/Windows, can use volume drivers (NFS, cloud block storage), and docker volume prune cleans up orphans.

docker volume create pgdata
docker volume ls
docker volume inspect pgdata
docker volume rm pgdata

Backup a named volume:

docker run --rm -v pgdata:/data -v "$(pwd)":/backup alpine \
  tar czf /backup/pgdata-$(date +%F).tar.gz -C /data .

# Restore
docker run --rm -v pgdata:/data -v "$(pwd)":/backup alpine \
  sh -c "cd /data && tar xzf /backup/pgdata-2026-08-27.tar.gz"

Bind mounts

You control the exact host path. Essential for development (edit on host, run in container). Downsides: the host path must exist and match across machines; performance is poor on macOS/Windows (the FS crosses a VM boundary); and permissions get messy.

The permission problem

The container process runs as some UID (often root, or a fixed app UID like 1000). Files it creates on a bind mount are owned by that UID on the host. If the container runs as root, you get root-owned files in your project directory.

Fixes:

  • Run the container as your host user: docker run -u "$(id -u):$(id -g)" ...
  • Use USER in the Dockerfile with a UID that matches yours, or make it configurable via build arg.
  • For named volumes this rarely matters (Docker manages the path); it's mostly a bind-mount issue.

tmpfs

services:
  app:
    tmpfs:
      - /tmp
      - /run:size=32m

Data never touches disk and is gone when the container stops. Good for decrypted secrets, session scratch, and reducing writes on flash storage.

Verification and troubleshooting

docker inspect -f '{{json .Mounts}}' <container> | jq
docker exec <container> ls -lan /var/lib/postgresql/data
docker system df -v          # space used by volumes
  • Data disappears on docker compose down — plain down keeps named volumes; down -v deletes them. If it still vanishes, you used an anonymous volume (-v /var/lib/postgresql/data with no name) — a new one is created each run.
  • "permission denied" writing to a bind mount — container UID ≠ host file owner, or SELinux (:z/:Z suffix needed on RHEL: -v $(pwd):/app:Z).
  • Bind-mounted node_modules/vendor empty or wrong — the host directory shadowed the image's. Add an anonymous volume over the subpath (see the Compose post).
  • macOS: builds/tests crawl — bind mount FS overhead. Mount less, use :delegated, or keep dependencies in a named volume.
  • Named volume has old data after you "changed the init script" — Postgres et al. only run init scripts on an empty data dir. docker volume rm it.
  • Can't delete a volume: "volume is in use" — a stopped container still references it. docker rm that container first, or docker volume rm -f.
PrabathStuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.