What is it? #
Containers are disposable, but some data must survive them: database files, user uploads, generated content.
Volumes provide that persistence. Data in a volume lives outside the container's writable layer and remains when the container is removed.
There are two kinds worth knowing. Named volumes are managed by Docker and are the right choice for data. Bind mounts map a host directory into the container and are the right choice for source code during development.
The common trap is permissions. The user inside the container must be able to write to the mounted location, and the numeric user ID is what actually matters.
Think of it like this #
An external drive rather than the device's own storage.
Replace the device and plug the drive into the new one, and everything is still there. Keep the files on the device itself and they go into the bin with it.
Simple example #
A PostgreSQL container with a named volume for its data. The container can be destroyed, upgraded and recreated, and the database survives intact.
Code #
# Named volume: Docker manages the storage location
docker volume create shop-data
docker run -d --name db \
-v shop-data:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=secret \
postgres:16
# Prove it survives
docker exec db psql -U postgres -c "CREATE TABLE test (id int);"
docker rm -f db
docker run -d --name db -v shop-data:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=secret postgres:16
docker exec db psql -U postgres -c "\dt" # the table is still there
# Bind mount: a host directory, for development
docker run -d --name app \
-v "$(pwd)":/app \
-v /app/node_modules \ # keep the container's node_modules, not the host's
-p 3000:3000 \
myapp:dev
# Editing files on the host is immediately visible inside the container.
# Read-only, for configuration
docker run -v /etc/myapp/config.yml:/app/config.yml:ro myapp
Named volume or bind mount?
named volume data you care about: databases, uploads
Docker manages the location, works on every platform,
can be backed up and moved with docker commands
bind mount source code in development, config files, host logs
an exact host path, immediate two-way visibility,
permissions and paths differ between platforms
tmpfs mount in-memory only, for secrets or scratch data that must
never touch disk
# Managing volumes
docker volume ls
docker volume inspect shop-data # where it lives on the host
docker volume rm shop-data # deletes the data
docker volume prune # removes ALL unused volumes — check first
# Backing up a named volume
docker run --rm \
-v shop-data:/data:ro \
-v "$(pwd)":/backup \
alpine tar czf /backup/shop-data-$(date +%F).tar.gz -C /data .
# Restoring
docker run --rm -v shop-data:/data -v "$(pwd)":/backup \
alpine sh -c "cd /data && tar xzf /backup/shop-data-2026-09-22.tar.gz"
The permissions problem
A container running as UID 1000 writing to a bind-mounted host directory
owned by UID 1001 gets permission denied. Names are irrelevant; the
numeric IDs must match.
Fixes: run the container with --user "<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo stretchy="false">(</mo><mi>i</mi><mi>d</mi><mo>−</mo><mi>u</mi><mo stretchy="false">)</mo><mo>:</mo></mrow><annotation encoding="application/x-tex">(id -u):</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="katex-base"><span class="katex-strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mopen">(</span><span class="mord mathnormal">i</span><span class="mord mathnormal">d</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="katex-base"><span class="katex-strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal">u</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">:</span></span></span></span>(id -g)", or chown the
host directory to the container's UID, or use a named volume, which
Docker initialises with the right ownership.
How it works #
A named volume is stored in Docker's own area on the host and mounted into the container at the given path. Docker creates it on first use and initialises its contents from the image if the target directory already had files.
That initialisation only happens for named volumes and only when the volume is empty, which is why a database container populates a fresh volume correctly on first start.
Bind mounts map an exact host path. There is no initialisation and no ownership adjustment, which is why permission problems concentrate here.
The anonymous volume trick in the Node example — mounting /app/node_modules without a source — shadows the host directory at that path, so the container keeps the modules it installed rather than seeing the host's, which may have been built for a different platform.
Read-only mounts with :ro are worth using for configuration. If the container has no reason to write, removing the ability is free.
Backing up a named volume means running a throwaway container that mounts both the volume and a host directory, then archiving one into the other. Docker has no built-in backup command.
docker volume prune deletes every volume not currently attached to a container. Running it carelessly is a genuine way to lose a database.
Real-world use #
Databases in containers always use named volumes. Without one, the first container replacement — including a routine image update — destroys the data.
Development setups use bind mounts for source code so edits appear immediately, usually combined with a file watcher inside the container.
On macOS and Windows, bind mounts cross a virtualisation boundary and are noticeably slower for large directories. Named volumes avoid that, which is why dependencies are often kept in one.
In production, the common pattern is to keep stateful services outside containers — a managed database — while the application containers stay fully disposable.
Volume backups are easy to forget because Docker does not do it for you. Anything in a volume needs the same backup treatment as anything on a server.
Common mistakes #
- Running a database container with no volume and losing the data on replacement.
- Running
docker volume prunewithout checking what it will delete. - Permission errors from mismatched numeric user IDs on bind mounts.
- Using bind mounts for production data instead of named volumes.
- Forgetting that volumes need their own backups.
Practice #
Run a database container with a named volume, create data, destroy and recreate the container, and confirm the data survived. Then back the volume up to a tar file, delete the volume, recreate it from the backup, and verify the data is intact.