TechEarl

Docker Volumes vs Bind Mounts (and When to Use Each)

Three ways to persist data with Docker: named volumes, bind mounts, and tmpfs. What each one actually does, where the bytes live on disk, and the right choice for databases, dev workflows, and caches.

Ishan KarunaratneIshan Karunaratne⏱️ 8 min readUpdated
Share thisCopied

Three ways to persist data in a Docker container: named volumes, bind mounts, and tmpfs. They look similar at the CLI (-v something:/path/in/container), but they behave very differently and serve different jobs. Most "I lost my data" disasters trace back to picking the wrong one for the job.

The short version

Mount typeWhat it isBest for
Named volume (-v name:/path)Docker-managed storage at a path Docker choosesDatabases, app state, anything you want to persist and you don't care where it lives
Bind mount (-v /host/path:/path)A host directory mapped into the containerEditing source code from the host, host-side backups, sharing config files
Anonymous volume (-v /path)An unnamed Docker-managed volumeAlmost never. Use a named volume instead.
tmpfs (--tmpfs /path)In-memory onlyScratch space, caches that don't need to survive container restart

For databases and other stateful services, default to named volumes. For development where you're editing files on the host and want changes reflected in the container, bind mounts. For everything else, named volumes.

Named volumes

bash
docker run -d -v pgdata:/var/lib/postgresql/data postgres:17

Docker creates a volume named pgdata (if it doesn't exist) and mounts it at /var/lib/postgresql/data inside the container. The actual bytes live in /var/lib/docker/volumes/pgdata/_data/ on a Linux host, or inside the Docker Desktop VM on Mac/Windows.

The key properties:

  • Survives container removal. docker rm does not touch the volume. You can stop, remove, and recreate the container; the data is intact.
  • Surive even docker compose down — but not docker compose down -v (the -v flag explicitly removes named volumes).
  • Docker manages the on-disk location — you don't pick the path, you pick the name. Cleaner ownership, no UID surprises on Mac/Windows.

Manage them:

bash
docker volume ls                    # list
docker volume inspect pgdata        # driver, mountpoint, labels
docker volume rm pgdata             # remove (must not be in use)
docker volume prune                 # remove all unused (data loss; asks for confirmation)

Bind mounts

bash
docker run -d -v /home/me/myproject:/app node:22 npm start

Maps the host directory /home/me/myproject to /app inside the container. Both directions: changes inside the container modify the host directory; changes on the host appear inside the container.

The key properties:

  • You choose the host path. Perfect for "this is my project; edit it on the host."
  • Performance varies by platform. Native filesystem access on Linux is fast. Docker Desktop on Mac/Windows uses a virtualized filesystem that's slower, especially with thousands of small files.
  • UID/GID matters. A container process writing to a bind mount creates files owned by whatever UID it ran as (root by default). Those files appear on the host with that ownership. Mac/Windows abstract this away to some extent; Linux exposes it directly.

Modes:

bash
-v /host/path:/path:ro        # read-only
-v /host/path:/path:rw        # read-write (default)
-v /host/path:/path:z         # SELinux relabel (private)
-v /host/path:/path:Z         # SELinux relabel (shared)

When to use which

Databases → named volume, always.

bash
docker run -d --name pg -v pgdata:/var/lib/postgresql/data postgres:17
  • The container's UID 999 (or whatever Postgres runs as) needs to own the data directory; named volumes handle this transparently.
  • On Mac/Windows, bind-mounted database directories are noticeably slower and occasionally hit permission issues.
  • You don't need direct host filesystem access to the database files; you'd use pg_dump instead.

Source code in development → bind mount.

bash
docker run -d -v "$(pwd):/app" -w /app node:22 npm run dev
  • You edit on the host with your editor, the container picks up changes immediately for hot reload.
  • Files created by the container appear on the host (a build output, a generated migration). That's usually what you want.

The hybrid: bind-mount source + named volume for node_modules.

yaml
services:
  app:
    image: node:22
    volumes:
      - .:/app
      - app-node_modules:/app/node_modules

volumes:
  app-node_modules:

This is the trick that keeps dev fast on Mac/Windows. The source bind mount gives you live editing; the named volume overlay on node_modules keeps that directory in a Docker-managed location instead of the slow bind-mount space. Without this, npm install and any tool that walks node_modules is dramatically slower.

Config files → bind mount, read-only.

bash
docker run -d -v ./nginx.conf:/etc/nginx/conf.d/default.conf:ro nginx:alpine
  • One file, edited on the host, mounted into the container.
  • :ro prevents the container from modifying your host file.

Caches and tmpfiles → tmpfs.

bash
docker run -d --tmpfs /tmp:size=100m my-app
  • Lives in RAM, not on disk. Disappears when the container stops.
  • Useful for scratch space or temp files that shouldn't survive a restart.

Backups

Named volume backup:

bash
docker run --rm \
  -v pgdata:/source:ro \
  -v "$(pwd):/backup" \
  alpine tar czf /backup/pgdata-$(date +%Y%m%d).tar.gz -C /source .

That spins up a throwaway alpine container with the volume mounted at /source and the host's current directory at /backup, then tars the volume contents to a host file.

Restore:

bash
docker run --rm \
  -v pgdata:/restore \
  -v "$(pwd):/backup" \
  alpine tar xzf /backup/pgdata-20260601.tar.gz -C /restore

For databases specifically, prefer the database's own dump tool (pg_dump, mysqldump, mongodump) — those produce portable, version-independent backups. Volume-level backups require stopping the container for consistency and are tied to the specific database version.

Where named volumes live on disk

  • Linux: /var/lib/docker/volumes/<name>/_data
  • macOS: inside Docker Desktop's virtual disk (~/Library/Containers/com.docker.docker/Data/...). Not directly accessible from macOS.
  • Windows: inside Docker Desktop's WSL2 distro's filesystem (\\wsl$\docker-desktop-data\...).

Full picture for macOS in Where Are Docker's Files on a Mac?.

Permission errors on bind mounts (and how to avoid them)

The container's UID writing to a bind mount creates files owned by that UID on the host. By default, that UID is root. On a Linux host, ls -la shows the file as owned by root; you need sudo rm to delete it.

Two fixes:

bash
# Run the container as your host user
docker run --rm -u "$(id -u):$(id -g)" -v "$(pwd):/app" -w /app node:22 npm test
bash
# After the fact (Linux)
sudo chown -R $(id -u):$(id -g) .

This is why named volumes are nicer for stateful services on Mac/Windows — they sidestep the UID question entirely.

Common pitfalls

  • Using an anonymous volume by accident. -v /var/lib/postgresql/data (no name on the left of the colon) creates an anonymous volume. It's persistent, but you can't easily reference it later. Use a named volume.
  • docker compose down -v wiping data. The -v flag removes named volumes. Don't run it casually on stacks with state you care about.
  • Bind mount over a directory that contains useful image content. Mounting ./empty-dir:/app/wp-content on a WordPress container overlays the image's default wp-content, hiding the bundled themes. Named volumes copy the image content into the volume on first init; bind mounts do not.
  • Permission flips on Mac. Docker Desktop's file sharing handles the UID translation transparently but occasionally produces odd ownership. Restart Docker Desktop, or move to a named volume.
  • Trusting a volume to survive everything. docker volume rm and docker volume prune both remove volumes. The volume is durable across container removal, not across explicit volume operations.

What to do next

FAQ

Sources

Authoritative references this article was fact-checked against.

TagsDockerVolumesBind MountsPersistenceStorageDevOps

Found this useful? Pass it on.

Copied
Ishan Karunaratne

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts

d

docker logs: View Container Output and Tail Logs

Read the stdout and stderr of a running or stopped container. Follow live output, tail the last N lines, filter by time, prepend timestamps, and the cases where docker logs doesn't help because the app writes to a file instead.

D

Docker Restart Policies and Health Checks

Make containers come back automatically after crashes and reboots, and tell Compose how to wait until a service is actually ready (not just started). Restart policies, HEALTHCHECK, and depends_on: condition: service_healthy.