# Media permissions — `media` GID 10000

Standard for any LXC reading/writing `/mnt/library` on [hubris](../hosts/hubris.md). Applied 2026-04-20.

## Standard

Every LXC that mounts `/mnt/library` participates in a shared `media` group with **GID 10000**. Shared subtrees are owned by that group with the setgid bit (`drwxrwsr-x`, mode `2775`), so new files auto-inherit the right group regardless of which container wrote them.

## Why

`/mnt/library` is a cross-container storage pool. \*arr writes, jellyfin reads, mulita scans, paperless ingests. Without a shared group, each container sees files as `nobody:nogroup` (unprivileged) or `www-data` (privileged 1:1) and the permission web collapses into one-off chmods. GID 10000 bridges privileged and unprivileged containers.

## Onboarding a new LXC

1. `pct set <id> -mp0 /mnt/library,mp=/mnt/library` (if not already mounted).
2. Inside the container:
   ```
   groupadd -g 10000 media
   usermod -aG media <service-user>   # for every user that needs library access
   ```
3. If the container is **unprivileged** (check `pct config <id> | grep unprivileged`), append this idmap block to `/etc/pve/lxc/<id>.conf` (back up first):
   ```
   lxc.idmap: u 0 100000 65536
   lxc.idmap: g 0 100000 10000
   lxc.idmap: g 10000 10000 1
   lxc.idmap: g 10001 110001 55535
   ```
   Then `pct stop <id> && pct start <id>`.
4. For systemd services running with `User=root` (not typical), add a drop-in with `SupplementaryGroups=media`. Systemd skips `initgroups()` for `User=root`.
5. `pct exec` sessions don't get supplementary groups (no initgroups). Use `sudo -i` or `su - <user>` inside the container to verify membership interactively. Real services use `initgroups` and work correctly.

## State snapshot

### Host

- Group `media` GID 10000 exists.
- `/etc/subgid` has `root:100000:65536` AND `root:10000:1` (second line required for unprivileged LXCs to receive GID 10000).
- Shared subtrees owned `:media` mode `2775` (drwxrwsr-x, setgid):
  - `movies`, `tv`, `music`, `anime`, `podcasts` — jellyfin libraries
  - `audiobooks`, `audiobookshelf-metadata`, `books`, `comics` — audiobookshelf / grimmory
  - `downloads` — \*arr stack output
  - `images` — photoprism / immich / mulita
  - `roms` — emu frontends
  - `syncthing` — empty subtree, retained for archaeology (LXC 109 destroyed 2026-05-14)
- Container-specific subtrees intentionally **not** migrated (keep their own owner:group):
  - `documents` (paperless, `www-data:www-data 750`)
  - `homecloud` (nextcloud — its own permission model, easy to break)
  - `marimo` (marimo venv) — *LXC since destroyed; review whether subtree still serves a purpose*
  - `notes`, `sophia` (single-container use); `heaper` — orphaned data subtree (LXC since destroyed 2026-05-14, 224 MiB retained)
  - `repos` (owner UID 102 GID 105 from inside [gitea](../containers/104-gitea.md) — don't touch)

### LXCs with media-group membership

| ID  | Name                                          | Priv | Media-group members                           |
| --- | --------------------------------------------- | ---- | --------------------------------------------- |
| 101 | [jellyfin](../containers/101-jellyfin.md)     | **unpriv + idmap** | jellyfin                          |
| 103 | [paperless](../containers/103-paperless.md)   | priv | www-data                                      |
| 104 | [gitea](../containers/104-gitea.md)           | priv | www-data, gitea                               |
| 105 | [apps](../containers/105-apps.md)             | priv | www-data                                      |
| 114 | [nextcloud](../containers/114-nextcloud.md)   | priv | www-data                                      |
| 119 | [sophia](../containers/119-sophia.md)         | priv | www-data                                      |
| 120 | [mule-images](../containers/120-mule-images.md) | priv | www-data                                    |
| 122 | [arriman](../containers/122-arriman.md)       | priv | www-data, audiobookshelf, radarr, sonarr, lidarr, prowlarr, qbittorrent, bazarr, jellyseerr, mylar, jackett, overseerr, plex, arr |
| 130 | [grimmory](../containers/130-grimmory.md)     | priv | Docker container uses `GROUP_ID=10000` env var (linuxserver pattern) — no in-LXC group needed |
| 132 | [rclone](../containers/132-rclone.md)         | priv | **read-only** mount; runs as root → reads all subtrees. No media group needed |

> Some entries from earlier snapshots — 100 (arr-yunohost), 107 (marimo), 109 (syncthing), 110 (photoprism), 112 (immich), 116 (heaper) — referenced LXCs that have since been destroyed. See [containers/index](../containers/index.md#recently-destroyed-kept-for-archaeology).

Config backups: `/root/101.conf.bak.*`, `/root/109.conf.bak.*` (109 destroyed 2026-05-14).

## Gotchas

- **[apps (105)](../containers/105-apps.md) and [grimmory (130)](../containers/130-grimmory.md) are Docker hosts.** Adding `media` to the LXC alone is *not* enough for Docker containers inside. Each Docker container needs its GID passed in explicitly: `--group-add 10000`, `user: "<uid>:10000"`, or `GROUP_ID=10000` (linuxserver images) in compose. Grimmory, audiobookshelf-in-docker, etc. need this per-container.
- **`pct exec` does NOT run initgroups.** So `pct exec <id> -- id` shows only the primary group. For interactive verification, use `pct exec <id> -- sudo -i -u root id` or `su - <user> -c id`. Real systemd services work fine.
- **systemd `User=root`** skips initgroups — explicit `SupplementaryGroups=media` drop-in needed.
- **`pct restore`** or template rebuilds wipe in-container group membership and unprivileged-LXC idmap blocks. Re-apply from this page.
- **`/etc/subgid`** must retain both `root:100000:65536` AND `root:10000:1`. Dropping the second breaks startup of any unprivileged LXC with the idmap block.
- **\*arr "Set Permissions" options** can override the setgid inheritance by explicitly chown'ing files. Leave those off, or set the group to `media`. Relevant to Sonarr/Radarr/qBittorrent on [arriman (122)](../containers/122-arriman.md).
- **Nextcloud** files under `/mnt/library/homecloud` are deliberately NOT in the media group. NC manages its own permission model. See [nextcloud (114)](../containers/114-nextcloud.md).
- **\*arr stack on arriman** required `MEDIACENTER_GID=10000` (not 13000) in `.env` because s6-setuidgid only honors the primary PGID; `group_add:` doesn't propagate. See [arriman (122)](../containers/122-arriman.md#changelog).

## Related
- [Hubris host](../hosts/hubris.md)
- All container pages list whether they're in the standard

## Changelog

### 2026-05-14 — LXC 109 (syncthing) destroyed
Removed the syncthing row from the membership table and the syncthing-as-`User=root` example from the onboarding section. `/mnt/library/syncthing` subtree was already empty and retained as an empty dir.

### 2026-05-14 — LXC 116 (heaper) destroyed
Removed the heaper row from the LXC membership table and noted the orphaned `/mnt/library/heaper` subtree (224 MiB retained). See [host changelog](../hosts/hubris.md#changelog).

### 2026-04-28 — wiki entry created
Initial documentation.

### 2026-04-26 — `MEDIACENTER_GID` fix on [arriman (122)](../containers/122-arriman.md)
qBit was erroring every torrent with "Permission denied" because `MEDIACENTER_GID=13000` was set as a supplementary GID via `group_add:`. Changed to 10000 (primary GID); fix described above is now standard.

### 2026-04-20 — standard rolled out
GID 10000 hostgroup, idmap blocks for unprivileged LXCs, setgid 2775 on shared subtrees, `media` membership for service users in every participating LXC.
