Skip to content
Docker
Lab 4 of 11·40mIntermediate

Persist data properly, and meet the UID problem

Compare a named volume against a bind mount, then fix the permission denied that every bind mount eventually gives you.

You need

  • Docker Engine 24+ on Linux (or a Linux VM)

Do first

Lab 1 showed the writable layer is disposable. There are two ways to keep data past it, they behave differently, and the difference bites on exactly one thing: who owns the files.

1. A named volume

mkdir -p ~/labs/docker-data && cd ~/labs/docker-data
docker volume create labvol
docker volume inspect labvol --format '{{.Mountpoint}}'
docker run --rm -v labvol:/data alpine:3.20 sh -c 'echo persisted > /data/note'
docker run --rm -v labvol:/data alpine:3.20 cat /data/note

Docker owns the storage, somewhere under /var/lib/docker/volumes. The container writes as root and the volume is owned by root, so nothing complains.

Verify

docker run --rm -v labvol:/data alpine:3.20 cat /data/note # persisted

2. A bind mount

mkdir -p ./site
echo "<h1>from the host</h1>" > ./site/index.html
docker run --rm -v "$PWD/site:/usr/share/nginx/html:ro" \
  -p 8080:80 -d --name labweb nginx:alpine
sleep 2
curl -s localhost:8080
docker rm -f labweb

A bind mount maps a host path in. Edits on the host appear instantly inside — which is why it is the right tool for development and the wrong one for production data. :ro makes it read-only, and a config file mounted read-only is a habit worth keeping.

Verify

docker run --rm -v "$PWD/site:/site:ro" alpine:3.20 \ sh -c 'echo x > /site/x 2>&1 || echo "read-only enforced"' # read-only enforced

3. Now break it

mkdir -p ./appdata
docker run --rm -u 1000:1000 -v "$PWD/appdata:/data" alpine:3.20 \
  sh -c 'echo hello > /data/f; echo "exit=$?"'
sudo chown 9999:9999 ./appdata
docker run --rm -u 1000:1000 -v "$PWD/appdata:/data" alpine:3.20 \
  sh -c 'echo hello > /data/f2 2>&1; echo "exit=$?"'

The second fails. A bind mount carries the host's ownership straight through, and the container's user is just a number — uid 1000 inside is uid 1000 outside. If the host directory is owned by 9999, uid 1000 cannot write to it, and no chmod inside the container will change that.

This is the single most common container permission bug, and it only happens with bind mounts. A named volume avoids it because Docker creates the directory with the ownership the image expects.

Verify

ls -ld ./appdata | awk '{print $3}' # 9999 — the host ownership the container has to match

4. Fix it three ways

sudo chown 1000:1000 ./appdata
docker run --rm -u 1000:1000 -v "$PWD/appdata:/data" alpine:3.20 \
  sh -c 'echo a > /data/fix1; echo "match host ownership: exit=$?"'

sudo chown 9999:9999 ./appdata
docker run --rm -u 9999:9999 -v "$PWD/appdata:/data" alpine:3.20 \
  sh -c 'echo b > /data/fix2; echo "match container user: exit=$?"'

sudo chmod 777 ./appdata
docker run --rm -u 1000:1000 -v "$PWD/appdata:/data" alpine:3.20 \
  sh -c 'echo c > /data/fix3; echo "world-writable: exit=$?"'

All three work and only two are acceptable. Align the host ownership to the container's user, or run as the uid that already owns the directory. chmod 777 also "works" and gives every process on the host write access to your application's data.

Verify

ls -1 ./appdata | wc -l # 3 or more files written by the fixes above

5. See which one survives what

docker run -d --name labdb -v labvol:/data alpine:3.20 sleep 300
docker rm -f labdb
docker volume ls | grep labvol
docker run --rm -v labvol:/data alpine:3.20 cat /data/note

The container is gone; the volume and its contents are not. Volumes outlive containers by design, which is also how a disk quietly fills:

docker volume ls -q | wc -l
docker system df -v | head -n 5

docker volume prune removes volumes no container references. Read what it proposes first — an anonymous volume holding a database that is merely stopped looks identical to garbage.

Verify

docker volume inspect labvol --format '{{.Name}}' # labvol — still there after the container was removed

The rule

Use a named volume for data the application owns: databases, uploads, state. Use a bind mount for things the host owns: source code in development, a config file, a certificate. If you are hitting permission errors, you are almost certainly using a bind mount where a volume belongs.

Clean up

docker volume rm labvol
cd ~ && sudo rm -rf ~/labs/docker-data

Where this goes next

Your data survives. Next: making the build that produces the image fast enough to iterate on.