Homelab
Jellyfin en un NAS: Docker, transcoding por hardware y la TV que no quería conectar
7 min de lectura
Puntos clave
- Jellyfin en Docker con la GPU integrada de Intel (/dev/dri) para convertir vídeo sin fundir la CPU.
- Puerto publicado solo en 127.0.0.1 y Caddy delante con HTTPS automático.
- Una TV LG antigua fallaba con error -27: certificado ECDSA y cabecera X-Frame-Options. Se arregla en dos líneas.
- Un procesador modesto (Celeron J4125) aguanta 4-5 conversiones simultáneas por hardware.
Quería una sola aplicación para ver mis películas y series en la TV del salón, en el móvil y fuera de casa, sin pagar suscripciones ni subir nada a la nube. Jellyfin cumple todo eso: es libre, no tiene cuentas de pago y funciona en casi cualquier dispositivo.
Lo que no esperaba era que el problema más difícil no fuera instalarlo, sino conseguir que una TV LG de hace unos años se conectara. Esta guía cuenta el montaje completo y ese caso, para que no pierdas una tarde como yo.
Qué vas a montar
- Jellyfin en Docker, con la gráfica integrada de Intel para convertir vídeo (transcoding por hardware).
- Caddy delante, con HTTPS automático, para entrar desde fuera con un dominio.
- Un par de ajustes para que clientes lentos o antiguos (TV, Fire TV) vayan fluidos.
Mi equipo es un NAS con un Intel Celeron J4125 (gráfica UHD 600) y 8 GB de RAM. Si tu NAS o miniPC tiene un Intel parecido, todo lo que sigue te sirve tal cual.
Requisitos
- Docker y Docker Compose en el NAS o servidor.
- Un procesador Intel con gráfica integrada (Quick Sync). Compruébalo así:
ls -l /dev/dri
Si aparece renderD128, tienes gráfica disponible para Jellyfin.
- Un dominio apuntando a tu IP pública, si quieres entrar desde fuera (en los ejemplos uso
jellyfin.example.com).
Paso 1: el docker-compose.yml
Crea una carpeta para Jellyfin (en Synology, por ejemplo /volume1/docker/jellyfin) y dentro este archivo:
services:
jellyfin:
image: jellyfin/jellyfin:latest # cuando funcione, fija la versión que probaste
container_name: jellyfin
user: "1026:100" # tu UID:GID (ver abajo)
group_add:
- "937" # grupo dueño de /dev/dri/renderD128 (ver abajo)
devices:
- /dev/dri:/dev/dri # la gráfica integrada, para transcoding
ports:
- "127.0.0.1:8096:8096" # solo accesible desde el propio servidor
volumes:
- ./config:/config
- ./cache:/cache
- /volume1/media:/media:ro # tu biblioteca, en solo lectura
environment:
- TZ=Europe/Madrid
- JELLYFIN_PublishedServerUrl=https://jellyfin.example.com
restart: unless-stopped
Los dos números que tienes que cambiar:
id # uid=... gid=... de tu usuario -> "user"
stat -c '%g' /dev/dri/renderD128 # grupo de la gráfica -> "group_add"
Arranca y comprueba que el contenedor ve la gráfica:
docker compose up -d
docker exec jellyfin ls -l /dev/dri
El puerto se publica en
127.0.0.1, no en0.0.0.0. Así Jellyfin no queda abierto en tu red ni en Internet: solo Caddy, en el mismo equipo, llega a él.
Paso 2: activar el transcoding por hardware
Cuando un dispositivo no puede reproducir un vídeo tal cual (formato, resolución o ancho de banda), Jellyfin lo convierte al vuelo. Sin gráfica, eso lo hace la CPU y un J4125 se ahoga con una sola película 4K. Con la gráfica, puede con varias a la vez.
En Jellyfin: Panel de control → Reproducción → Transcodificación:
- Aceleración por hardware:
Intel QuickSync (QSV). - Decodificación por hardware: marca
H264,HEVC,HEVC 10bit,VP9yMPEG2. No marques AV1 si tu gráfica no lo soporta (la UHD 600 no). - Guarda.
Para comprobarlo, reproduce en el móvil una película forzando una calidad baja (por ejemplo 720p) y mira Panel de control → Actividad. La sesión debe indicar que transcodifica y el uso de CPU debe quedarse bajo.
En un J4125 he llegado a 4-5 conversiones simultáneas. Más allá, empiezan los cortes.
Paso 3: publicarlo con Caddy
Caddy consigue y renueva los certificados HTTPS solo. Este es el bloque mínimo para Jellyfin:
jellyfin.example.com {
reverse_proxy localhost:8096 {
flush_interval -1 # streaming sin buffer
transport http {
response_header_timeout 0 # las películas largas no se cortan
}
}
}
Recarga Caddy y entra en https://jellyfin.example.com. En el móvil, el navegador y la mayoría de apps, esto ya funciona.
El caso de la TV que no quería conectar
La app de Jellyfin en mi TV LG (webOS de hace unos años) daba error -27 y no cargaba. Mientras, el móvil, el iPad y la Fire TV iban perfectos. El problema resultó ser doble.
Causa 1: el certificado
Caddy genera por defecto certificados ECDSA, que Let’s Encrypt firma con una cadena que termina en una raíz nueva. Muchas TV antiguas no tienen esa raíz y rechazan la conexión. Con un certificado RSA, la cadena acaba en la raíz clásica (ISRG Root X1), que sí conocen.
jellyfin.example.com {
tls {
key_type rsa2048
}
# ...resto igual
}
Solo lo aplico a Jellyfin. El resto de servicios sigue con ECDSA, que es más ligero.
Causa 2: la cabecera que bloquea la app
La app de webOS carga la web de Jellyfin dentro de un marco (iframe). Si tu proxy añade X-Frame-Options: DENY o una CSP con frame-ancestors 'self' (cabeceras de seguridad muy habituales), la TV no puede mostrarla y falla con el mismo error. Para Jellyfin, quita esas dos cabeceras y conserva el resto:
header {
-Server
-X-Frame-Options
-Content-Security-Policy
X-Content-Type-Options "nosniff"
Referrer-Policy "strict-origin-when-cross-origin"
Strict-Transport-Security "max-age=31536000; includeSubDomains"
}
Con estos dos cambios, la TV conectó a la primera.
Paso 4 (opcional): que vaya fluido en clientes lentos
La TV conectaba, pero la pantalla de inicio tardaba mucho. Dos ajustes en Caddy lo mejoraron:
- Caché agresiva de lo que no cambia. Las imágenes y los archivos de la web de Jellyfin llevan una huella en la URL: si cambian, cambia la URL. Se pueden guardar un año sin riesgo.
- Comprimir solo texto. Comprimir vídeo o imágenes gasta CPU y no reduce nada.
@immutable path_regexp ^/(Items/[^/]+/Images/|web/.+\.[0-9a-f]{8,}\.(js|css|woff2?|png|jpg|svg))
header @immutable Cache-Control "public, max-age=31536000, immutable"
encode zstd gzip {
match {
header Content-Type text/*
header Content-Type application/json*
header Content-Type application/javascript*
}
}
Y como es un servidor privado, no una web, bloqueo a los robots:
respond /robots.txt <<TXT
User-agent: *
Disallow: /
TXT 200
El Caddyfile completo
Junta todo lo anterior. Copia, cambia el dominio y listo:
jellyfin.example.com {
tls {
key_type rsa2048 # compatibilidad con TV antiguas
}
header {
-Server
-X-Frame-Options # la app de webOS usa un iframe
-Content-Security-Policy
X-Content-Type-Options "nosniff"
Referrer-Policy "strict-origin-when-cross-origin"
Strict-Transport-Security "max-age=31536000; includeSubDomains"
}
respond /robots.txt <<TXT
User-agent: *
Disallow: /
TXT 200
@immutable path_regexp ^/(Items/[^/]+/Images/|web/.+\.[0-9a-f]{8,}\.(js|css|woff2?|png|jpg|svg))
header @immutable Cache-Control "public, max-age=31536000, immutable"
encode zstd gzip {
match {
header Content-Type text/*
header Content-Type application/json*
header Content-Type application/javascript*
}
}
reverse_proxy localhost:8096 {
flush_interval -1
transport http {
response_header_timeout 0
}
}
}
Problemas frecuentes
| Síntoma | Causa probable | Solución |
|---|---|---|
| La reproducción usa 100 % de CPU | El contenedor no ve la gráfica | Revisa devices y group_add; docker exec jellyfin ls -l /dev/dri |
| Error al transcodificar tras activar QSV | Códec marcado que la gráfica no soporta | Desmarca AV1 o HEVC 10bit y prueba de nuevo |
| La TV da error -27 | Certificado ECDSA o X-Frame-Options |
Sección “El caso de la TV” |
| Películas largas se cortan | Tiempo máximo de respuesta del proxy | response_header_timeout 0 y flush_interval -1 |
| No encuentra los archivos | Permisos de la carpeta de medios | El UID de user debe poder leer /volume1/media |
Conclusión
Jellyfin en Docker con la gráfica integrada es una de las cosas más útiles que he montado en el NAS: un Netflix propio que funciona en todos los dispositivos de casa. La parte difícil no fue Jellyfin, sino los clientes antiguos. Si algo falla solo en un dispositivo, sospecha primero del certificado y de las cabeceras del proxy.
En el próximo capítulo de la serie contaré cómo protejo todo lo que publico (Caddy, CrowdSec y qué dejo solo para la VPN).
Preguntas frecuentes
¿Necesito una GPU dedicada?
No. Casi cualquier Intel con gráfica integrada desde 2016 (Quick Sync) convierte vídeo por hardware. En un Celeron J4125 aguanta 4-5 reproducciones convertidas a la vez.
¿Es seguro exponer Jellyfin a Internet?
Con cuidado: HTTPS, contraseñas fuertes, sin cuenta de administrador para el día a día y, si puedes, detrás de una VPN o de un sistema que bloquee IPs abusivas (por ejemplo CrowdSec).
¿Por qué mi TV muestra error -27 y el móvil funciona?
Normalmente por la cadena del certificado (las TV antiguas no confían en la raíz nueva de los certificados ECDSA) o por cabeceras que impiden cargar la web dentro de la app. Mira la sección de la TV.
Fuentes
Proyecto relacionado
Infraestructura Pesoz
Homelab self-hosted en producción 24/7: un NAS Synology con ~30 servicios en Docker tras Caddy + CrowdSec y un portátil con GPU como servidor de IA y observabilidad en k3s. VPN WireGuard, GitOps con Forgejo y estado en vivo con Uptime Kuma.




