What PUID and PGID do in Docker Compose
PUID and PGID are not Docker settings. They are an entrypoint convention in linuxserver.io images. Why bind mount files land as 911:911, and how to fix it.
What PUID and PGID actually are
PUID and PGID are two environment variables that certain container images read at startup. Docker itself never looks at them. They are a convention, used by linuxserver.io images and a handful of others, so an image that was not written to read them ignores them silently.
Inside a linuxserver.io image there is a user called abc, created at build time with UID (user ID) 911 and GID (group ID) 911. The container starts as root, runs its init scripts, and one of those scripts renumbers that user before anything else happens:
groupmod -o -g "${PGID}" abc
usermod -o -u "${PUID}" abcThe -o flag allows an ID that is already in use elsewhere. After that the init drops privileges and runs the application as abc. So PUID=1000 never reaches Docker. The variable renumbers a user inside the container before the application starts, which means every file that application writes lands on your disk owned by 1000. Leave PUID unset and abc keeps 911, which is why an unconfigured bind mount fills up with files owned by 911:911.
Get your two numbers with id
Run this on the host, as the user who owns the data directories:
iduid=1000(deploy) gid=1000(deploy) groups=1000(deploy),27(sudo),988(docker)uid is your PUID and gid is your PGID. For a script, id -u and id -g print the bare numbers. On most fresh VPS images the first human account is 1000:1000, but do not assume it. A rebuilt server, or a second account added later, gives 1001 or higher, and a wrong number here is the entire bug. If your services run under a dedicated service account instead of your own login user, run id thatuser and take the numbers from there.
Why your files show up as 911:911
ls -l prints a numeric ID instead of a name when no host account matches that ID. Nothing on your server is UID 911, so there is no name to print. Use ls -ln to see numbers every time and remove the ambiguity:
ls -ln /srv/appdata/sonarrdrwxr-xr-x 2 911 911 4096 Aug 7 09:12 Backups
-rw-r--r-- 1 911 911 512 Aug 7 09:12 config.xmlThat output says the container ran with the built-in defaults. Confirm it from inside the container rather than guessing:
docker exec sonarr id abc
docker compose logs sonarr | head -n 25The linuxserver init prints its result in the startup log as two lines:
User UID: 911
User GID: 911If those lines read 911 after you set PUID=1000 in your Compose file, the variable never reached the container. The usual cause is that you edited docker-compose.yml and then ran docker compose restart, which reuses the existing container with its original environment. Environment changes need docker compose up -d, which recreates the container.
Why you cannot delete a file the container wrote
The kernel compares numbers, never names. Your shell runs as UID 1000. The file belongs to UID 911. The directory holding it is drwxr-xr-x and also belongs to 911, so group and other get read and execute but no write. Deleting a file requires write permission on its directory, not on the file, so you get this even when the file itself looks harmless:
rm: cannot remove '/srv/appdata/sonarr/config.xml': Permission deniedA writing container hits the same wall from the other side. If the host directory belongs to your user at mode 755 and the application runs as 911, its first write fails with Permission denied and the app reports it in its own words. In a .NET application such as Sonarr or Radarr that surfaces as UnauthorizedAccessException: Access to the path '/data/downloads' is denied. The permission string in front of the file tells you which of the three permission sets you are actually being judged by, and reading drwxr-xr-x correctly is what turns that error from mysterious into obvious.
This is a bind mount problem specifically. When Docker creates an empty named volume and mounts it over a path that exists in the image, it copies that path's contents into the volume, ownership and permission bits included, so the application finds a directory it already owns. A bind mount gets none of that treatment: Docker mounts your host directory exactly as it is. That difference is one of the practical reasons to know when a bind mount beats a named volume and when it does not.
Fixing a directory that is already wrong
Setting PUID and PGID changes what the application does from now on. It does not retroactively fix files already on disk. Stop the stack, correct the ownership yourself, then start it again:
docker compose down
sudo chown -R 1000:1000 /srv/appdata/sonarr
docker compose up -dUse sudo chown -R "$(id -u):$(id -g)" /srv/appdata/sonarr if you would rather not type the numbers. Do this with the container stopped, because a running application that is mid-write during a recursive chown can end up with a half-corrected directory tree and a confusing second round of errors.
What PUID and PGID do not fix
Here is the part that catches people who did everything right. The linuxserver init chowns exactly three paths on startup: /app, /config and /defaults. Your media mounts are not on that list. /data, /downloads and /tv are handed to the application untouched, so if the host side of those mounts has ownership the container user cannot write to, the container starts cleanly, prints the correct UID in its banner, and then fails on the first import.
That is the right behaviour. A recursive chown across a twelve terabyte media library at every container start would be a disaster. It does mean the media directories are your job, and they are the mounts where permissions actually go wrong.
Three ways to control the user, and when each one applies
PUID and PGID environment variables
This works only on images whose entrypoint reads them. It is popular because the container still starts as root, does its own setup, fixes /config, and only then drops privileges. Docker Mods and custom init scripts keep working. The cost is that you are trusting a convention rather than a platform feature, and the variable names are not standard across projects.
The user: key in Compose
This one is a real Docker feature and works on every image, because the container runtime applies it before the image's own code runs:
services:
sonarr:
image: lscr.io/linuxserver/sonarr:latest
user: "1000:1000"The process never runs as root, not even for a moment, which is a genuine security gain. It also breaks anything in the entrypoint that needed root. On linuxserver images the project supports this on a reasonable endeavours basis and only for images it has tested, and the caveats are specific: PUID and PGID stop having any effect, Docker Mods will not run, custom services will not run, and you become responsible for the permissions on every mounted volume. Their documented pattern pairs the flag with a writable /run:
user: 1000:1000
tmpfs:
- /run:uid=1000,gid=1000,exec
security_opt:
- no-new-privileges=trueOne cosmetic side effect surprises people. A numeric user: has no matching entry in the container's /etc/passwd, so tools inside report whoami: cannot find name for user ID 1000. The ID is valid and file access works normally. Only the name lookup fails.
Rootless Docker
Rootless Docker runs the daemon itself as your unprivileged user, so nothing on the box runs as real root. It changes the ownership arithmetic completely. Container UID 0 maps to the host UID of the user running rootless Docker, and container UID n for any n of 1 or more maps to subuid + (n - 1), where subuid is the base of the range allocated to you in /etc/subuid and /etc/subgid. Docker expects at least 65,536 subordinate IDs there.
Read that mapping again, because it inverts the usual advice. Under rootless Docker a container writing as root produces files owned by you. A container writing as UID 1000 produces files owned by a subordinate ID somewhere around 100999, which your shell cannot touch. So the PUID value that is correct on a rootful daemon is the wrong one here. The two mechanisms solve the same problem in different layers, and stacking them without checking is how people end up with a directory they need sudo to remove. If you go rootless, test the ownership of one written file on your own server before you migrate a library into it.
For most self-hosted stacks on a single VPS, PUID and PGID on a rootful daemon is the pragmatic choice, because it is what the images are built and documented for. Reach for user: when the image README says that image is tested for it, or when you are running an official upstream image that has no PUID support at all.
The media stack case: one group shared across containers
An arr media stack with Sonarr, Radarr and a download client is where this stops being theory. The download client writes a finished file into /data/downloads. Sonarr then hardlinks or moves that file into /data/media. For the hardlink to work the two containers need write access to the same tree, and if the download client runs as 1000 while Sonarr runs as 1001, one of them owns files the other can only read.
The fix is a shared group that every container in the stack uses as its PGID:
sudo groupadd -g 13000 media
sudo usermod -aG media deploy
sudo chown -R deploy:media /srv/media
sudo find /srv/media -type d -exec chmod 2775 {} +
sudo find /srv/media -type f -exec chmod 0664 {} +The leading 2 in 2775 is the setgid bit. On a directory it means every new file and subdirectory created inside inherits the group media instead of the creator's own primary group, so the arrangement survives new downloads without you re-running chown. Log out and back in, or run newgrp media, before you check your own access: a group added with usermod -aG does not appear in an already-open shell session.
Inside the container, groupmod -o -g 13000 abc renumbers the abc group to 13000, so abc writes with the same GID as your host media group. Every container in the stack keeps its own PUID and shares that one PGID.
Then set UMASK=002 on every linuxserver container in the stack. This is the step people miss. The default in these images is UMASK=022, which strips the group write bit from every new file, so files land as 0644 and the sharing you just configured does nothing. 002 produces 0664 files and 0775 directories, and the group can write:
services:
sonarr:
image: lscr.io/linuxserver/sonarr:latest
container_name: sonarr
environment:
- PUID=${PUID}
- PGID=${PGID}
- UMASK=002
- TZ=Etc/UTC
volumes:
- /srv/appdata/sonarr:/config
- /srv/media:/data
restart: unless-stoppedThose two values belong in a .env file next to the Compose file, so the whole stack reads one definition:
PUID=1000
PGID=13000Compose reads that file automatically for ${PUID} style substitution, which is the same mechanism you use for credentials. The habits around keeping values out of docker-compose.yml and into a .env file apply here too, with the difference that these two numbers are not secret.
Verify it end to end instead of trusting the config. Write a file from inside one container and read it from the host:
docker exec sonarr touch /data/downloads/permtest
ls -ln /srv/media/downloads/permtestA healthy result shows your PUID as the owner, 13000 as the group, and -rw-rw-r-- as the mode. If the group reads 1000 the setgid bit is missing from that directory. If the mode reads -rw-r--r-- the UMASK variable did not take effect, so check that you recreated the container rather than restarting it. Remove the test file with rm /srv/media/downloads/permtest when you are done.
Which images use which variable
linuxserver.io images use PUID, PGID and UMASK. Paperless-ngx uses different names for the same idea: USERMAP_UID and USERMAP_GID, both defaulting to 1000, and its documentation tells you to read them from id -u and id -g. Many official upstream images, including the common database and web server images, ship a fixed built-in user and expect you to use user: or leave it alone.
So check the README of each image before you copy an environment block between projects. Docker passes any environment variable you set into any container, whether or not something inside reads it, and a PUID that nothing consumes produces no error, no warning and no effect. The container runs as whatever user its own Dockerfile ended with, and you find out through the ownership of the files it writes.
FAQ
Why are my Docker files owned by 911:911?
911 is the UID and GID of the abc user built into linuxserver.io images. Seeing it means the container started without PUID and PGID set, so its init script left the built-in defaults in place. ls -l shows the raw numbers because no account on your host has ID 911, so there is no name to display. Set PUID and PGID to the output of id, recreate the container with docker compose up -d, then fix the existing files with sudo chown -R 1000:1000 on the affected directory.
Do PUID and PGID work on every Docker image?
No. They are not a Docker feature and Docker never reads them. They work only on images whose own entrypoint reads them and calls usermod and groupmod before starting the application, which is the linuxserver.io family and a few projects that copied the pattern. Other projects use other names, such as USERMAP_UID and USERMAP_GID in paperless-ngx. On an image that reads neither, the variables are accepted and ignored with no warning.
Should I use PUID and PGID or the user: key in Docker Compose?
Use PUID and PGID when the image supports them, because the entrypoint still runs as root long enough to fix /config and start its own services correctly. Use user: when the image has no PUID support, or when the image README states it is tested for non-root operation. On a linuxserver image, setting user: makes PUID and PGID inert, stops Docker Mods and custom services from running, and makes every mounted volume's permissions your responsibility.
Sonarr has the right PUID but still cannot move files. What is wrong?
Check three things in order. First, the media mount itself: the init only chowns /app, /config and /defaults, so /data or /downloads keeps whatever ownership it has on the host. Second, the shared group: if the download client and Sonarr run under different GIDs, neither can modify the other's files, so give every container in the stack the same PGID. Third, the umask: the image default UMASK=022 writes files as 0644 with no group write bit, which defeats a shared group entirely. Set UMASK=002 and set the setgid bit on the directories with chmod 2775 so new files inherit the group.