← Back to Wiki
Docker / Proxmox / Storage

PUID and PGID Ignored: Docker UID Mapping in an Unprivileged LXC

Radarr sat on three finished downloads for two weeks with importPending. Unpackerr reported Extraction Failed — at 100% extracted. Nothing was broken in either app. The cause was that PUID and PGID are not a Docker feature, and the container had been running as a user nobody intended, on the far side of a uid offset that only exists because the container is unprivileged.

Share on X

The symptom

The stack: Radarr, Sonarr, Lidarr, Prowlarr and qBittorrent as LinuxServer.io Docker images, plus Unpackerr (golift/unpackerr) to unpack RAR'd releases, all inside an unprivileged Proxmox VE LXC container, writing to a Synology NAS over NFS.

Radarr's queue:

Young.Guns.1988.COMPLETE.UHD.BLURAY-B0MBARDiERS | importPending
  statusMessages: ["Found archive file, might need to be extracted"]

Unpackerr, on the same release:

[Radarr] Status: Young.Guns.1988.COMPLETE.UHD.BLURAY-B0MBARDiERS
  (Extraction Failed, elapsed: 4h25m7s) on archive: 1/1 @ 90.1GB/90.1GB (100%)

"Extraction Failed" at "100%" is the tell. Extraction had worked perfectly. Something after extraction failed, and the default INFO log level doesn't show it. You have to go looking:

docker logs unpackerr 2>&1 | grep -i error
[ERROR] Extraction Failed: Young.Guns...-B0MBARDiERS: extraction failed:
  os.Rename(): rename /downloads/movies/Young.Guns...-B0MBARDiERS_unpackerred/YOUNG_GUNS_UHD.iso
            -> /downloads/movies/Young.Guns...-B0MBARDiERS/YOUNG_GUNS_UHD.iso: permission denied

Unpackerr extracts into a temporary <release>_unpackerred/ directory it creates itself, then renames the result back into the release folder. It could create its own directory. It could not write into the one qBittorrent made.

The one command that shows the whole bug

On the NAS, list the two directories with numeric owners — -n matters, because names would be resolved through the NAS's own user database and hide the numbers that are the point:

ls -ln /volume1/downloads/movies/ | grep B0MBARDiERS
drwxr-xr-x 1 101000 101000  Young.Guns.1988...-B0MBARDiERS
drwxr-xr-x 1 100000 100000  Young.Guns.1988...-B0MBARDiERS_unpackerred

Two different owners, 1000 apart, for two directories in the same job. That gap is the entire problem.

Why the numbers look like that

An unprivileged LXC container maps container uids into an unused range on the host, conventionally starting at 100000. That's the security property: container root isn't host root. Anything the container writes to a network share arrives with the mapped uid.

inside the unprivileged LXC as the NAS sees it qBittorrent / Radarr / Sonarr uid 1000 (PUID honoured) uid 101000 ← owns the folder unpackerr (PUID ignored) uid 0, root uid 100000 ← not the owner drwxr-xr-x owner 101000 → 100000 falls through to "other" = r-x → no write → permission denied
The uid offset. Both containers are in the same LXC. One runs as uid 1000 and lands on 101000; the other runs as root and lands on 100000. The NAS applies ordinary Unix permissions to two users that, as far as it's concerned, are unrelated.

The actual root cause: PUID is not a Docker feature

This is the part worth taking away, because it applies far beyond this stack.

PUID and PGID are a LinuxServer.io convention. Their images ship an init step that reads those variables and switches to that user before starting the app. Docker itself does not know what PUID means. An image that wasn't built to read it will accept the variable, ignore it completely, and run as whatever its Dockerfile says — usually root.

golift/unpackerr is not a LinuxServer.io image. The compose file had PUID=1000 and PGID=1000 set, looking exactly like the containers around it, and they did nothing at all. Setting a variable an image ignores fails silently and looks like configuration.

Check what a container is actually running as, rather than what you asked for:

docker exec unpackerr id
# uid=0(root) gid=0(root) groups=0(root)     <- not what the compose file says

Why it broke when it did, not when it was configured

The misconfiguration was there from day one and harmless for months, because the download folders used to be mode 777. World-writable means uid is irrelevant. Sorting the share by modification time shows the break cleanly:

drwxrwxrwx  Jun 30, Jul 5, Jul 8      <- extracted fine (world-writable)
drwxr-xr-x  Jul 20, Jul 27, Jul 30     <- "Extraction Failed"

Something tightened the mode from 777 to 755 — most likely a qBittorrent image update changing its umask, though that was never confirmed. A latent misconfiguration plus an unrelated, reasonable-looking permissions change equals a bug that appears to come from nowhere. Nobody noticed for two weeks because most releases aren't RAR'd, so only archived releases touched the broken path at all.

The no_root_squash red herring

The NFS export on the NAS looked like it should make root all-powerful:

/volume1/downloads  10.0.1.43(rw,async,no_wdelay,crossmnt,insecure,no_root_squash,...)

Plenty of guides will tell you that permission denied plus root equals a root_squash problem. It's a dead end here, and understanding why is useful: an unprivileged LXC never sends uid 0 in the first place. Container root is already mapped to 100000 before the request reaches NFS, so there is no root for the server to squash. no_root_squash is neither the problem nor the fix, and toggling it changes nothing except your security posture.

The fix

Tell Docker directly, instead of asking the image nicely. The user: key is enforced by Docker, not by the image's entrypoint, so it works regardless of what conventions the image follows:

services:
  unpackerr:
    image: golift/unpackerr
    user: "1000:1000"        # enforced by Docker; PUID/PGID were not
    environment:
      - UN_RADARR_0_URL=...

Or on the command line: docker run --user 1000:1000 …. Then confirm it took effect, and confirm the ownership the NAS sees:

docker exec unpackerr id          # expect uid=1000
ls -ln /volume1/downloads/movies  # expect 101000 on new directories

Preventing the whole class of problem

The one-line version: PUID/PGID only work on images built to read them, an unprivileged LXC adds 100000 to every uid the storage sees, and the two together mean a container you believe is user 1000 is really writing as a user that owns nothing. ls -ln on the share, and docker exec … id in the container, answer it in ten seconds.