User/Group Scheme¶
Pattern¶
Every service LXC follows the same rule:
- One shared group per access domain (controls access to shared data on $DATA_ROOT)
- One user per service (isolates each service to its own config directory only)
- Services run with PUID/PGID env vars in docker-compose (most LinuxServer.io images support this natively)
This means Radarr can write to its own config dir and to the shared movies folder, but cannot touch Sonarr's config. Jellyfin can read the full media library but cannot write to any config folder.
Before creating any user or group, read lxc idmap - the Proxmox unprivileged LXC UID remapping affects how host-side ownership must be set up.
Shared Groups¶
These GIDs must be identical on the host and inside every LXC that accesses the same $DATA_ROOT subdirectory. The host filesystem only sees numbers - if two LXCs use different GIDs for media, cross-container permissions break.
GIDs are set to the start of the UID range for that domain, so the numbering is self-consistent: IoT UIDs start at 1200 → iot GID is 1200, etc.
| GID | Group | Purpose | Used by |
|---|---|---|---|
| 1200 | iot |
IoT service shared data | iot (CT 201) |
| 1210 | ultrafeeder |
ADS-B feeder data (single-service group) | ultrafeeder (CT 200) only |
| 1300 | media |
Shared media library (movies, TV, downloads) | CT 300 (media-arr), CT 301 (media-server), CT 302 (media-dl) |
| 1400 | gaming |
Game server data | future game LXCs |
Create on host and in every LXC that needs the group:
Per-Service User UID Ranges¶
UID ranges mirror the LXC ID convention - the category is immediately readable from the number.
| UID Range | Category | Mirrors LXC Range |
|---|---|---|
| 1100–1199 | Infrastructure | 100–199 |
| 1200–1299 | IoT & monitoring | 200–299 |
| 1300–1399 | Media & storage | 300–399 |
| 1400–1499 | Game servers | 400–499 |
| 1600–1699 | Personal apps | 600–699 |
Per-Service Users¶
These only need to exist inside their own LXC and on the host (required for bind mount ownership - see lxc idmap).
| UID | User | Service | Primary GID | Extra Groups | LXC |
|---|---|---|---|---|---|
| 1200 | homebridge |
Homebridge | 1200 (iot) |
- | docker-iot |
| 1201 | zigbee2mqtt |
Zigbee2MQTT | 1200 (iot) |
- | docker-iot |
| 1202 | mosquitto |
Mosquitto | 1200 (iot) |
- | docker-iot |
| 1210 | ultrafeeder |
Ultrafeeder | 1210 (ultrafeeder) |
- | ultrafeeder |
| 1300 | sonarr |
Sonarr | 1300 (media) |
- | CT 300 (media-arr) |
| 1301 | radarr |
Radarr | 1300 (media) |
- | CT 300 (media-arr) |
| 1302 | prowlarr |
Prowlarr | 1300 (media) |
- | CT 300 (media-arr) |
| 1303 | bazarr |
Bazarr | 1300 (media) |
- | CT 300 (media-arr) |
| 1304 | seerr |
Seerr | 1300 (media) |
- | CT 300 (media-arr) - UID unused: Seerr runs as built-in node (1000:1000), ignores PUID/PGID |
| 1305 | unpackerr |
Unpackerr | 1300 (media) |
- | CT 300 (media-arr) |
| 1306 | recyclarr |
Recyclarr | 1300 (media) |
- | CT 300 (media-arr) |
| 1308 | dispatcharr |
Dispatcharr | 1300 (media) |
- | CT 300 (media-arr) - UID reserved/unused: aio image runs internal Postgres/Redis as root, ignores PUID/PGID |
| 1310 | jellyfin |
Jellyfin | 1300 (media) |
- | CT 301 (media-server) |
| 1311 | jellystat |
Jellystat | 1300 (media) |
- | CT 301 (media-server) - UID reserved; no bind mount (named Docker volume) |
| 1307 | sabnzbd |
SABnzbd | 1300 (media) |
- | CT 302 (media-dl) |
| 1320 | qbittorrent |
qBittorrent | 1300 (media) |
- | CT 302 (media-dl) |
| 1001 | actual-budget |
Actual Budget | private (1001) | - | CT 600 (personal-apps) - UID hardcoded in image (runs as internal actual user), no PUID/PGID support. Same exception pattern as Seerr (node/1000). |
Zigbee USB dongle access: the device is owned by group
iot(GID 1200) via the Proxmoxdev0config (gid=1200). zigbee2mqtt accesses it through its primaryiotgroup membership - nodialoutneeded. See lxc idmap for whydialout(GID 20) cannot be used in an unprivileged LXC.
Create a service user (run on host and inside the LXC):
Adding a New Service¶
- Pick the next available UID in the correct range (check the table above)
- Decide which shared group it needs - check the GIDs table above and add a new entry if a new domain is needed (current: iot=1200, media=1300, gaming=1400)
- Add a row to the table above
- Create the user on the host (required for bind mount ownership)
- Create the user inside the target LXC
- Create the $DATA_ROOT config directory with correct ownership (see data layout)
- Set
PUID/PGIDin the service's docker-compose entry - Update data layout with the new bind mount
Management LXC - Exception¶
Komodo Core and Traefik run as root (Docker socket access and port 80/443 binding). The management LXC does not use the per-service user pattern.
What you control at the OS level:
- Create an admin system user for SSH access
- Add admin to the docker group for CLI access without sudo
$DATA_ROOT Directory Permissions Reference¶
| Path | Owner (UID:GID) | Mode | Who can access |
|---|---|---|---|
$DATA_ROOT/config/iot/ |
100000:1200 |
750 | container root + iot group traversal |
$DATA_ROOT/config/iot/homebridge/ |
1200:1200 |
750 | homebridge only |
$DATA_ROOT/config/iot/zigbee2mqtt/ |
1201:1200 |
750 | zigbee2mqtt only |
$DATA_ROOT/config/iot/mosquitto/ |
1202:1200 |
750 | mosquitto only |
$DATA_ROOT/media-stack/media/movies/ |
root:1300 |
775 | all media group members |
$DATA_ROOT/media-stack/media/movies-4k/ |
root:1300 |
775 | all media group members |
$DATA_ROOT/media-stack/media/tv/ |
root:1300 |
775 | all media group members |
$DATA_ROOT/media-stack/media/anime/ |
root:1300 |
775 | all media group members |
$DATA_ROOT/media-stack/torrents/ |
root:1300 |
775 | all media group members |
$DATA_ROOT/config/media-arr/sonarr/ |
1300:1300 |
750 | sonarr only |
$DATA_ROOT/config/media-arr/radarr/ |
1301:1300 |
750 | radarr only |
$DATA_ROOT/config/media-arr/prowlarr/ |
1302:1300 |
750 | prowlarr only |
$DATA_ROOT/config/media-arr/bazarr/ |
1303:1300 |
750 | bazarr only |
$DATA_ROOT/config/media-arr/seerr/ |
1000:1000 |
750 | seerr (node user, UID 1000 - ignores PUID/PGID) |
$DATA_ROOT/config/media-arr/recyclarr/ |
1306:1300 |
750 | recyclarr only |
$DATA_ROOT/config/media-arr/dispatcharr/ |
100000:100000 |
750 | dispatcharr (aio image, container-root - ignores PUID/PGID) |
$DATA_ROOT/config/media-server/ |
100000:1300 |
750 | container root traversal + media group |
$DATA_ROOT/config/media-server/jellyfin/ |
1310:1300 |
750 | jellyfin only |
$DATA_ROOT/config/media-dl/sabnzbd/ |
1307:1300 |
750 | sabnzbd only |
$DATA_ROOT/config/media-dl/qbittorrent/ |
1320:1300 |
750 | qbittorrent only |
Key Takeaways¶
- GIDs for shared groups must match on the host and in every LXC that shares the same $DATA_ROOT folder
- UID ranges mirror LXC ID ranges - 1200s = IoT, 1300s = media, etc.
- Service users must be created on the host as well as inside the LXC - bind mount ownership requires it
- Management LXC is the only exception to the per-service-user rule
- See lxc idmap before creating any LXC with service-owned bind mounts