Backend / Docker / 09_volume_types.md

Volumes, bind mounts and tmpfs

Updated 5 interview angles 4 min read source
On this page7
  1. Named volumes are the production default
  2. Bind mounts: development, and the sharp edges
  3. tmpfs: never touches disk
  4. Mount order and shadowing
  5. The --mount syntax
  6. Related
  7. Interview angle

Volumes, bind mounts and tmpfs

A container’s filesystem dies with the container. Three mechanisms outlive it, and picking the wrong one is a portability or a data-loss problem.

Type Lives Use for
Named volume Docker-managed area persistent data
Bind mount a host path local development
tmpfs RAM only secrets, scratch
bash
docker run -v pgdata:/var/lib/postgresql/data postgres
docker run -v "$PWD/src:/app/src" myapp
docker run --tmpfs /tmp myapp

Named volumes are the production default

Docker creates and owns the storage, so nothing in the command depends on the host’s directory layout. That is what makes the same Compose file work on a laptop, in CI and on a server.

They also work with volume drivers — the same declaration can be backed by NFS, EBS or a cloud file share, which is how a volume outlives a host and not just a container.

yaml
services:
  db:
    image: postgres:18
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:

They survive, deliberately

Removing a container leaves its volumes behind. That is the whole point, and also why unused volumes accumulate:

bash
docker compose down          # volumes survive
docker compose down -v       # volumes deleted
docker volume prune          # every unused volume

Gotcha: docker volume prune deletes anything not currently attached to a container, which includes the database volume of a stack you have merely stopped. It is the one Docker command worth reading twice before running.

Bind mounts: development, and the sharp edges

A bind mount maps a host path straight into the container, so an edit on the host is visible immediately. That live-reload loop is what they are for.

Three things go wrong with them in production:

  1. Host coupling. The path must exist, with the right contents, on every machine that runs it.
  2. Permissions. The container’s UID must be able to read and write the host directory. On Linux a root-owned bind mount and a non-root container is the most common “permission denied” in Docker.
  3. Performance. On macOS and Windows the mount crosses a VM boundary; a large node_modules over a bind mount is dramatically slower than native.

The anonymous-volume trick

yaml
volumes:
  - ./:/app          # source from the host
  - /app/node_modules  # keep the image's copy

The second line masks the first for that subdirectory, so container-installed dependencies are not shadowed by the host’s. It looks like a typo and is deliberate — worth recognising.

tmpfs: never touches disk

bash
docker run --tmpfs /tmp:size=64m,mode=1777 myapp

Backed by RAM, gone when the container stops, and never written to the host’s disk. That last property is the reason to use it: decrypted secrets, session scratch, anything that must not survive in a filesystem someone can later image.

It counts against the container’s memory, so size it. Linux only.

Mount order and shadowing

Mounting over a path hides whatever the image had there:

bash
# The image's /app/config is now invisible
docker run -v conf:/app/config myapp

If the volume is empty and you mounted over image content, the container sees an empty directory. Named volumes are the exception: Docker copies the image’s content into a new, empty named volume on first use. Bind mounts and tmpfs never do — they simply shadow.

That asymmetry explains the classic “my config disappeared when I added a bind mount” and is worth being able to state precisely.

The --mount syntax

bash
docker run --mount type=volume,src=db,dst=/data pg

docker run --mount type=bind,src=/srv,dst=/app,ro app

More verbose than -v and explicit about the type, which matters because -v guesses: a source containing / is a bind mount, anything else is a named volume. A typo’d host path with -v silently creates a volume named after it rather than failing.

readonly is worth reaching for by default on anything the container should only read — config, certificates, source in a production image.

Interview angle 5

  • “Volume, bind mount, or tmpfs?” - named volumes are Docker-managed and the right default for persistent data; bind mounts map a host path and suit local development with live reload; tmpfs is memory-only for secrets or scratch that must not touch disk.
  • “Why prefer a named volume over a bind mount in production?” - it’s portable, managed by the runtime, avoids host path and permission coupling, and works with volume drivers for networked storage.
  • “What happens to a volume when the container is removed?” - it survives. That’s the point, and also why unused volumes accumulate; prune them deliberately, since a stray docker volume prune can delete data you wanted.
  • “What happens if you mount over a directory that has content?” - it is shadowed. A new named volume is the exception: Docker seeds it from the image on first use. Bind mounts and tmpfs never seed, so mounting an empty host directory over /app/config gives you an empty config directory.
  • -v or --mount?” - --mount in anything scripted. -v infers the type from whether the source looks like a path, so a mistyped host path silently becomes a named volume instead of an error.