Smart Transcode

User and group IDs: the part that decides whether it works

Smart Transcode reads your files, writes a new file and replaces the original. Whether Linux allows that depends on numbers, not names.

How Linux decides

Every file stores the numeric user ID (UID) and group ID (GID) of its owner, plus permission bits. A program running as UID 1000 / GID 1000 may write a file when it is the owner and the owner bit allows it, when its GID matches and the group bit allows it, or otherwise when "others" may. Names such as alice are only a lookup in /etc/passwd on each machine. They are not stored in the file.

$ ls -ln /srv/media/tv
drwxrwxr-x 12 1000 1000 4096 Oct 27 10:02 Show.Name      # owner uid 1000, group gid 1000
$ stat -c '%u:%g  %U:%G' /srv/media
1000:1000  alice:alice
$ id alice
uid=1000(alice) gid=1000(alice) groups=1000(alice),44(video),105(render)

Why it matters even more in a container

A container has its own /etc/passwd and its own idea of who exists, but the files on your mounted folders keep the numbers they have on the host. A user called alice inside the container means nothing; only "the app runs as UID 1000" counts. That is what the two settings do:

SettingMeaningSet it to
PUIDThe numeric user ID the app runs as inside the container.The UID of the owner of your media (id -u alice).
PGIDThe numeric group ID it runs as.The GID of your media's group (id -g alice, or the shared group's number).
UMASKPermission bits removed from files the app creates. 002 lets the group write; 022 keeps it to the owner.002 if several users or apps (Sonarr, Radarr, Plex) share the files; otherwise either.

At start the container creates a user with exactly that UID / GID, gives it ownership of /config (the database), adds it to the groups that own the GPU devices, and drops root. Files it creates are owned by PUID:PGID on the host.

The compose file, line by line

services:
  smart-transcode:
    image: aarondyck/smart-transcode:latest
    environment:
      PUID: 1000          # = "stat -c %u /srv/media"  (the owner of the files)
      PGID: 1000          # = "stat -c %g /srv/media"  (the group of the files)
      UMASK: "002"        # new files: rw for owner and group
    volumes:
      - ./config:/config                  # owned by PUID:PGID after first start
      - /srv/media:/srv/media             # the files keep their host owner: 1000:1000

How the host and container line up for a library owned by 1000:1000 (mode 775 folders, 664 files):

PUID : PGIDResult
1000 : 1000Works. The app is the owner. New files are owned by 1000:1000.
1001 : 1000Works through the group (needs group write, so UMASK=002 and group-writable files). New files are owned by 1001:1000.
1001 : 1001Fails. Neither owner nor group matches: you can read the files at best, but cannot replace them.
0 : 0Works, but runs the app as root and leaves root-owned files behind. Avoid.
not setDefaults to 1000:1000. Fine only if that happens to be your media's owner.

Several folders, several owners

The app runs as one UID/GID. If your folders have different owners, pick a group they share and set PGID to it, give that group write access, and keep UMASK=002:

sudo groupadd -g 2000 media                 # once; use the same GID everywhere
sudo chgrp -R media /srv/media
sudo chmod -R g+rwX /srv/media
sudo find /srv/media -type d -exec chmod g+s {} +     # new files inherit the group
# then in the compose file:  PGID: 2000   UMASK: "002"

Give every container that touches the library (Sonarr, Radarr, Plex…) the same PUID, PGID and UMASK, so none of them creates files the others cannot change.

Network shares

  • NFS: the server stores plain numbers, so PUID/PGID must be the numbers the files have on the server (and the export must not squash them).
  • SMB / CIFS: the numbers come from your mount options. Mount with uid=1000,gid=1000,file_mode=0664,dir_mode=0775 and use the same PUID/PGID.
  • Keep /config on a local disk. SQLite on a network share is slow and can corrupt.

When you use user: instead of PUID / PGID

If a compose file sets user: "1000:1000" (or docker run --user), the container starts as that user directly. PUID, PGID and UMASK are then ignored, nobody fixes the ownership of /config for you (give the folder to that UID first), and the GPU groups are not added (use group_add with the numeric GID of the render group: stat -c %g /dev/dri/renderD128). Rootless Podman works this way on purpose, see Podman.

Check it from inside

docker exec smart-transcode id transcode           # uid / gid it runs as; groups include the GPU's
docker exec smart-transcode ls -ln /srv/media      # the numbers it sees; should match the host's ls -ln
docker exec -u transcode smart-transcode touch /srv/media/.write-test && echo writable
docker logs smart-transcode | head                 # "Smart Transcode 1.0 as transcode (1000:1000, groups: …)"
SymptomLikely cause
The library looks empty, or folders cannot be addedThe app's UID cannot read the folder (owner / group / mode mismatch), or the path inside the container is not the one you mounted.
Scans work, but every conversion fails at the end with "permission denied"The app can read but not write: PUID/PGID are not the owner (or in the group) and the group lacks write permission.
Converted files belong to root or to another userPUID/PGID are 0, unset, or different from the rest of your stack.
Sonarr / Radarr cannot read converted filesDifferent UMASK or group between the containers. Use the same PUID / PGID / UMASK everywhere.
"unable to open database file" at start/config is owned by another UID and the container was started with user:, or sits on a read-only or network mount.
No Intel / AMD profile in Settings → VideoThe user is not in the group that owns /dev/dri/renderD128 (check with ls -l /dev/dri inside the container).

The Linux service follows the same rule

install.sh --media /srv/media reads the owner of the folder and runs the service as that user (or creates transcode with the same numeric IDs). It never changes ownership of your files, and it tells you at the end if the account cannot write to a folder. Container helper: ./docker-setup.sh --media /srv/media does the same and writes the IDs into your .env.