Side reading

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:

  • r on a directory: you can list what is in it.
  • x on a directory: you can traverse into it and reach things by name.
  • r without x: you can see the names and open nothing.
  • x without r: 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 #

  1. ls -ln on the file and every directory above it.
  2. docker compose exec <service> id to see what the container really is.
  3. docker compose exec <service> ls -ln /the/path to see it from inside.
  4. Compare the numbers. They will not match, and now you know which way to fix it.

Settings

Your values

The book is written with placeholder names so it makes sense to everybody. Put your own in and every chapter, every command and every copy-paste prompt updates to match.

Nothing here is sent anywhere. It is saved in this browser, so it comes back next time. A different browser or a private window gets the placeholders again.

Live preview

$ ssh admin@192.168.1.20
$ sudo ufw allow from 192.168.1.0/24 to any port 22 proto tcp
$ sudo hostnamectl set-hostname homeserver
$ sudo timedatectl set-timezone Europe/Paris

Real commands from chapters 2, 3 and 4. They change as you type.

The account you log in as. Not root, and not necessarily the same name you use on your laptop.

Introduced in Chapter 2, Meet your server

The book's placeholder is admin

What the machine calls itself. You choose it, and it shows up in your shell prompt and your logs.

Introduced in Chapter 3, A safe front door

The book's placeholder is homeserver

The IP address your server has on your home network, from ip -brief addr.

Introduced in Chapter 2, Meet your server

The book's placeholder is 192.168.1.20

The address range and prefix shown by ip route or ip -brief addr, written in CIDR form. Copy the real prefix; do not guess /24.

Introduced in Chapter 2, Meet your server

The book's placeholder is 192.168.1.0/24

The address traffic goes to on its way out of your house, from ip route.

Introduced in Chapter 2, Meet your server

The book's placeholder is 192.168.1.1

In Region/City form, or Etc/UTC if you would rather read logs in UTC.

Introduced in Chapter 3, A safe front door

The book's placeholder is Europe/Paris

A registered name you control. Chapter 8 uses it for the LAN route; chapter 12 uses a separate private Tailscale name remotely.

Introduced in Chapter 8, One door, many rooms

The book's placeholder is example.com

The email identity allowed to administer the tagged server in your Tailscale policy.

Introduced in Chapter 10, Your own private network

The book's placeholder is you@example.com

The mailbox that should receive actionable home-server alerts.

Introduced in Chapter 14, Knowing it is alive

The book's placeholder is alerts@example.com

Once you save, the prose and the commands read with your names, the copy buttons copy your values, and the copy-paste prompts describe your machine accurately. That last one matters: an assistant told your network is 192.168.1.0/24 when it is not will send you chasing the wrong thing.

Anything you leave empty keeps the book's placeholder.