A container’s filesystem is temporary: remove the container and everything written inside it is gone. That’s by design — containers should be disposable. Data that must survive (databases, uploads, configuration) belongs outside the container, in a volume or a bind mount.
Three ways to store data
| Type | Where data lives | Managed by | Best for |
|---|---|---|---|
| Named volume | /var/lib/docker/volumes/<name>/_data |
Docker | Databases, app data |
| Bind mount | Any host path you choose | You | Config files, source code in development |
| tmpfs | RAM only | Kernel | Temporary, sensitive data |
Named volumes
docker volume create pgdata
docker run -d --name db \
-e POSTGRES_PASSWORD=secret \
-v pgdata:/var/lib/postgresql/data \
postgres:17
If the volume doesn’t exist, docker run -v pgdata:… creates it. Remove and recreate the container — even with a newer image — and the data is still there:
docker rm -f db
docker run -d --name db -e POSTGRES_PASSWORD=secret -v pgdata:/var/lib/postgresql/data postgres:17
A handy property: when an empty named volume is mounted over a directory that has content in the image, Docker copies that content into the volume first. Bind mounts don’t do this.
Inspect and manage
docker volume ls
docker volume inspect pgdata # shows Mountpoint
docker system df -v # size of each volume
docker volume rm pgdata # only if no container uses it
Bind mounts
Mount a host directory or file:
docker run -d -p 127.0.0.1:8080:80 \
-v /srv/site/html:/usr/share/nginx/html:ro \
-v /srv/site/nginx.conf:/etc/nginx/conf.d/default.conf:ro \
nginx:stable
:romakes it read-only inside the container — use it whenever the container shouldn’t write.- The host path must exist; otherwise Docker creates an empty directory (even when you meant a file), a classic source of confusion.
The longer --mount syntax is more explicit and errors out instead of creating missing paths:
--mount type=bind,source=/srv/site/html,target=/usr/share/nginx/html,readonly
In Docker Compose
services:
app:
image: ghcr.io/example/app:2
volumes:
- ./config:/app/config:ro # bind mount, relative to compose.yaml
- uploads:/app/uploads # named volume
db:
image: mariadb:11
volumes:
- dbdata:/var/lib/mysql
volumes:
uploads:
dbdata:
Compose prefixes named volumes with the project name (myapp_dbdata). docker compose down keeps them; docker compose down -v deletes them. See the Docker Compose tutorial.
Permissions: the most common problem
Processes inside containers run as some UID (often root, sometimes a dedicated user like 999 for databases or 1000 for Node images). With bind mounts, the numeric UID must have access to the host directory:
docker run --rm node:22-slim id # see which user the image runs as
sudo chown -R 1000:1000 /srv/app/data # match it on the host
Symptoms of a mismatch: EACCES: permission denied, mkdir: cannot create directory, or a database refusing to start. Named volumes avoid most of these problems because the image’s entrypoint can set ownership on first use.
On SELinux systems (AlmaLinux, Rocky), add :z (shared) or :Z (private) to bind mounts so Docker relabels them: -v /srv/data:/data:Z.
tmpfs mounts
docker run --tmpfs /app/cache:rw,size=64m myapp
Fast, never written to disk, gone when the container stops.
Back up a volume
Run a temporary container that mounts the volume and writes a tarball to the host:
docker run --rm -v pgdata:/data:ro -v "$PWD":/backup debian:stable \
tar czf /backup/pgdata-$(date +%F).tar.gz -C /data .
Restore into a (new) volume:
docker volume create pgdata
docker run --rm -v pgdata:/data -v "$PWD":/backup debian:stable \
tar xzf /backup/pgdata-2026-11-04.tar.gz -C /data
For databases, stop the container first or — better — use the database’s own dump tool (pg_dump, mysqldump) through docker exec. Copying the files of a running database can produce an inconsistent backup:
docker exec db pg_dump -U postgres appdb | gzip > appdb-$(date +%F).sql.gz
More in backing up Docker volumes.
Clean up unused volumes
docker volume ls -f dangling=true # volumes not used by any container
docker volume prune # remove anonymous unused volumes
docker volume prune -a # remove ALL unused volumes, named ones too
Careful: a stack that’s currently down has no containers, so its volumes count as unused. Double-check before pruning. See docker prune.
Where to put Docker data
Volumes live under Docker’s data root (/var/lib/docker by default). If your server has a separate data disk, move the data root there, or use bind mounts on that disk for large data sets. A Storage VPS with a large disk suits volume-heavy setups like Nextcloud or media servers.
Quick reference
docker volume create NAME
docker volume ls
docker volume inspect NAME
docker volume rm NAME
docker volume prune
docker run -v NAME:/path IMAGE # named volume
docker run -v /host/path:/path:ro IMAGE # bind mount, read-only
docker run --tmpfs /path IMAGE # tmpfs
Next: Docker networking and the Docker commands cheat sheet.