Docker persistent storage keeps your data after a container is deleted. Anything a container writes outside a volume or bind mount lives in the container layer and disappears with docker rm, and when the process inside the container runs as a non-root user it usually fails with "permission denied" on a freshly created volume. This guide sets up both storage types and fixes the ownership so the container can actually write.
Before you start
You need:
- A Linux VPS running Ubuntu 22.04 or 24.04, Debian 12, or AlmaLinux/Rocky 8–9.
- Docker Engine installed, with a normal login user such as
deploywho can usesudo. - Shell access over SSH. Check your server IP, username, and root password in your VPS provider's control panel if you are not sure.
- A rough idea of which path inside the container holds the data you care about, for example
/var/lib/postgresql/dataor/app/uploads. Check the image documentation if you do not know.
Warning: Adding a user to the docker group gives that user root-equivalent access to the host. Only add accounts you trust. See Docker security checklist for a public VPS before you widen access.
Docker persistent storage: volumes or bind mounts
There are two ways to keep data, and the ownership rules differ.
- Named volume — Docker creates the directory under
/var/lib/docker/volumes/<name>/_dataand owns it asroot. It survivesdocker rmanddocker compose downunless you pass-v. Good for databases and app state. - Bind mount — you point at an existing host directory such as
/srv/app/data. You control the ownership with normal Linux tools, which makes it easier to share with a non-root container user.
If you are still deciding, Docker volumes vs bind mounts: store container data safely covers the trade-offs in more detail.
Give the login user access to Docker
On a fresh VPS the docker group usually exists. Confirm it and add your user:
getent group docker
sudo usermod -aG docker deployLog out and back in, then check that the group is active:
id deploy
docker psIf docker ps still says permission denied, see Fix "permission denied" on the Docker socket.
Tip: Groups are applied at login. newgrp docker opens a shell with the new group immediately, but a full re-login is cleaner.
Create a named volume and give the container user ownership
Create the volume first, then look at where Docker put it:
docker volume create appdata
docker volume inspect appdataThe Mountpoint field shows something like /var/lib/docker/volumes/appdata/_data. That directory is owned by root:root, so a container running as UID 1000 cannot write to it.
Find out which UID the image uses:
docker run --rm nginx:alpine idRun the container as a fixed UID and GID so the behaviour is predictable:
docker run -d --name web \
--user 1000:1000 \
-v appdata:/data \
nginx:alpineNow hand ownership of the volume to that UID using a short-lived helper container:
docker run --rm -v appdata:/data alpine chown -R 1000:1000 /dataThe helper runs as root by default, which is why it can change ownership. Do not add --user to that command.
Use a bind mount instead
A bind mount puts you in charge of the host directory:
sudo mkdir -p /srv/app/data
sudo chown -R 1000:1000 /srv/app/data
sudo chmod 750 /srv/app/dataThen mount it read-write:
docker run -d --name app \
--user 1000:1000 \
-v /srv/app/data:/data \
nginx:alpineThe container user and the host directory owner now match, so writes succeed.
Warning: Never run chown -R across all of /var/lib/docker. Changing ownership of unrelated volume or overlay directories breaks running containers and can stop the daemon from starting. Target one volume path at a time.
On AlmaLinux and Rocky with SELinux enforcing, add the :z suffix so the directory gets the right label:
docker run -d --name app \
-v /srv/app/data:/data:z \
nginx:alpineFix ownership on an existing volume
If data already exists and the container suddenly cannot write, check the ownership first:
docker run --rm -v appdata:/data alpine ls -ln /dataThen repair it for the UID the container uses:
docker run --rm -v appdata:/data alpine chown -R 1000:1000 /dataIf two services must share one folder with different UIDs, put them in a shared group and use ACLs rather than opening permissions to 777:
sudo setfacl -R -m g:developers:rwx /srv/app/data
sudo setfacl -R -d -m g:developers:rwx /srv/app/dataThe pattern is the same as Create a shared folder several users can edit.
Check it works
Write a file inside the container, then confirm it is on the host:
docker exec web sh -c 'echo hello > /data/test.txt'
docker exec web cat /data/test.txtNow destroy the container and mount the same volume in a new one:
docker rm -f web
docker run --rm --user 1000:1000 -v appdata:/data alpine cat /data/test.txtIf you see hello, the data outlived the container and the non-root user can read it. Repeat the same test for your bind mount path.
Troubleshooting
mkdir: cannot create directory '/data/x': Permission denied— the container user's UID does not match the volume owner. Runidinside the container, thenchownthe volume to that UID with a root helper container.- The volume looks empty inside the container — a bind mount covers whatever was in the image at that path, and the host directory takes priority. Either copy the image's default files out first or start from an empty directory.
- Data vanished after
docker rm— it was written to the container layer, not to a volume. Add-v name:/pathor a bind mount and recreate the container.docker volume lsshows what actually exists. permission deniedwhen running any docker command — the login user is not in thedockergroup or has not logged in since being added. Re-check withid, then re-login.- Works on Debian but not Rocky — SELinux is blocking the bind mount. Add
:zto the mount and checksudo ausearch -m avc -ts recentfor denials.
Next steps
- Plan storage sizing and disk layout with Move Docker data root to another disk or path.
- Keep containers running across reboots with Docker restart policies: keep containers running after a reboot.
- Build your stack reproducibly with Install Docker and Docker Compose on a Linux VPS.
