Three layers, two problems: a GPU passthrough that stopped halfway
The device was in the container, the check confirmed it, and the app still couldn't see it. Because the app was running one layer further in. And once it could see it, it had no permission to open it.
Immich, the self-hosted photo library of the homelab,
transcodes the iPhone’s HEVC videos to H.264 once, on import. The server’s
integrated graphics accelerates it. For that you have to pass /dev/dri into
the container, and I’d already done that for Jellyfin successfully. I
repeated exactly the same steps.
I verified the passthrough from the host:
pct exec 108 -- ls -la /dev/dri
# card1, renderD128: both devices, with their permissionsThe devices were there. Immich failed all the same:
Device 'renderD128' does not existProblem 1: the missing layer
The difference from Jellyfin wasn’t in the passthrough. It was in where the process runs. Jellyfin runs natively inside the LXC container. Immich runs inside Docker, which in turn runs inside the LXC. Three layers:
Host -> LXC 108 -> Docker -> Immich process
ok ok missingDocker doesn’t automatically inherit the devices of the system it runs on.
They have to be declared in the compose file, one by one. My check was
correct and definitive for Jellyfin, and here it only covered half the path.
With the declaration added:
docker compose exec immich-server ls -la /dev/dri
# now yes: card1 and renderD128The lesson is about where you verify: in the layer where the process that needs the resource runs, not the intermediate one. The container seeing the device says nothing about what the process inside the container inside the container sees.
Problem 2: seeing isn’t being able to use
With the device now visible from Docker, Immich stopped saying it didn’t exist and started saying something else:
Device creation failed: -22Seeing the device and having permission to open it are different things.
The container is unprivileged: users inside are mapped to a range of host
UIDs, and renderD128 belongs to the host’s render group, which falls
outside that range. In an unprivileged container, not even root can bypass
the permissions of a file whose owner is outside the mapping.
The fix was to give the device, on the host, a group that does fall inside the container’s mapped range, and to make it persistent so it survives a reboot. And to verify it where it matters: inside the Immich process, not in the LXC.
How much it actually mattered
Not much, day to day. Immich transcodes once per video and stores the result; Jellyfin does it in real time on every playback. Without acceleration the videos get processed anyway, just slower and with the CPU pinned. It makes a real difference during the bulk import of the library (about 20,000 items pending). With the migration done, its impact is marginal.
I’m telling it anyway because the mistake isn’t about Immich or the GPU. It’s a way of failing that repeats every time there’s one more layer than you think:
- A passthrough is verified in the layer where the process runs.
- A visible device isn’t a usable device; permissions are checked separately.
- A check that was definitive in a similar case may only be half of it in the next. Similar is more deceptive than different.
The same week something of the same kind happened to me with Docker and
ports: with docker run -p EXTERNAL:INTERNAL only the external one is
reachable from outside, and twice in a row I pointed at the port where the
application “said” it was listening in its logs. One more layer than I
thought. The first thing to do on a connection error that cuts off dead is
docker ps, not the port that seems obvious.