Ayyoub Amjahed

Tres capas, dos problemas: un passthrough de GPU que se quedó a medias

El dispositivo estaba en el contenedor, la verificación lo confirmaba y aun así la app no lo veía. Porque la app corría una capa más adentro. Y cuando lo vio, no tenía permiso para abrirlo.

3 min de lectura Proyecto: Homelab infraestructurahomelabdockerlxc

Immich, la biblioteca de fotos autoalojada del homelab, transcodifica los vídeos HEVC del iPhone a H.264 una vez, al importarlos. La tarjeta gráfica integrada del servidor lo acelera. Para eso hay que pasar /dev/dri al contenedor, y eso ya lo había hecho antes para Jellyfin con éxito. Repetí exactamente los mismos pasos.

Verifiqué el passthrough desde el host:

bash
pct exec 108 -- ls -la /dev/dri
# card1, renderD128: los dos dispositivos, con sus permisos

Los dispositivos estaban. Immich fallaba igual:

Device 'renderD128' does not exist

Problema 1: la capa que faltaba

La diferencia con Jellyfin no estaba en el passthrough. Estaba en dónde corre el proceso. Jellyfin corre nativo dentro del contenedor LXC. Immich corre dentro de Docker, que a su vez corre dentro del LXC. Tres capas:

Host  ->  LXC 108  ->  Docker  ->  proceso de Immich
  ok        ok         falta

Docker no hereda automáticamente los dispositivos del sistema donde se ejecuta. Hay que declararlos en el compose, uno a uno. Mi verificación era correcta y definitiva para Jellyfin, y aquí solo comprobaba la mitad del camino. Con la declaración añadida:

bash
docker compose exec immich-server ls -la /dev/dri
# ahora sí: card1 y renderD128

La lección es sobre dónde se verifica: en la capa donde corre el proceso que necesita el recurso, no en la intermedia. Que el contenedor vea el dispositivo no dice nada sobre lo que ve el proceso dentro del contenedor dentro del contenedor.

Problema 2: ver no es poder usar

Con el dispositivo ya visible desde Docker, Immich dejó de decir que no existía y pasó a decir otra cosa:

Device creation failed: -22

Ver el dispositivo y tener permiso para abrirlo son cosas distintas. El contenedor es unprivileged: los usuarios de dentro se mapean a un rango de UIDs del host, y renderD128 pertenece al grupo render del host, que cae fuera de ese rango. En un contenedor sin privilegios, ni siquiera root puede saltarse los permisos de un fichero cuyo propietario queda fuera del mapeo.

La solución fue darle al dispositivo, en el host, un grupo que sí cae dentro del rango mapeado del contenedor, y hacerlo persistente para que sobreviva a un reinicio. Y verificarlo donde importa: dentro del proceso de Immich, no en el LXC.

Cuánto importaba, en realidad

Poco, para el día a día. Immich transcodifica una sola vez por vídeo y guarda el resultado; Jellyfin lo hace en tiempo real en cada reproducción. Sin aceleración los vídeos se procesan igual, solo más lento y con la CPU saturada. Marca una diferencia real durante la importación masiva de la biblioteca (unos 20.000 elementos pendientes). Con la migración terminada, su impacto es marginal.

Lo cuento igualmente porque el error no es de Immich ni de la GPU. Es una forma de fallar que se repite cada vez que hay una capa más de la que crees:

  • Un passthrough se verifica en la capa donde corre el proceso.
  • Un dispositivo visible no es un dispositivo utilizable; los permisos se comprueban aparte.
  • Una verificación que fue definitiva en un caso parecido puede ser solo la mitad en el siguiente. Lo parecido engaña más que lo distinto.

La misma semana me pasó algo del mismo tipo con Docker y los puertos: con docker run -p EXTERNO:INTERNO solo el externo es alcanzable desde fuera, y dos veces seguidas apunté al puerto donde la aplicación «decía» escuchar en sus logs. Otra capa más de la que creía. Lo primero ante un error de conexión que corta en seco es docker ps, no el puerto que parece obvio.