Side reading
Users, groups, and the file owned by nobody
Why a container writes files owned by a user that does not exist, and how to make it stop.
The most common confusing failure with containers and bind mounts. Nothing is broken; two systems are naming the same number differently.
Numbers, not names #
The kernel does not know about usernames. It knows uid 1000 and gid 1000. Names come from /etc/passwd and /etc/group, which are just files, and each container has its own.
So when a process inside a container writes a file as uid 999, the file on your host is owned by uid 999. If your host has no account with that number, ls -l shows the number. If it has a different account with that number, it shows that name, which is more confusing than the number.
$ ls -l # names, resolved through the host's own files
$ ls -ln # the numbers, which is what is really stored
When permissions are confusing, use ls -ln.
Reading the mode #
-rw-r----- 1 1000 1000 4096 Aug 29 12:00 file
^^^ ^^^ ^^^
| | others
| group
owner
r read, w write, x execute. On a directory they mean something slightly different, which trips people up:
ron a directory: you can list what is in it.xon a directory: you can traverse into it and reach things by name.rwithoutx: you can see the names and open nothing.xwithoutr: you can open files whose names you already know, but not list them.
That last combination is genuinely useful: chmod 0711 on a home directory lets a web server reach a specific path without being able to enumerate the rest.
Numerically, r=4, w=2, x=1. So 750 is rwxr-x---.
Which user is my container #
$ sudo docker inspect <container> --format '{{.Config.User}}'
$ sudo docker compose exec <service> id
Empty means root. id inside is authoritative.
The three ways to fix it #
Tell the container which id to be. Many images accept PUID and PGID, or compose's user::
user: "1000:1000"
Cleanest when it works. It breaks for images that need root to set themselves up at startup, and those will fail in ways that do not mention permissions.
Make the host directory match the container. Find out what uid the image uses, then chown to it. Works always, at the cost of a directory on your host owned by a number you did not choose.
Use a shared group. Choose an unused numeric GID, create the same group on the host, add the administrator to it, and pass that GID to each service as a supplementary group with Compose group_add:
$ getent group <chosen-gid>
$ sudo groupadd --gid <chosen-gid> media
$ sudo usermod --append --groups media admin
$ sudo chgrp -R media /srv/homeserver/data/media
$ sudo find /srv/homeserver/data/media -type d -exec chmod g+rwx,g+s {} +
$ sudo find /srv/homeserver/data/media -type f -exec chmod g+rw {} +
services:
example:
group_add: ["<chosen-gid>"]
Log in again before expecting the administrator's new supplementary group to appear. This is appropriate only when two processes genuinely need to share the directory; it is not a reason to give every container access to all household data.
setgid on directories #
g+s on a directory means new files inside inherit the directory's group rather than the creating user's. Without it, two applications writing to a shared directory create files the other cannot read, and you fix it by hand for ever.
umask #
The mask of permissions removed from newly created files. 022 means new files are 644 and directories 755. 077 means owner only.
Scripts handling secrets should set umask 077 at the top, which is why chapter 9's rendering script does.
Special cases worth recognising #
nobody or nogroup, often 65534. Some images deliberately run as this. Files it writes cannot be written by anything else without a chown.
Files owned by a very high number, like 100999. User namespace remapping is in play: the container's uid 999 is being mapped to a different range on the host. Good for isolation, confusing the first time.
Permission denied where the file looks readable. Check every directory in the path for x. Traversal needs it on all of them, and one restrictive parent blocks everything below it.
Operation not permitted rather than Permission denied. Usually not a permissions problem: a dropped capability, a read-only mount, or a security profile.
Debugging order #
ls -lnon the file and every directory above it.docker compose exec <service> idto see what the container really is.docker compose exec <service> ls -ln /the/pathto see it from inside.- Compare the numbers. They will not match, and now you know which way to fix it.