Docker Volumes and Bind Mounts: Persisting Data
Table of contents
- Key takeaways
- Why a container's data doesn't persist
- Named volumes versus bind mounts
- How to create and mount a volume
- Backing up and restoring volumes
- Permissions and the user (PUID/PGID)
- tmpfs and ephemeral data
- Frequently asked questions
- What is the difference between a volume and a bind mount?
- Where does Docker store named volumes?
- Does docker compose down delete the data?
- Conclusion
- Sources
A container loses its data the moment you delete it, unless you store that data outside. Docker gives you two ways: named volumes, which it manages itself under /var/lib/docker/volumes, and bind mounts, which link a host folder. Here you will see when to use each, how to mount them in Compose and how to back them up.
When you delete a container, its data goes with it. That is the detail that bites everyone the first time: you update the image, recreate the container and suddenly the database is empty. The fix is to move the data out of the container, and Docker gives you two tools for it: named volumes and bind mounts. Here you will see how they differ, how to mount them in Docker Compose, how to handle permissions and how to make a backup you can actually restore. The same guide is available in Spanish.
Key takeaways
- A container is ephemeral: its writable layer disappears when you delete it. To keep data, you mount it outside, in a volume or a bind mount.
- Docker volumes are managed by the engine itself and live under
/var/lib/docker/volumes/<name>/_data. They are the recommended option for application and database data. - A bind mount links a specific host folder into the container. It fits configuration, code in development and backups.
- In Docker Compose you declare named volumes in the top-level
volumes:block; bind mounts use a relative path such as./data:/path. - Permissions are the number-one cause of errors: many images use the
PUIDandPGIDvariables (default1000) to write as your user.
Why a container’s data doesn’t persist
A container starts from a read-only image and adds a thin writable layer on top. Everything the process creates (temporary files, the database, uploaded files) lives in that layer. The moment you run docker rm or recreate the service with a new image, the layer is discarded and everything inside goes with it. This has been the default since the earliest Docker versions, and it is not a bug: it keeps containers light and reproducible.
The problem shows up when the process saves something you want to keep. That is where persistent storage comes in. The official documentation puts it plainly: «Volumes are the preferred mechanism for persisting data generated by and used by Docker containers». If you are coming from installing the engine, this step fits right after installing Docker on Ubuntu 24.04 and understanding Docker networking.
Named volumes versus bind mounts
Both mount data into the container, but they behave differently.
A named volume is a space that Docker creates and manages for you. As the documentation says, «Volumes are managed by Docker and are isolated from the core functionality of the host machine». You do not worry about where the files end up: Docker places them under /var/lib/docker/volumes/ and identifies them by a name. They are portable, easy to back up and do not depend on the host’s folder layout.
A bind mount is the opposite: you choose the host folder and Docker links it into the container. The documentation defines it like this: «When you use a bind mount, a file or directory on the host machine is mounted from the host into a container». You get full control over the path, which is ideal for injecting a configuration file or editing code and seeing it live. The cost is coupling: if that folder is missing or its permissions change, the container notices. A bind mount also has write access to the host by default, so add the :ro option (read-only) when the container should not modify anything.
The rule of thumb is simple. For application or database data, use a named volume. For configuration, code in development or exposing a backup folder, use a bind mount.
How to create and mount a volume
You can create a volume by hand and examine it:
docker volume create app_data
docker volume inspect app_data
docker volume ls
The inspect command shows the Mountpoint, which will point to /var/lib/docker/volumes/app_data/_data. In practice you almost never create it by hand: you declare it in docker-compose.yml and Docker generates it when the service comes up. Here is a full example with PostgreSQL 17.5 that combines a named volume for the data and a bind mount for backups:
services:
db:
image: postgres:17.5
restart: unless-stopped
environment:
POSTGRES_PASSWORD: change_this_password
POSTGRES_DB: app
volumes:
- db_data:/var/lib/postgresql/data
- ./backups:/backups:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d app"]
interval: 10s
timeout: 5s
retries: 5
volumes:
db_data:
Notice two things. First, db_data:/var/lib/postgresql/data is the named volume: Docker manages it and it survives docker compose down. Second, ./backups:/backups:ro is the bind mount, a real folder in your project mounted read-only. The top-level volumes: block is what turns db_data into a managed volume rather than a plain bind mount. On why we add restart and healthcheck, we follow the Compose v2 convention (no version: key) and pin a concrete tag instead of :latest, which would point to a different version every time you pull it.
To start and check the status:
docker compose up -d
docker compose ps
docker volume ls
Backing up and restoring volumes
A named volume is not a file you can copy directly, because it lives inside /var/lib/docker. The usual pattern is to launch an ephemeral Alpine container that mounts the volume and packs it into a .tar.gz on a host folder:
docker run --rm \
-v myproject_db_data:/data:ro \
-v "$(pwd)":/backup \
alpine tar czf /backup/db_data.tar.gz -C /data .
The container mounts the volume read-only, compresses its contents and leaves db_data.tar.gz in your current directory. When it finishes, --rm removes it. Restoring is the same move in reverse: mount the target volume with write permission and extract into it.
docker run --rm \
-v myproject_db_data:/data \
-v "$(pwd)":/backup \
alpine sh -c "cd /data && tar xzf /backup/db_data.tar.gz"
You can then upload that .tar.gz to an external destination. If you want to automate encrypted, scheduled backups to S3 or a remote folder, a service like Duplicati does exactly that on top of this same mechanism.
Permissions and the user (PUID/PGID)
The most common volume error has nothing to do with Docker and everything to do with Unix permissions. The process inside the container writes with a user id (UID) that may not match yours on the host, and then the dreaded «Permission denied» shows up on a bind mount.
Many popular images, especially those from LinuxServer.io, solve this with two environment variables: PUID and PGID. You tell the container which user and group to write as, so the files appear on the host owned by you. The default is usually 1000, the first user Linux creates:
services:
app:
image: lscr.io/linuxserver/bookstack:latest
environment:
PUID: 1000
PGID: 1000
volumes:
- ./config:/config
To find your real UID and GID, run id -u and id -g on the host. With a named volume this problem is less frequent, because Docker manages ownership, but with bind mounts it is worth keeping in mind.
tmpfs and ephemeral data
Not everything a container writes deserves to survive. A cache, a session file or sensitive data you do not want touching the disk fit better in a tmpfs mount, which lives only in the host’s RAM and vanishes when the container stops. In Compose you declare it like this:
services:
web:
image: nginx:1.29-alpine
tmpfs:
- /tmp
- /var/cache/nginx
Because tmpfs never writes to disk, it is faster and leaves less of a trace. The trade-off is obvious: if the container restarts, that content is gone. Use it for what is genuinely temporary, not for data you need tomorrow.
Frequently asked questions
What is the difference between a volume and a bind mount?
A named volume is created and managed by Docker under /var/lib/docker/volumes, and does not depend on the host’s folder layout. A bind mount links a specific folder you choose. Volumes are the recommended option for application data; bind mounts, for configuration and code in development.
Where does Docker store named volumes?
On Linux, under /var/lib/docker/volumes/<name>/_data. You can see the exact path with docker volume inspect <name>, in the Mountpoint field. You should not edit those files by hand: always interact through the container or a helper container.
Does docker compose down delete the data?
No, if it is in a named volume. docker compose down stops and removes the containers but respects the declared volumes. They are only deleted if you add the -v option (docker compose down -v), which does destroy the project’s volumes. That is why backups matter.
Conclusion
Persisting data in Docker comes down to one decision: a named volume for what the application needs to keep, a bind mount for what you control from the host, and tmpfs for what can evaporate. With engine version 29.6.1 (June 2026) the model is still the same as years ago, precisely because it works. The natural next step is to secure that data with automatic backups before putting any service into production.