CoursesDocker in depthStorage & volumes

Volumes, bind mounts & tmpfs

The three ways to attach data.

Intermediate12 min · lesson 9 of 30

A container’s writable layer is ephemeral, so anything you want to keep lives on a mount. Docker offers three kinds and choosing correctly is most of the skill. A volume is storage Docker creates and manages in its own area (/var/lib/docker/volumes). A bind mount maps a specific host directory into the container. A tmpfs mount is memory-backed and never touches disk. Same idea — attach storage at a path — three very different lifecycles and use cases.

terminal
$ docker volume create pgdata # a managed volume
$ docker run -d --mount type=volume,source=pgdata,target=/var/lib/postgresql/data postgres:16
$ docker run -d --mount type=bind,source="$(pwd)"/src,target=/app/src myapp:1.0
$ docker run -d --mount type=tmpfs,target=/run,tmpfs-size=64m myapp:1.0

--mount versus -v

The old -v shorthand packs source, target, and options into one colon-separated string — terse, but ambiguous, and it silently creates a host directory when a bind source does not exist. The newer --mount spells out type, source, and target as explicit key/value pairs: more to type, self-documenting, and it errors instead of guessing. Prefer --mount in anything you will read again.

terminal
# these do the same thing; the second says what it means and fails loudly on typos
$ docker run -v pgdata:/var/lib/postgresql/data postgres:16
$ docker run --mount type=volume,source=pgdata,target=/var/lib/postgresql/data postgres:16

Scenario: a database that must survive redeploys

A Postgres container is recreated on every deploy, and its data must persist across all of them. A named volume is the answer: docker rm removes the container and its writable layer but leaves the volume untouched, so the new container remounts the same data. Volumes are also how you take a consistent backup — mount the volume plus a host directory into a throwaway container and tar it out.

terminal
# back up a named volume to a tarball on the host
$ docker run --rm \
-v pgdata:/data:ro \
-v "$(pwd)":/backup \
alpine tar czf /backup/pgdata-$(date +%F).tgz -C /data .
# restore into a fresh volume by reversing the mounts and untarring

Scenario: live-editing code in development

You want edits on your laptop to appear instantly in the container without rebuilding. A bind mount of your source directory does exactly that. The classic gotcha in Node projects: bind-mounting the whole project shadows the image’s node_modules with your host’s (often empty or wrong-arch). The fix is an anonymous volume on node_modules, which “masks” the bind mount for that subpath and keeps the image’s installed dependencies.

terminal
$ docker run -d \
--mount type=bind,source="$(pwd)",target=/app \
--mount type=volume,target=/app/node_modules \ # keep the image’s deps
myapp:dev
# host source is live-mounted; node_modules stays the container’s own

Scenario: secrets that must never hit disk

A process needs a credential file at runtime but you do not want it written to the node’s disk where it could linger or be backed up. A tmpfs mount is memory-backed: the path exists inside the container, lives in RAM, and vanishes the instant the container stops — nothing to recover from disk afterward. It is also useful for scratch and cache directories that should not bloat the writable layer.

terminal
$ docker run -d --mount type=tmpfs,target=/secrets,tmpfs-size=8m,tmpfs-mode=0700 myapp:1.0
$ docker exec <id> sh -c 'mount | grep /secrets'
tmpfs on /secrets type tmpfs (rw,size=8192k,mode=700) # RAM-backed, gone on stop
A misspelled bind source becomes an empty directory
With -v, a bind source path that does not exist is created as an empty directory and mounted — so a typo silently hands your container an empty folder instead of your data, and the app looks broken for no clear reason. --mount errors on a missing bind source instead, which is exactly why it is the safer default for anything important.