clarify the non-root docs

This commit is contained in:
Josh Hawkins
2026-08-29 16:31:10 -05:00
parent 496327985a
commit 92c4aae7d7
+86 -68
View File
@@ -3,48 +3,53 @@ id: non_root
title: Running as a non-root user
---
# Running as a non-root user
Frigate's services run as an unprivileged user inside the container. The main Frigate process and nginx run as `frigate`, and go2rtc runs as its own more restricted `go2rtc` user. Only the s6 init system and the certsync helper stay root.
By default the runtime user is uid/gid `1000:1000`. You can change it with `PUID`/`PGID`, or bypass Frigate's user handling entirely with Docker's own `user:`.
The runtime user is uid/gid `1000:1000` by default. You can change it with `PUID`/`PGID`, or bypass Frigate's user handling entirely with Docker's own `user:`.
Most upgrades need nothing. Frigate aligns your volume ownership on the first boot and grants access to your hardware at startup. The sections below cover the cases that need attention: large storage volumes, network storage, and hardware the automatic grant can't reach.
## Run modes
| Mode | How to enable | Ownership of `/config` and `/media/frigate` | `read_only: true` |
| -------------------------------- | -------------------------------------- | ---------------------------------------------------------- | ----------------- |
| Default | nothing, this is the default | Aligned to `1000:1000` on first boot | Not supported |
| `PUID`/`PGID` | `PUID=1001`, `PGID=1001` | Aligned to the values you set, on first boot | Not supported |
| Docker-native user | `user: "1001:1001"` | You own it, Frigate never changes ownership | Not supported |
| Root (escape hatch) | `FRIGATE_RUN_AS_ROOT=true` | Never touched | Not supported |
| Granular root | `FRIGATE_ROOT_SERVICES=frigate` | Aligned at boot; recordings and exports also at create | Not supported |
| Mode | How to enable | Ownership of `/config` and `/media/frigate` | `read_only: true` |
| ------------------- | ------------------------------- | ------------------------------------------------------ | ----------------- |
| Default | nothing, this is the default | Aligned to `1000:1000` on first boot | Not supported |
| `PUID`/`PGID` | `PUID=1001`, `PGID=1001` | Aligned to the values you set, on first boot | Not supported |
| Docker-native user | `user: "1001:1001"` | You own it, Frigate never changes ownership | Not supported |
| Root (escape hatch) | `FRIGATE_RUN_AS_ROOT=true` | Never touched | Not supported |
| Granular root | `FRIGATE_ROOT_SERVICES=frigate` | Aligned at boot; recordings and exports also at create | Not supported |
`PUID`/`PGID` remapping runs `usermod` at startup, which writes to `/etc/passwd`, so it can't work with a read-only root filesystem. That combination fails fast at startup with a message pointing here rather than failing obscurely later.
`PUID`/`PGID` remapping runs `usermod` at startup, which writes to `/etc/passwd`, so it can't work with a read-only root filesystem. That combination stops at startup with a message pointing here.
`FRIGATE_RUN_AS_ROOT` is matched against the exact lowercase string `true`. `True`, `TRUE`, and `1` are all ignored.
`FRIGATE_DEVICE_ACLS` follows the same exact-match rule: only the lowercase string `false` disables the automatic device grants.
`FRIGATE_RUN_AS_ROOT` is matched against the exact lowercase string `true`. `True`, `TRUE`, and `1` are all ignored. `FRIGATE_DEVICE_ACLS` works the same way: only the lowercase string `false` turns off the automatic device grants.
### Keeping individual services root
`FRIGATE_ROOT_SERVICES` takes a comma-separated list of `frigate`, `go2rtc`, and `nginx`. A listed service keeps running as root while everything else about non-root operation still applies: `PUID`/`PGID` remapping, the ownership sweep, and ownership of the files those services create.
`FRIGATE_ROOT_SERVICES` takes a comma separated list of `frigate`, `go2rtc`, and `nginx`. A listed service keeps running as root, and everything else about non-root operation still applies: `PUID`/`PGID` remapping, the ownership sweep, and ownership of the files those services create.
The two cases this exists for:
There are two reasons to use it:
- Your detector hardware won't cooperate with [device access](#hardware-device-access) as an unprivileged user. `FRIGATE_ROOT_SERVICES=frigate` keeps the main process (and every detector it spawns) root while nginx and go2rtc stay unprivileged.
- You want root behavior but still want your volumes owned by `PUID`/`PGID` instead of root. `FRIGATE_ROOT_SERVICES=frigate,go2rtc,nginx` runs everything as root and still keeps file ownership aligned.
- Your detector hardware won't work as an unprivileged user, even after reading [Hardware device access](#hardware-device-access). `FRIGATE_ROOT_SERVICES=frigate` keeps the main process and its detectors as root while nginx and go2rtc stay unprivileged.
- You want everything to run as root but still want your files owned by `PUID`/`PGID` instead of root. `FRIGATE_ROOT_SERVICES=frigate,go2rtc,nginx` does that.
Prefer solving device access with `EXTRA_GROUPS` before reaching for this. The `frigate` service owns the API and every ffmpeg process decoding your camera streams, so listing it runs those as root too, not just the detectors.
Try the device grants and `EXTRA_GROUPS` first. The `frigate` service runs the API and every ffmpeg process that decodes your camera streams, so listing it puts those back on root as well, not just your detectors.
Recordings and exports are owned by `PUID`/`PGID` the moment they're written, even by a root service. Everything else (snapshots, thumbnails, and other files under `clips/`) is realigned on each restart, so files written mid-run can show as root-owned from the host until the next one. A listed service also keeps root's home directory, so library caches land in the container layer instead of `/config`, same as the escape hatch.
Recordings and exports are owned by `PUID`/`PGID` as soon as they're written, even by a root service. Snapshots, thumbnails, and other files under `clips/` are corrected on each restart, so they can show as root-owned from the host until then. A listed service also keeps root's home directory, so library caches go to the container layer instead of `/config`.
Listing all three services is not the same as `FRIGATE_RUN_AS_ROOT=true`. The escape hatch never touches ownership at all; the list keeps the ownership machinery running. Unknown names in the list stop the container at startup rather than silently dropping a service you meant to keep root. Changing the list re-runs the full ownership sweep once on the next boot. If both are set, `FRIGATE_RUN_AS_ROOT=true` wins and the list is ignored entirely. With Docker's own `user:` the list has no effect, since the container never has root to keep.
Listing all three services is not the same as `FRIGATE_RUN_AS_ROOT=true`. The escape hatch never touches ownership; the list keeps the ownership handling active. A few more details:
- An unknown name in the list stops the container at startup, rather than silently leaving a service unprivileged.
- Changing the list runs the full ownership sweep once on the next boot.
- If both are set, `FRIGATE_RUN_AS_ROOT=true` wins and the list is ignored.
- With Docker's `user:`, the list does nothing, since the container never has root to keep.
## Migrating an existing install
Volumes created by earlier versions of Frigate are owned by root. Ownership has to be aligned with the runtime user once.
Volumes from earlier versions of Frigate are owned by root, so ownership has to be aligned with the runtime user once. This happens automatically on the first boot after upgrading.
There are two independent prerequisites to upgrading, and doing one without the other is the most common way this goes wrong. This section covers volume ownership. If you use a Coral, a GPU, or any other accelerator, skim [Hardware device access](#hardware-device-access) as well: Frigate grants those devices to the runtime user at startup, so most setups need nothing, but hardware the automatic grant can't reach still needs host side work that has nothing to do with your volumes.
This happens automatically on the first boot after upgrading, but on large recordings volumes it's much better to do it from the host beforehand. The boot sweep runs before any service starts, so a multi-terabyte `/media/frigate` can hold the container in startup long enough for Docker's healthcheck to mark it unhealthy, and orchestrators that react to health will restart it mid-sweep. If you'd rather not run the script, raise the healthcheck start period instead (`--start-period=1800s`, or `start_period: 1800s` under `healthcheck:` in compose).
On large recordings volumes, do it from the host beforehand instead. The boot sweep runs before any service starts, so a multi-terabyte `/media/frigate` can hold the container in startup long enough for Docker's healthcheck to mark it unhealthy, and orchestrators that watch health will restart it mid-sweep. If you'd rather not run the script, raise the healthcheck start period instead (`--start-period=1800s`, or `start_period: 1800s` under `healthcheck:` in compose).
Grab `fix-permissions.sh` from `docker/migration/` in the Frigate repo and dry run it first:
@@ -58,7 +63,9 @@ That reports how many entries would change and touches nothing. When it looks ri
./fix-permissions.sh /path/to/your/config /path/to/your/storage
```
Both the script and the boot sweep report progress as they go, so you can tell a slow sweep apart from a stuck one:
Pass `PUID` and `PGID` as the third and fourth arguments if you're not using the default `1000:1000`. The script wraps the same helper the container uses, so the result is identical either way. Override the image it pulls with `FRIGATE_IMAGE=...` if you're not on `stable`.
Both the script and the boot sweep report progress, so you can tell a slow sweep from a stuck one:
```
[INFO] fix-ownership: scanning /media/frigate for ownership mismatches; this may take a while on large filesystems
@@ -68,66 +75,71 @@ Both the script and the boot sweep report progress as they go, so you can tell a
[INFO] fix-ownership: finished /media/frigate in 12m 4s
```
The scan has no percentage behind it because the total isn't known until it finishes. Watch the boot sweep with `docker logs -f frigate`.
The scan has no percentage because the total isn't known until it finishes. Watch the boot sweep with `docker logs -f frigate`.
Pass `PUID` and `PGID` as the third and fourth arguments if you're not using the default `1000:1000`. The script wraps the same `fix-ownership` helper the container uses, so it's the same logic either way. Override the image it pulls with `FRIGATE_IMAGE=...` if you're not on `stable`.
Once the volumes are aligned, start Frigate normally. A file at `/config/.permissions_version` records what was done, so later boots skip the sweep unless you change `PUID`/`PGID`.
Once the volumes are aligned, start Frigate normally. A sentinel at `/config/.permissions_version` records what was done, so later boots skip the sweep entirely unless you change `PUID`/`PGID`.
`lost+found` is left alone. It belongs to the filesystem rather than to Frigate, and `fsck` recovers pieces of arbitrary files into it with root-only permissions, so handing it to the runtime user would expose whatever ends up there. Expect it to stay owned by root on any volume that's a dedicated mount.
`lost+found` is left alone. It belongs to the filesystem rather than to Frigate, and `fsck` recovers fragments of arbitrary files into it under root-only permissions, so handing it to the runtime user would expose whatever ends up there. Expect to see it still owned by root afterward, on any volume that's a dedicated mount.
If something under your volumes genuinely can't be chowned, a read-only btrfs snapshot directory for example, the sweep warns and names the path, and it deliberately doesn't write the sentinel. That means it retries on the next boot rather than recording a migration that didn't finish. Either move those paths outside `/media/frigate` or expect the scan to repeat.
If something under your volumes can't be chowned, a read-only btrfs snapshot directory for example, the sweep warns and names the path and doesn't record the migration as finished. It retries on the next boot instead. Either move those paths outside `/media/frigate` or expect the scan to repeat.
### Network storage
Storing recordings on a NAS is common, and ownership behaves differently there. Check what you have before migrating anything:
Recordings on a NAS behave differently, so check what you have before migrating:
```bash
findmnt -T /path/to/your/storage -o TARGET,FSTYPE,OPTIONS
```
**SMB and CIFS** don't store ownership per file at all. It's synthesized from the mount options, so a per-file `chown` fails, and you don't need one. Mount the share as the uid and gid Frigate will run as, and every file already looks correct to the sweep:
**SMB and CIFS** don't store per-file ownership at all. It's synthesized from the mount options, so a per-file `chown` fails and isn't needed. Mount the share as the uid and gid Frigate runs as, and every file already looks correct to the sweep:
```
//nas/frigate /media/frigate cifs credentials=/root/.smb,uid=1000,gid=1000,file_mode=0664,dir_mode=0775 0 0
```
**NFS** exports default to `root_squash` on most servers, which maps the container's root to `nobody`. The recursive chown then fails outright, you get `[WARN] fix-ownership: some entries under /media/frigate could not be updated`, and because the sweep didn't finish it deliberately doesn't write the sentinel, so it retries on the next boot and every boot after.
**NFS** exports default to `root_squash` on most servers, which maps the container's root to `nobody`. The chown then fails, you get `[WARN] fix-ownership: some entries under /media/frigate could not be updated`, and since the sweep didn't finish it doesn't record the migration, so it retries on every boot.
The best fix is to not chown over NFS at all. Do it on the server, locally, where there's no squash and no network round trip per file:
The best fix is to not chown over NFS at all. Do it on the server, where there's no squash and no network round trip per file:
```bash
# on the NAS itself, against the exported directory
chown -R 1000:1000 /export/frigate
```
Frigate's own sweep then finds nothing to change and records the sentinel normally. If you can't get a shell on the server, the alternatives are to export temporarily with `no_root_squash`, migrate, and put it back, or to leave ownership alone and set `PUID`/`PGID` to whichever uid already owns the files.
Frigate's sweep then finds nothing to change and records the migration normally. If you can't get a shell on the server, you can export temporarily with `no_root_squash`, migrate, and put it back, or leave ownership alone and set `PUID`/`PGID` to whichever uid already owns the files.
Either way the uid has to mean the same thing on both machines. NFS sends numeric uids, so container uid 1000 is simply uid 1000 on the server no matter what the usernames are.
Either way the uid has to mean the same thing on both machines. NFS sends numeric uids, so container uid 1000 is uid 1000 on the server no matter what the usernames are.
Expect the first boot to be slow even when nothing needs changing, because verifying ownership costs a round trip per file. The sentinel makes that a one-time price. **If the sweep runs on every boot rather than once, ownership isn't actually being applied**, and the warning above will tell you so.
Expect the first boot to be slow even when nothing needs changing, because checking ownership costs a round trip per file. That's a one-time cost. **If the sweep runs on every boot rather than once, ownership isn't actually being applied**, and the warning above will say so.
Keep `/config` on local storage regardless. Frigate's database is SQLite and network shares handle its locking poorly. That's a pre-existing recommendation, not something running non-root introduces.
Keep `/config` on local storage either way. Frigate's database is SQLite and network shares handle its locking poorly. That's a long-standing recommendation, not something running non-root introduces.
## Rolling back
Set `FRIGATE_RUN_AS_ROOT=true` and restart. Everything runs as root again, exactly as it did before. This is the fastest way to get a broken install running while you work out a device permission problem, and it's the recommended fallback for hardware you can't grant access to.
Set `FRIGATE_RUN_AS_ROOT=true` and restart. Everything runs as root again, exactly as it did before. This is the fastest way to get a broken install running while you sort out a device permission problem.
The escape hatch never changes ownership, and it deletes the sweep sentinel on startup, so switching back to non-root later re-sweeps whatever root created in the meantime. Toggling in either direction is safe.
The escape hatch never changes ownership, and it clears the record of the last sweep on startup, so switching back to non-root later corrects whatever root created in the meantime. Toggling in either direction is safe.
## Hardware device access
Frigate grants the runtime user access to mapped-in devices automatically at startup: pass your hardware with `--device` (or `devices:` in compose) and detection and hardware acceleration work with no group or udev setup. The grant covers the common accelerator and camera nodes (GPU render nodes, Coral, Hailo, Rockchip, Jetson, `/dev/video*`, and the USB bus). For hardware it misses, add your own paths with `DEVICE_ACL_PATHS` (comma-separated globs, for example `DEVICE_ACL_PATHS=/dev/mydev*`), or fall back to the manual setup below. Set `FRIGATE_DEVICE_ACLS=false` (exact lowercase) if you manage device permissions yourself and want Frigate to touch nothing.
Frigate grants the runtime user access to your devices at startup. Pass your hardware with `--device` (or `devices:` in compose) and detection and hardware acceleration work with no group or udev setup on the host.
The sections below are the manual fallback for hardware the automatic grant cannot cover, and for `user:` mode, where there is no root startup to do the granting.
The grant covers the common accelerator and camera nodes: GPU render nodes, Coral, Hailo, Rockchip, Jetson, `/dev/video*`, and the USB bus. For hardware it misses, add your own paths with `DEVICE_ACL_PATHS`, a comma separated list of globs:
One host-visible side effect to know about: `--device` nodes are private to the container, but a bind-mounted `/dev/bus/usb` (the documented Coral USB setup) shares the host's nodes, so the grant for the runtime user is briefly visible on the host until udev recreates the node. It's a per-user grant, not a mode change.
```yaml
environment:
DEVICE_ACL_PATHS: "/dev/mydev*"
```
The reason any of this needs explaining is that your accelerator almost certainly works today *because* Frigate runs as root. Device nodes are commonly owned by `root:root`, and root either matches the file's group or bypasses the check entirely. The runtime user does neither, so a node that was fine yesterday can become unreadable with no change to your Frigate config at all.
Set `FRIGATE_DEVICE_ACLS=false` if you manage device permissions yourself and want Frigate to leave them alone.
The automatic grant is what closes that gap: it adds a uid-scoped ACL entry for the runtime users and leaves the node's owner and mode alone. For anything it can't reach, device node permissions are the host's to set, so the fix belongs there.
Frigate grants access by adding an ACL entry for the runtime users. The device's owner and mode are unchanged, and nothing is made world accessible. One thing to know: `--device` nodes belong to the container, but a bind mounted `/dev/bus/usb` (the usual Coral USB setup) shares the host's device nodes, so the entry is visible on the host until udev recreates the node.
### Read what your device actually requires
The rest of this section is the manual fallback, for hardware the grant can't reach and for Docker's `user:` mode, where there's no root startup to do the granting.
Your accelerator most likely worked in older versions because Frigate ran as root. Device nodes are usually owned by `root:root`, and root either matches the group or skips the check entirely. The runtime user does neither, so a device that worked before can become unreadable with no change to your Frigate config.
### Read what your device requires
Find the node and look at its owner, group, and mode:
@@ -140,38 +152,38 @@ crw-rw---- 1 0 105 226, 128 Jul 5 10:12 /dev/dri/renderD128
# mode: owner rw, group rw, other none
```
Then ask which of the three permission sets the runtime user lands in. It isn't the owner (that's root), so it gets the group bits only if it belongs to that GID, and otherwise falls through to "other". In the example above "other" is empty, so without membership in group 105 the runtime user cannot open the node at all.
Then work out which of the three permission sets applies to the runtime user. It isn't the owner, since that's root, so it gets the group bits if it belongs to that GID and otherwise falls through to "other". In the example above "other" is empty, so without membership in group 105 the runtime user can't open the node.
The trap is a node that looks permissive but isn't. A USB Coral defaults to this:
Watch for a node that looks permissive but isn't. A USB Coral defaults to this:
```bash
ls -ln /dev/bus/usb/004/003
crw-rw-r-- 1 0 0 189, 386 Jul 5 10:12 /dev/bus/usb/004/003
```
That's group `0`, so "other" applies to everyone else, and "other" here is read only. `libedgetpu` needs to *write* to the node, so detection fails with `No EdgeTPU was detected` as though no Coral were attached. Read access alone is not enough for most accelerators.
The group is `0`, so "other" applies to the runtime user, and "other" here is read only. `libedgetpu` needs to write to the node, so detection fails with `No EdgeTPU was detected` as though no Coral were attached. Read access alone isn't enough for most accelerators.
### Grant access
Give the runtime user the GID with `EXTRA_GROUPS`, a comma separated list of numeric host GIDs. They're added to both the `frigate` and `go2rtc` users at startup, which matters because go2rtc needs render and video access of its own to run hardware accelerated restreams.
Give the runtime user the GID with `EXTRA_GROUPS`, a comma separated list of numeric host GIDs. They're added to both the `frigate` and `go2rtc` users, which matters because go2rtc needs its own render and video access for hardware accelerated restreams.
```yaml
environment:
EXTRA_GROUPS: "105,44" # host render and video GIDs
```
Use numeric GIDs from the host, not names. Group names don't have to agree between the host and the container, and the kernel only checks the number. If the GID doesn't exist in the image, Frigate creates a placeholder group for it.
Use numeric GIDs from the host, not names. Group names don't have to match between the host and the container, and the kernel only checks the number. If the GID doesn't exist in the image, Frigate creates a placeholder group for it.
Two things that look like they should work and don't:
Two things that look like they should work but don't:
- Docker's `group_add` has no effect in the default or `PUID` modes. `s6-setuidgid` rebuilds the supplementary group list from `/etc/group` when it drops privileges, which discards whatever Docker gave the init process. It *is* the right tool with Docker-native `user:`, where no privilege drop happens and `EXTRA_GROUPS` in turn does nothing.
- `privileged: true` doesn't help. It grants capabilities to root, and the runtime user isn't root, so ordinary file permissions on the node still apply.
- Docker's `group_add` has no effect in the default or `PUID` modes. Frigate rebuilds the supplementary group list from `/etc/group` when it drops privileges, which discards what Docker passed in. It is the right tool with Docker's `user:`, where no privilege drop happens and `EXTRA_GROUPS` does nothing.
- `privileged: true` doesn't help. It grants capabilities to root, and the runtime user isn't root, so the file permissions on the node still apply.
If the node's group is `root` or the mode denies the group, no `EXTRA_GROUPS` value can help. You need a udev rule first.
If the node's group is `root` or the mode denies the group, no `EXTRA_GROUPS` value will help. You need a udev rule first.
### Verify before you rely on it
### Verify access
Check the group landed, then check the runtime user can actually open the node. Test for write, not just read:
Check the group landed, then check the runtime user can open the node. Test for write, not just read:
```bash
docker exec frigate id frigate
@@ -179,16 +191,16 @@ docker exec frigate /command/s6-setuidgid frigate sh -c 'test -w /dev/dri/render
docker exec frigate /command/s6-setuidgid go2rtc sh -c 'test -w /dev/dri/renderD128 && echo ok'
```
Permission probes are still only a proxy for the driver working. These exercise the real libraries as the runtime user:
A permission check is only a proxy for the driver working. These exercise the real libraries as the runtime user:
```bash
docker exec frigate /command/s6-setuidgid frigate vainfo
docker exec frigate /command/s6-setuidgid frigate python3 -c "import openvino as ov; print(ov.Core().available_devices)"
```
`vainfo` should reach `va_openDriver() returns 0` and list profiles. Complaints about `XDG_RUNTIME_DIR` or an X server above that are normal and harmless. OpenVINO must list `GPU`; if it returns only `CPU`, detection has quietly fallen back and inference will be far slower without any error in the log.
`vainfo` should reach `va_openDriver() returns 0` and list profiles. Complaints about `XDG_RUNTIME_DIR` or an X server above that are normal. OpenVINO should list `GPU`; if it returns only `CPU`, detection has fallen back and inference will be much slower without an error in the log.
If something isn't working, the fastest way to tell a permissions problem from anything else is to start the container once with `FRIGATE_RUN_AS_ROOT=true`. If the device appears as root and not otherwise, it's node permissions and a udev rule is the fix. If it's missing either way, the problem is your device mapping or the host, and it isn't related to running non-root.
To tell a permissions problem from anything else, start the container once with `FRIGATE_RUN_AS_ROOT=true`. If the device works as root and not otherwise, it's node permissions and a udev rule is the fix. If it's missing either way, the problem is your device mapping or the host, and isn't related to running non-root.
### udev rules by device
@@ -198,18 +210,18 @@ Rules go in `/etc/udev/rules.d/` on the host and take effect after:
sudo udevadm control --reload-rules && sudo udevadm trigger
```
An already-connected device sometimes keeps its original ownership through a trigger. If `ls -ln` doesn't show the new group, replug it, or reboot for a built-in device.
A device that's already connected sometimes keeps its original ownership through a trigger. If `ls -ln` doesn't show the new group, replug it, or reboot for a built-in device.
**Coral USB** needs two rules, because the device re-enumerates after firmware load. It appears as Global Unichip `1a6e` cold and Google `18d1` once running, with a different node each time. A rule covering only `1a6e` gives you a Coral that initializes once and then disappears mid-run.
**Coral USB** needs two rules, because the device re-enumerates after loading firmware. It appears as Global Unichip `1a6e` before and Google `18d1` after, with a different node each time. A rule covering only `1a6e` gives you a Coral that starts up once and then disappears mid-run.
```
SUBSYSTEM=="usb", ATTRS{idVendor}=="1a6e", GROUP="plugdev", MODE="0664"
SUBSYSTEM=="usb", ATTRS{idVendor}=="18d1", GROUP="plugdev", MODE="0664"
```
Map the whole `/dev/bus/usb` rather than a single node, for the same re-enumeration reason. Most hosts already put `plugdev` at GID 46 and the image agrees, so you often need no `EXTRA_GROUPS` entry for a USB Coral. Confirm with `getent group plugdev` and add the number if your host differs.
Map the whole `/dev/bus/usb` rather than a single node, for the same reason. Most hosts put `plugdev` at GID 46 and the image agrees, so a USB Coral often needs no `EXTRA_GROUPS` entry. Confirm with `getent group plugdev` and add the number if your host differs.
**Coral PCIe** is frequently `crw------- root root`, which nothing but root can open:
**Coral PCIe** is often `crw------- root root`, which only root can open:
```
SUBSYSTEM=="apex", MODE="0660", GROUP="apex"
@@ -217,13 +229,13 @@ SUBSYSTEM=="apex", MODE="0660", GROUP="apex"
Create the group with `sudo groupadd -f apex`, then add its GID to `EXTRA_GROUPS`.
**Hailo** follows the same shape. Grant `/dev/hailo0` a group and add that GID:
**Hailo** works the same way. Grant `/dev/hailo0` a group and add that GID:
```
SUBSYSTEM=="hailo_chardev", MODE="0660", GROUP="hailo"
```
**Intel and AMD GPUs** usually need nothing beyond `EXTRA_GROUPS`, since distributions ship a `render` group owning `/dev/dri/renderD128` already. Note the GID often differs between host and image, so pass the host's number rather than assuming the name resolves. Debian based images have no `render` group at all.
**Intel and AMD GPUs** usually need nothing beyond `EXTRA_GROUPS`, since most distributions ship a `render` group that owns `/dev/dri/renderD128`. The GID often differs between the host and the image, so pass the host's number rather than assuming the name resolves. Debian based images have no `render` group at all.
### Quick reference
@@ -250,10 +262,16 @@ SUBSYSTEM=="hailo_chardev", MODE="0660", GROUP="hailo"
## Known limitations
`telemetry.stats.network_bandwidth` uses nethogs, which needs `CAP_NET_ADMIN` and `CAP_NET_RAW` and therefore root. The stat is disabled automatically when Frigate isn't running as root, with one warning in the log. Use `FRIGATE_ROOT_SERVICES=frigate` (or `FRIGATE_RUN_AS_ROOT=true`) if you need it.
`telemetry.stats.network_bandwidth` uses nethogs, which needs `CAP_NET_ADMIN` and `CAP_NET_RAW` and therefore root. The stat is turned off automatically when Frigate isn't running as root, with one warning in the log. Use `FRIGATE_ROOT_SERVICES=frigate` (or `FRIGATE_RUN_AS_ROOT=true`) if you need it.
go2rtc's ffmpeg processes no longer appear in Intel GPU stats. Frigate reads per-process GPU usage from `/proc/<pid>/fdinfo`, which the kernel won't let one user read for another user's processes, so anything go2rtc spawns is invisible to it. Overall GPU utilization is unaffected.
If you mount your own TLS certificate at `/etc/letsencrypt/live/frigate`, the private key has to be readable by the runtime user. Frigate won't change ownership of a certificate you supplied, since the mount may be read-only.
If you're debugging nginx, run the config check as the runtime user with stdout discarded: `docker exec frigate /command/s6-setuidgid frigate bash -c 'nginx -t -c /tmp/nginx/conf/nginx.conf >/dev/null'`. Running `nginx -t` as root hands nginx's runtime directories to root as a side effect, which breaks the running workers until the service restarts, and the config's `/dev/stdout` logs can't be reopened through a root-owned `docker exec` pipe (the results print on stderr either way).
If you're debugging nginx, run the config check as the runtime user with stdout discarded:
```bash
docker exec frigate /command/s6-setuidgid frigate bash -c 'nginx -t -c /tmp/nginx/conf/nginx.conf >/dev/null'
```
Running `nginx -t` as root hands nginx's runtime directories to root as a side effect, which breaks the running workers until the service restarts, and the config's `/dev/stdout` logs can't be reopened through a root-owned `docker exec` pipe. The results print on stderr either way.