Llevas cincuenta y dos lecciones administrando srv-tramontana. Sabes montar un proxy inverso con TLS, ajustar PostgreSQL, cifrar un volumen, endurecer una unidad de systemd y reconstruir la máquina entera con Ansible en cincuenta minutos. Y existe una duda razonable que casi todo el mundo tiene a estas alturas: ¿cuánto de esto es «cosa de servidores de empresa» y cuánto se aplica de verdad en cualquier sitio?
Esta lección responde a esa pregunta construyendo algo que es tuyo: un servidor de medios para tu casa. Vas a montar Jellyfin sobre tu propio hardware, con almacenamiento redundante, aceleración por hardware para la transcodificación, comparticiones para los dispositivos de la familia, acceso desde fuera y copias de seguridad. Y vas a descubrir que es exactamente el mismo trabajo: los mismos UUID en fstab, la misma clave en keyrings, la misma unidad endurecida, el mismo smartctl, el mismo Ansible.
Con dos diferencias que en casa importan y en el centro de datos no: el consumo eléctrico y el ruido. Y con una advertencia legal que conviene leer antes de empezar.
Contenido
- Advertencia legal: qué contenido es legítimo
- Objetivo, requisitos y decisiones de diseño
- Elegir el hardware
- Almacenamiento: RAID, Btrfs y la verdad sobre las copias
- Jellyfin frente a Plex y Emby
- Instalación y unidad de systemd endurecida
- Aceleración por hardware para transcodificación
- Organización de la biblioteca y nombres de fichero
- Compartir: Samba para todo, NFS para Linux
- Acceso desde fuera de casa: tres opciones y su riesgo
- Descargas y automatización
- Copias de seguridad de la biblioteca y de la configuración
- Consumo, ruido y temperatura
- Automatización con Ansible
- Operación: actualizaciones, salud de discos y qué hacer cuando uno falla
Advertencia legal: qué contenido es legítimo
Va primero porque es lo primero.
Un servidor de medios es una herramienta neutra: sirve ficheros que tú le das. Lo que determina su legalidad es el origen del contenido, y en España la situación es razonablemente clara:
| Contenido | Situación |
|---|---|
| Fotos y vídeos que has grabado tú | Tuyo, sin ninguna duda |
| Copia de un DVD o Blu-ray que has comprado | Zona gris: la copia privada existe, pero saltarse la protección anticopia está prohibido |
| Música de CD que has comprado, ripeada | Copia privada, generalmente aceptada |
| Contenido comprado en tiendas digitales sin DRM | Tuyo, según la licencia |
| Obras en dominio público o con licencia libre | Legítimo |
| Descargas de redes P2P de obras protegidas | No es legítimo |
| Contenido de una suscripción, descargado saltándose el DRM | No es legítimo |
Este curso no cubre en ningún momento la obtención de contenido protegido por derechos de autor. Todo lo que sigue asume que la biblioteca contiene material propio, adquirido legalmente, o libre. Si tienes 4 TB de vídeos familiares, discos comprados y cine clásico en dominio público, este proyecto es exactamente para ti.
Y una nota adicional: si compartes el servidor con personas fuera de tu unidad familiar, aunque sea con contenido comprado, estás haciendo comunicación pública, que es otra cosa distinta a la copia privada.
Objetivo, requisitos y decisiones de diseño
Objetivo. Un servidor doméstico que sirva la biblioteca familiar a televisores, móviles, tabletas y portátiles dentro de casa; accesible desde fuera de forma segura; con almacenamiento tolerante al fallo de un disco; con copias de lo insustituible; y con un consumo eléctrico que no duela.
Decisiones de diseño, tomadas antes de comprar nada:
| Decisión | Elección | Por qué |
|---|---|---|
| Sistema operativo | Ubuntu Server 24.04 LTS | El mismo del curso: todo lo aprendido se aplica |
| Interfaz gráfica | Ninguna | Consume RAM y añade superficie de ataque para nada |
| Servidor de medios | Jellyfin | Libre, sin cuenta, sin telemetría (apartado 5) |
| Almacenamiento | Btrfs RAID 1 sobre dos discos | Checksums, instantáneas y ampliación flexible |
| Acceso desde fuera | VPN (08-04) | Cero puertos expuestos |
| Compartición interna | Samba, y NFS solo si hay clientes Linux | Compatibilidad universal |
| Gestión | Ansible | Reconstruible como srv-tramontana |
Y una decisión que conviene tomar explícitamente: este servidor es el segundo laboratorio. Al ser tuyo y no crítico para nadie, es donde puedes probar cosas que no probarías en producción. Esa es una de las mejores razones para montarlo.
Llamaremos al equipo srv-casa, con IP fija 192.168.1.20 en la red doméstica.
Elegir el hardware
| Opción | Precio orientativo | Consumo | Transcodificación | Almacenamiento | Ruido |
|---|---|---|---|---|---|
| Raspberry Pi 5 (8 GB) | 80-120 € | 4-8 W | Muy limitada; sin decodificación H.265 por hardware | USB, sin SATA nativo | Silenciosa |
| Mini PC con Intel N100 | 130-200 € | 6-15 W | Excelente: Quick Sync moderno, AV1 | 1-2 M.2 + SATA | Muy bajo |
| NAS comercial (Synology, QNAP) | 300-700 € | 15-30 W | Variable según modelo | 2-4 bahías | Bajo |
| PC reutilizado | 0 € | 40-90 W | Depende de la gráfica | Muchas bahías | Alto |
| Servidor de segunda mano | 100-300 € | 60-150 W | Sin GPU normalmente | Muchas bahías | Muy alto |
La recomendación es el mini PC con Intel N100 o similar, y por una razón que se ve en la tabla del apartado 13: el consumo. Un servidor que está encendido 24 horas al día durante un año consume 8.760 horas de su potencia media. La diferencia entre 10 W y 60 W son 438 kWh al año, unos 105 € en España a 0,24 €/kWh. En tres años, la diferencia de electricidad paga el mini PC entero y sobra.
Sobre la Raspberry Pi 5, que es la opción más popular y merece un matiz honesto: es magnífica para una biblioteca de música, fotos o vídeo que los clientes reproduzcan directamente sin conversión. Pero la Pi 5 eliminó el decodificador H.265 por hardware que tenía la Pi 4, y no tiene codificador de vídeo. Si algún cliente necesita transcodificar —un televisor viejo, un móvil por datos, un formato no soportado— la Pi lo hará por software, con un resultado que va de lento a inviable. Si tu biblioteca es H.264 y tus clientes son modernos, la Pi va perfecta; si tienes material H.265 4K y un televisor antiguo, no.
Sobre reutilizar un PC antiguo: es gratis y es la peor opción a medio plazo. Consume mucho, hace ruido, sus ventiladores tienen diez años y su fuente de alimentación es lo que más falla en un equipo encendido permanentemente. Como laboratorio para aprender, perfecto. Como servidor permanente, sale caro.
Sobre RAM: 4 GB bastan para Jellyfin sirviendo a dos o tres clientes. 8 GB dan holgura para caché de sistema de ficheros, que es lo que hace que la biblioteca se navegue con fluidez.
Almacenamiento: RAID, Btrfs y la verdad sobre las copias
Cuánto espacio hace falta de verdad
| Contenido | Tamaño típico |
|---|---|
| Canción en FLAC | 30-50 MB |
| Álbum completo en FLAC | 400-600 MB |
| Foto de móvil moderno | 3-8 MB |
| Foto RAW de cámara | 25-45 MB |
| Episodio de serie 1080p H.264 | 1,2-2,5 GB |
| Episodio de serie 1080p H.265 | 400-900 MB |
| Película 1080p H.264 | 4-12 GB |
| Película 1080p H.265 | 2-5 GB |
| Película 4K HDR | 25-60 GB |
| Copia íntegra de un Blu-ray | 25-45 GB |
Una biblioteca familiar realista —20 años de fotos, la música comprada, unas doscientas películas y algunas series— ronda los 4-8 TB. La regla práctica: calcula lo que crees que necesitas y duplícalo, porque el 4K y las fotos RAW crecen más rápido de lo que la intuición sugiere.
Por qué aquí sí conviene la redundancia
En 05-04 montaste LVM sin RAID, porque srv-tramontana es una VM cuyo almacenamiento ya es redundante en el anfitrión y porque su valor está en las copias, no en el disco. En casa la situación es la contraria: los discos son físicos y son tuyos, y hay contenido insustituible —las fotos familiares— que no se puede volver a descargar ni comprar.
| Nivel | Discos | Capacidad útil | Tolera | Comentario |
|---|---|---|---|---|
| RAID 0 | 2+ | 100 % | Nada | Duplica el riesgo. No en casa |
| RAID 1 | 2 | 50 % | 1 disco | Lo recomendado: simple y suficiente |
| RAID 5 | 3+ | (n−1)/n | 1 disco | Reconstrucción larga y arriesgada con discos grandes |
| RAID 6 | 4+ | (n−2)/n | 2 discos | Para 6+ discos |
| RAID 10 | 4+ | 50 % | 1 por par | Rápido, caro en discos |
RAID 5 con discos de 8 TB o más merece un aviso. Reconstruir el conjunto tras cambiar un disco puede tardar más de un día, durante el cual los demás discos trabajan al límite leyendo cada sector. Es precisamente el momento en que otro disco tiene más probabilidad de fallar, y si falla, se pierde todo. Con dos discos y RAID 1, la reconstrucción es una copia sencilla y el riesgo es mucho menor.
mdadm frente a Btrfs y ZFS
mdadm + ext4 |
Btrfs | ZFS | |
|---|---|---|---|
| En el kernel de Ubuntu | Sí | Sí | Sí (módulo DKMS) |
| Detecta corrupción silenciosa | No | Sí: checksum de datos y metadatos | Sí |
| La repara automáticamente | No | Sí, con RAID 1 | Sí |
| Instantáneas | Con LVM | Nativas, instantáneas | Nativas |
| Añadir un disco de otro tamaño | No | Sí | Difícil |
| Compresión transparente | No | Sí (zstd) | Sí |
| RAID 5/6 | Sólido | NO USAR: sigue considerándose inestable | Sólido (RAIDZ) |
| Consumo de RAM | Bajo | Bajo | Alto: ~1 GB por TB con deduplicación |
| Licencia | GPL | GPL | CDDL: no se distribuye en el kernel |
La elección es Btrfs en RAID 1, y el argumento decisivo es el checksumming. Un RAID clásico protege del fallo de un disco entero, que es ruidoso y evidente. No protege de la corrupción silenciosa: un bit que cambia en el disco por degradación magnética, un error de firmware, un cable defectuoso. mdadm no puede detectarlo —tiene dos copias distintas y no sabe cuál es la buena—, y ese bit corrompido se propaga a tus copias de seguridad. Btrfs guarda una suma de comprobación de cada bloque: detecta el error, sabe que la otra copia es correcta y la repara.
Para fotos familiares que van a estar treinta años en un disco, eso no es un detalle técnico.
$ sudo apt install btrfs-progs smartmontools
# 1. Revisar el estado de los discos ANTES de usarlos, incluso nuevos
$ sudo smartctl -a /dev/sda | grep -E 'Model|Power_On_Hours|Reallocated'
Device Model: WDC WD80EFPX-68C4ZN0
9 Power_On_Hours 0x0032 100 100 000 Old_age Always - 4
5 Reallocated_Sector_Ct 0x0033 100 100 140 Pre-fail Always - 0
# 2. Crear el sistema de ficheros en RAID 1 (datos Y metadatos)
$ sudo mkfs.btrfs -L medios -d raid1 -m raid1 /dev/sda /dev/sdb
Label: medios
UUID: 8f3a2c11-4d5e-4a91-b7c2-1e9d0f8a3b45
Number of devices: 2
Data,RAID1: 8.00TiB
Metadata,RAID1: 8.00GiB
# 3. Montar por UUID, nunca por /dev/sdX (05-04)
$ sudo mkdir -p /srv/medios
$ blkid /dev/sda | grep -oP 'UUID="\K[^"]+'
8f3a2c11-4d5e-4a91-b7c2-1e9d0f8a3b45# /etc/fstab
# compress=zstd:3 -> comprime lo comprimible; el video ya comprimido lo
# detecta y lo salta, asi que no cuesta nada
# noatime -> no escribir la fecha de acceso en cada lectura:
# menos escrituras y mas vida del disco
# nofail -> SI EL DISCO NO ESTA, EL SISTEMA ARRANCA IGUAL.
# Sin esto, un disco desconectado deja el equipo en
# modo emergencia, sin red y sin SSH: hay que ir con
# teclado y monitor. En casa, imprescindible.
# x-systemd.device-timeout=10 -> no esperar 90 s a un disco ausente
UUID=8f3a2c11-4d5e-4a91-b7c2-1e9d0f8a3b45 /srv/medios btrfs \
defaults,compress=zstd:3,noatime,nofail,x-systemd.device-timeout=10 0 0$ sudo mount -a # SIEMPRE tras tocar fstab (convencion del curso)
$ findmnt /srv/medios
TARGET SOURCE FSTYPE OPTIONS
/srv/medios /dev/sda btrfs rw,noatime,compress=zstd:3,nofail
$ sudo btrfs filesystem usage /srv/medios
Overall:
Device size: 14.55TiB
Device allocated: 2.02TiB
Used: 2.01TiB
Free (estimated): 6.26TiB (min: 6.26TiB)
Data,RAID1: Size:1.01TiB, Used:1.00TiB
/dev/sda 1.01TiB
/dev/sdb 1.01TiBFíjate en que Device size muestra 14,55 TiB (los dos discos) pero Free estima 6,26 TiB: en RAID 1 cada bloque se escribe dos veces. df -h da cifras confusas con Btrfs; usa siempre btrfs filesystem usage.
El barrido: la operación que justifica Btrfs
# Verifica TODOS los checksums y repara lo que pueda con la otra copia
$ sudo btrfs scrub start -B /srv/medios
scrub done for 8f3a2c11-4d5e-4a91-b7c2-1e9d0f8a3b45
Total to scrub: 2.01TiB
Rate: 178.42MiB/s
Error summary: csum=2
Corrected: 2
Uncorrectable: 0Corrected: 2. Dos bloques estaban corruptos en un disco y Btrfs los ha reparado con la copia buena del otro. Con mdadm esos dos bloques habrían seguido ahí, corruptos, y probablemente habrían acabado en las copias de seguridad. Este es el argumento entero, en una línea de salida.
El barrido se programa mensualmente con un timer, siguiendo el patrón de 05-05:
# /etc/systemd/system/btrfs-scrub.timer
[Unit]
Description=Barrido mensual de Btrfs en /srv/medios
[Timer]
OnCalendar=Sun *-*-01..07 03:00:00 # primer domingo de cada mes
Persistent=true
RandomizedDelaySec=1h
[Install]
WantedBy=timers.target# /etc/systemd/system/btrfs-scrub.service
[Unit]
Description=Barrido de Btrfs
[Service]
Type=oneshot
ExecStart=/usr/bin/btrfs scrub start -B -c idle -n 5 /srv/medios
# -c idle -n 5: prioridad de E/S minima, para no molestar si alguien
# esta viendo una pelicula a las 3 de la manana
Nice=19
IOSchedulingClass=idleRAID no es copia de seguridad
Hay que decirlo con todas las letras, porque es el error doméstico más caro:
El RAID protege del fallo de un disco. No protege de nada más.
| Amenaza | ¿RAID 1 protege? |
|---|---|
| Se estropea un disco | Sí |
| Corrupción silenciosa de un bloque | Sí, con Btrfs; no con mdadm |
| Borras una carpeta por error | No: se borra de los dos discos |
| Un cifrador de ficheros ataca el equipo | No: cifra los dos discos |
| Falla la fuente de alimentación y se lleva los discos | No |
| Sobretensión, incendio, inundación, robo | No |
| Actualización que corrompe el sistema de ficheros | No |
De las siete filas, el RAID cubre una y media. Por eso el apartado 12 existe.
Jellyfin frente a Plex y Emby
| Jellyfin | Plex | Emby | |
|---|---|---|---|
| Licencia | GPL, libre | Propietaria | Propietaria (fue libre) |
| Requiere cuenta en la nube | No | Sí, incluso para uso local | Opcional |
| Telemetría | Ninguna | Sí, y ha habido polémicas | Sí |
| Funciones de pago | Ninguna: todo incluido | Plex Pass para HW, móvil, saltar intros | Emby Premiere |
| Aceleración por hardware | Gratis | Requiere suscripción | Requiere suscripción |
| Aplicaciones de TV | Android TV, webOS, Tizen, Roku, Kodi | Más y más pulidas | Intermedio |
| Funciona si Internet se cae | Sí, completamente | Puede fallar la autenticación | Sí |
| Madurez de la interfaz | Buena, algo más áspera | Excelente | Buena |
| Metadatos automáticos | Sí | Sí, algo mejores | Sí |
La elección es Jellyfin, y hay tres razones que pesan más que la interfaz algo menos pulida:
- No depende de nadie. Plex exige una cuenta en su nube incluso para ver una película desde el salón. El día que Plex cambie de modelo de negocio, suba precios o cierre —cosas que han pasado con productos de este tipo— tu biblioteca sigue funcionando con Jellyfin exactamente igual. Es la definición práctica de por qué el software libre importa, y esta lección es un buen sitio para comprobarlo.
- La aceleración por hardware es gratuita. En Plex y Emby es de pago, y es precisamente la función que decide si el servidor sirve o no sirve.
- Sin telemetría. Lo que ve tu familia en casa no sale de casa.
Instalación y unidad de systemd endurecida
Repositorio oficial con la clave en keyrings, exactamente como en 05-03 — nada de apt-key, que está obsoleto, ni de canalizar un script a bash:
$ sudo install -m 0755 -d /etc/apt/keyrings
$ curl -fsSL https://repo.jellyfin.org/jellyfin_team.gpg.key | \
sudo gpg --dearmor -o /etc/apt/keyrings/jellyfin.gpg
$ sudo chmod 0644 /etc/apt/keyrings/jellyfin.gpg
$ cat <<EOF | sudo tee /etc/apt/sources.list.d/jellyfin.sources
Types: deb
URIs: https://repo.jellyfin.org/ubuntu
Suites: noble
Components: main
Architectures: amd64
Signed-By: /etc/apt/keyrings/jellyfin.gpg
EOF
$ sudo apt update && sudo apt install jellyfin
$ systemctl is-active jellyfin
activeComprobación obligatoria de que la clave es la que dice ser, antes de confiar en el repositorio:
$ gpg --show-keys /etc/apt/keyrings/jellyfin.gpg | head -2
pub rsa4096 2018-11-08 [SC]
04B7DA6B1FE10D5E76D9D5A6F5B2F0D5A8E5D6C3Esa huella se contrasta con la publicada en la documentación oficial. Es un paso que casi todo el mundo se salta y que es la única defensa real frente a un repositorio comprometido.
La unidad endurecida
El paquete trae una unidad funcional pero poco restrictiva. Siguiendo la convención de drop-ins del curso, no se edita: se complementa.
# /etc/systemd/system/jellyfin.service.d/override.conf
[Service]
# --- Aislamiento del sistema de ficheros ---
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
# Solo estas rutas son escribibles
ReadWritePaths=/var/lib/jellyfin /var/log/jellyfin /var/cache/jellyfin /etc/jellyfin
# La biblioteca, SOLO LECTURA: Jellyfin no tiene ninguna razon para
# poder borrar tus peliculas. Es la restriccion mas importante de todas.
ReadOnlyPaths=/srv/medios
# --- Privilegios ---
NoNewPrivileges=true
PrivateDevices=false # false: necesita /dev/dri para la GPU
DeviceAllow=/dev/dri/renderD128 rw
DeviceAllow=/dev/dri/card0 rw
CapabilityBoundingSet=
AmbientCapabilities=
RestrictSUIDSGID=true
# --- Kernel y memoria ---
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
ProtectProc=invisible
MemoryDenyWriteExecute=false # .NET usa compilacion JIT: no se puede
LockPersonality=true
RestrictRealtime=true
RestrictNamespaces=true
# --- Red ---
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
IPAddressAllow=localhost 192.168.1.0/24 10.8.0.0/24
IPAddressDeny=any
# --- Llamadas al sistema ---
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @resources @obsolete
SystemCallArchitectures=native
# --- Limites de recursos (cgroups, 07-05) ---
# Evita que una transcodificacion descontrolada bloquee el equipo
CPUQuota=350%
MemoryMax=3G
IOWeight=50$ sudo systemctl daemon-reload && sudo systemctl restart jellyfin
$ systemd-analyze security jellyfin
→ Overall exposure level for jellyfin.service: 2.4 OKDe 8,2 (peligroso) a 2,4, en la misma línea que el 1,6 de tramontana.service. No baja más por dos motivos legítimos: necesita acceso a /dev/dri para la GPU y .NET requiere memoria ejecutable para su compilador JIT.
ReadOnlyPaths=/srv/medios merece un párrafo. Jellyfin lee la biblioteca; no la escribe. Con esta línea, ni un fallo de la aplicación ni una vulnerabilidad remota pueden borrar veinte años de fotos. Es una defensa de una línea con un valor enorme, y es exactamente el mismo razonamiento que aplicaste a tramontana.service en 05-05.
Y la comprobación de que funciona:
$ sudo -u jellyfin touch /srv/medios/prueba
touch: no se puede efectuar `touch' sobre '/srv/medios/prueba': Sistema de ficheros de sólo lecturaAceleración por hardware para transcodificación
Qué es transcodificar y por qué importa tanto
Cuando el cliente puede reproducir el fichero tal cual, Jellyfin hace reproducción directa: envía los bytes sin tocarlos, con un coste de CPU casi nulo. Cuando no puede —porque el códec, la resolución, el contenedor o el ancho de banda no le sirven—, Jellyfin transcodifica: decodifica el vídeo y lo vuelve a codificar en vuelo.
Transcodificar por software una película 1080p H.265 a H.264 consume varios núcleos al 100 % en tiempo real. En un mini PC de dos núcleos, eso significa que un solo espectador satura la máquina y el vídeo se entrecorta. Con aceleración por hardware, el mismo trabajo lo hace un bloque dedicado del chip con un coste de CPU del 5 %, y se pueden servir tres o cuatro flujos a la vez.
Es la diferencia entre un servidor que funciona y uno que no. Por eso hay que comprobar el hardware antes de comprarlo.
| Tecnología | Fabricante | Calidad | Notas |
|---|---|---|---|
| Quick Sync (VAAPI/QSV) | Intel iGPU | Muy buena | La mejor relación consumo/rendimiento |
| NVENC/NVDEC | NVIDIA | Muy buena | Límite de flujos simultáneos en las de consumo |
| VAAPI sobre AMD | AMD | Buena | Soporte algo más irregular |
| V4L2 M2M | Raspberry Pi 4 | Aceptable | No en la Pi 5 |
| Software (libx264) | CPU | Mejor calidad | Inviable en tiempo real en equipos modestos |
Comprobar si el equipo la tiene
$ sudo apt install vainfo intel-media-va-driver-non-free
# 1. ¿Existe el dispositivo de renderizado?
$ ls -l /dev/dri/
crw-rw---- 1 root video 226, 0 ago 18 09:02 card0
crw-rw---- 1 root render 226, 128 ago 18 09:02 renderD128
# 2. ¿Qué sabe hacer?
$ vainfo --display drm --device /dev/dri/renderD128 2>/dev/null | \
grep -E 'Driver version|VAProfileH264High|VAProfileHEVCMain|VAProfileAV1'
vainfo: Driver version: Intel iHD driver for Intel(R) Gen Graphics - 24.1.0
VAProfileH264High : VAEntrypointVLD (decodifica)
VAProfileH264High : VAEntrypointEncSliceLP (codifica)
VAProfileHEVCMain : VAEntrypointVLD
VAProfileHEVCMain10 : VAEntrypointVLD
VAProfileAV1Profile0 : VAEntrypointVLDCómo se lee: VAEntrypointVLD significa decodificación por hardware; VAEntrypointEncSlice significa codificación. Necesitas ambas para transcodificar de verdad. Este chip decodifica H.264, H.265 (incluido 10 bits) y AV1, y codifica H.264: perfecto para lo que hace falta.
renderD128 pertenece al grupo render, así que el usuario del servicio tiene que estar en él:
$ sudo usermod -aG render,video jellyfin
$ id jellyfin
uid=996(jellyfin) gid=996(jellyfin) grupos=996(jellyfin),44(video),993(render)
$ sudo systemctl restart jellyfinY esto conecta con la unidad endurecida: DeviceAllow=/dev/dri/renderD128 rw es lo que permite que el servicio, con PrivateDevices restringido, siga viendo la GPU. Sin esa línea, la pertenencia al grupo no sirve de nada — un detalle que provoca horas de desconcierto.
Configurar y verificar
En el panel de Jellyfin: Panel > Reproducción > Aceleración por hardware = VAAPI, dispositivo /dev/dri/renderD128, activando la decodificación de H.264, HEVC, HEVC 10 bits y VP9, y marcando «Habilitar decodificación por hardware mejorada» y «Permitir codificación en formato HEVC».
La verificación real se hace reproduciendo algo que fuerce la conversión:
# Con una reproduccion en curso que este transcodificando
$ ps -eo pcpu,comm | grep ffmpeg
6.2 ffmpeg
$ sudo intel_gpu_top -s 1000 | head -8
Freq MHz IRQ RC6 Power W IMC MiB/s
req act % gpu pkg rd wr
850 842 1284 12 3.21 8.94 412.1 188.4
ENGINES BUSY MI_SEM MI_WAIT
Render/3D 4.12% |█ | 0% 0%
Video 78.44% |███████████████ | 0% 0%
VideoEnhance 2.10% | | 0% 0%Video 78,44 % con ffmpeg al 6,2 % de CPU: la GPU está haciendo el trabajo. Si ffmpeg apareciera al 190 % de CPU y el motor de vídeo a 0 %, la aceleración no estaría funcionando pese a estar marcada en el panel.
La comparación, medida:
| Por software | Con VAAPI | |
|---|---|---|
CPU de ffmpeg (1080p H.265→H.264) |
185 % | 6 % |
| Flujos simultáneos posibles | 1, con cortes | 3-4 |
| Consumo eléctrico durante la conversión | +35 W | +7 W |
| Calidad visual a igual tasa de bits | Algo mejor | Ligeramente inferior |
Y la mejor optimización: no transcodificar
Todo lo anterior es la red de seguridad. La estrategia correcta es que la conversión no haga falta:
- Guarda el material en H.264 en contenedor MP4 si tus clientes son variados: es lo que reproduce todo directamente.
- Usa H.265 solo si todos tus dispositivos lo soportan; ahorra la mitad de espacio.
- Los subtitulos incrustados fuerzan la transcodificación aunque el vídeo sea compatible: los formatos gráficos (PGS de Blu-ray) obligan a «quemarlos» sobre la imagen. Los subtítulos en
.srtexternos se envían aparte y no fuerzan nada. Es la causa número uno de conversiones inesperadas. - El audio suele ser el culpable oculto: un DTS-HD que el televisor no entiende obliga a convertir la pista de audio, aunque el vídeo pase directo.
Organización de la biblioteca y nombres de fichero
Jellyfin identifica el contenido consultando bases de datos públicas de metadatos, y para acertar necesita nombres que pueda interpretar. Con la estructura correcta, el reconocimiento ronda el 99 %; con nombres libres, pasas horas corrigiendo a mano.
/srv/medios/
├── peliculas/
│ ├── El Verdugo (1963)/
│ │ ├── El Verdugo (1963) - 1080p.mkv
│ │ ├── El Verdugo (1963).es.srt
│ │ └── poster.jpg
│ └── Metropolis (1927)/
│ └── Metropolis (1927) - 1080p.mkv
├── series/
│ └── Nombre de la Serie (2019)/
│ ├── Season 01/
│ │ ├── Nombre de la Serie S01E01.mkv
│ │ └── Nombre de la Serie S01E02.mkv
│ └── Season 02/
│ └── Nombre de la Serie S02E01.mkv
├── musica/
│ └── Artista/
│ └── Album (2004)/
│ ├── 01 - Primera cancion.flac
│ └── 02 - Segunda cancion.flac
└── fotos/
└── 2026/
└── 2026-07 Vacaciones Pirineos/| Regla | Ejemplo correcto | Ejemplo problemático |
|---|---|---|
| Una carpeta por película, con año | El Verdugo (1963)/ |
peliculas/verdugo.mkv |
| Año entre paréntesis | Metropolis (1927) |
Metropolis 1927 |
Series con SxxExx |
Serie S01E03.mkv |
Serie 1x3.mkv |
Carpetas Season NN |
Season 01/ |
Temporada 1/ |
| Subtítulos con código de idioma | pelicula.es.srt |
subs.srt |
| Música: carpeta por artista y álbum | Artista/Album (2004)/ |
Todo en un directorio |
El año es lo más importante: distingue entre remakes y películas con el mismo título, que es el origen de la mayoría de las identificaciones erróneas.
Y una recomendación práctica: no renombres a mano. filebot (comercial) o tinyMediaManager (gratuito) hacen el trabajo. O, con las herramientas del Módulo 3:
# Ver que haria antes de hacerlo (convencion --dry-run del curso)
$ for f in /srv/medios/entrada/*.mkv; do
nuevo="$(basename "$f" | sed -E 's/\.(1080p|720p|x264|WEB-DL)//gI; s/\./ /g')"
printf '%s\n -> %s\n' "$f" "$nuevo"
donePermisos, siguiendo el modelo de grupos de 05-01:
$ sudo groupadd -f medios
$ sudo usermod -aG medios jellyfin
$ sudo usermod -aG medios "$USER"
$ sudo chown -R root:medios /srv/medios
$ sudo find /srv/medios -type d -exec chmod 2775 {} + # SGID: hereda grupo
$ sudo find /srv/medios -type f -exec chmod 0664 {} +El bit SGID en los directorios (el 2 de 2775) hace que todo lo creado dentro herede el grupo medios, sin depender del umask de quien lo cree. Es el mismo mecanismo de 05-02.
Compartir: Samba para todo, NFS para Linux
Jellyfin sirve la reproducción, pero también hace falta acceso a los ficheros: copiar fotos desde el móvil, añadir contenido desde el portátil, hacer copias.
| Samba (SMB) | NFS | |
|---|---|---|
| Clientes | Windows, macOS, Linux, Android, iOS, TV | Linux y Unix, principalmente |
| Autenticación | Usuario y contraseña propios | Por IP de la máquina (NFSv3) o Kerberos (v4) |
| Rendimiento en LAN | Muy bueno | Algo mejor, menor latencia |
| Permisos Unix | Emulados | Nativos |
| Configuración | Media | Simple, si te fías de la red |
| Cifrado | Sí, SMB3 | Solo con Kerberos o sobre VPN |
| Uso doméstico | La opción por defecto | Para montar en otros Linux |
La recomendación: Samba como base, porque un móvil, un televisor y un Windows lo hablan sin instalar nada, y NFS solo si tienes clientes Linux que vayan a montar la biblioteca permanentemente.
# /etc/samba/smb.conf (tras copiar el original a .bak-2026-08-18)
[global]
workgroup = CASA
server string = Servidor de medios
security = user
map to guest = never # NADA de invitados
# SMB1 esta roto por diseno (WannaCry se propago por ahi).
# Minimo SMB2; SMB3 aporta cifrado.
server min protocol = SMB2
client min protocol = SMB2
server smb encrypt = desired
# Escuchar SOLO en la LAN y la VPN, nunca en todas las interfaces
interfaces = 127.0.0.1 192.168.1.0/24 10.8.0.0/24
bind interfaces only = yes
hosts allow = 127.0.0.1 192.168.1.0/24 10.8.0.0/24
hosts deny = 0.0.0.0/0
# Sin impresoras: elimina superficie de ataque y ruido en el registro
load printers = no
printing = bsd
printcap name = /dev/null
disable spoolss = yes
log level = 1
log file = /var/log/samba/log.%m
max log size = 1000
[medios]
path = /srv/medios
comment = Biblioteca familiar
browseable = yes
read only = yes # por defecto, SOLO LECTURA
# Solo estos usuarios pueden escribir
write list = @medios
valid users = @medios
create mask = 0664
directory mask = 2775
force group = medios
vfs objects = recycle # papelera: salva de borrados
recycle:repository = .papelera
recycle:keeptree = yes
recycle:versions = yes
[fotos]
path = /srv/medios/fotos
valid users = @medios
read only = no
create mask = 0664
directory mask = 2775
force group = medios$ sudo apt install samba
$ testparm -s >/dev/null && echo "configuracion valida" # el nginx -t de Samba
configuracion valida
# Los usuarios de Samba son INDEPENDIENTES de los del sistema, aunque
# el nombre coincida. Hay que crearlos explicitamente.
$ sudo smbpasswd -a marta
New SMB password: ********
$ sudo smbpasswd -e marta
$ sudo systemctl restart smbd nmbd
$ smbclient -L //192.168.1.20 -U marta
Sharename Type Comment
--------- ---- -------
medios Disk Biblioteca familiar
fotos DiskTres decisiones defendibles de esa configuración: map to guest = never (el acceso anónimo es la causa de la mayoría de los incidentes con Samba), read only = yes por defecto con write list explícito, y la papelera recycle, que ha salvado más fotos familiares que cualquier otra opción de este fichero.
Y NFS, para el portátil Linux:
$ sudo apt install nfs-kernel-server
$ echo '/srv/medios 192.168.1.0/24(ro,sync,no_subtree_check,root_squash)' | \
sudo tee -a /etc/exports
$ sudo exportfs -ra
$ sudo exportfs -v
/srv/medios 192.168.1.0/24(ro,sync,no_subtree_check,root_squash)root_squash convierte al root del cliente en nobody: sin ella, cualquiera con root en un portátil de la red tendría root sobre tus ficheros. Y ro, porque para escribir ya está Samba con autenticación real.
Y el cortafuegos, siguiendo 06-03 — solo la LAN:
$ sudo ufw allow from 192.168.1.0/24 to any app Samba
$ sudo ufw allow from 192.168.1.0/24 to any port 8096 proto tcp comment 'Jellyfin'
$ sudo ufw allow from 192.168.1.0/24 to any port 2049 proto tcp comment 'NFS'
$ sudo ufw status | head -6
Estado: activo
Hasta Acción Desde
----- ------ -----
Samba ALLOW 192.168.1.0/24
8096/tcp (Jellyfin) ALLOW 192.168.1.0/24
2049/tcp (NFS) ALLOW 192.168.1.0/24Acceso desde fuera de casa: tres opciones y su riesgo
Quieres ver tu biblioteca desde un hotel. Hay tres formas, y no son equivalentes.
| Exponer con proxy inverso | VPN | Túnel inverso | |
|---|---|---|---|
| Puertos abiertos en el router | 80 y 443 | Uno UDP | Ninguno |
| Superficie de ataque | Jellyfin, expuesto a Internet | El túnel, sin puerto TCP | Depende del proveedor |
| Requiere IP pública | Sí | Sí | No: funciona con CGNAT |
| Configurar en cada dispositivo | No | Sí, una vez | Depende |
| Un fallo de Jellyfin te compromete | Sí | No: no es alcanzable | Sí |
| Acceso a otros servicios de casa | Solo el publicado | Todos | Solo el publicado |
| Complejidad | Media (08-01) | Media (08-04) | Baja |
| Dependencia de terceros | Ninguna | Ninguna | Alta |
La recomendación es la VPN, y es la lección siguiente. El razonamiento:
Exponer Jellyfin a Internet significa que cualquier vulnerabilidad de Jellyfin es una vulnerabilidad de tu casa. Es una aplicación grande, con autenticación propia y una historia de fallos de seguridad como cualquier software de su tamaño. Los escáneres automáticos encuentran un puerto 443 abierto en cuestión de horas. Con la VPN, un atacante que no tenga tu clave privada no puede ni siquiera comprobar si Jellyfin existe.
Y hay un argumento adicional que suele decidir la cuestión: con la VPN accedes también a Samba, al panel del router y a cualquier otra cosa que montes después, sin publicar nada nuevo.
Si aun así decides exponerlo —hay un caso legítimo: compartir con familiares que no van a instalar una VPN—, el mínimo innegociable es:
# /etc/nginx/sites-available/jellyfin — solo si decides exponerlo
server {
listen 443 ssl;
http2 on;
server_name medios.midominio.example;
ssl_certificate /etc/letsencrypt/live/medios.midominio.example/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/medios.midominio.example/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
add_header Strict-Transport-Security "max-age=63072000" always;
add_header X-Content-Type-Options "nosniff" always;
# Limite de tasa contra fuerza bruta sobre el formulario de acceso
limit_req zone=login burst=5 nodelay;
client_max_body_size 20M;
location / {
proxy_pass http://127.0.0.1:8096;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket: Jellyfin lo usa para el estado de reproduccion
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Sin buffering: es video en streaming
proxy_buffering off;
}
}Es literalmente la configuración de 08-01 con dos añadidos —WebSocket y proxy_buffering off— y demuestra el punto de esta lección: el trabajo es el mismo. Además, fail2ban con un filtro para el registro de Jellyfin, y contraseñas fuertes para todas las cuentas, incluida la del niño.
Descargas y automatización
Brevemente, porque es un área donde la técnica y la legalidad se cruzan.
Existe un ecosistema conocido como arr —Sonarr, Radarr, Lidarr, Prowlarr— que automatiza la búsqueda, descarga y organización de contenido. Se despliega habitualmente con Docker Compose, con lo que sabes de 07-05:
# ~/medios/compose.yml (fragmento ilustrativo)
services:
sonarr:
image: lscr.io/linuxserver/sonarr:latest
environment:
- PUID=1000
- PGID=1002 # grupo medios
- TZ=Europe/Madrid
volumes:
- ./sonarr-config:/config
- /srv/medios/series:/series
ports:
- "127.0.0.1:8989:8989" # SOLO localhost: se llega por VPN
restart: unless-stoppedDos observaciones aplicables independientemente del uso:
ports: "127.0.0.1:8989:8989"es el detalle crítico. Docker crea sus propias reglas deiptablesy se saltaufw, como viste en 07-05: publicar8989:8989a secas expone el puerto a toda la red aunqueufwdiga lo contrario. Especificar la dirección lo evita.- Estas herramientas son perfectamente legítimas para gestionar contenido propio, sustituir un fichero dañado por tu propia copia de seguridad, o descargar material con licencia libre. Su uso para obtener obras protegidas no lo es, y este curso no cubre esa parte.
Un uso doméstico útil y sin ambigüedad: la copia automática de las fotos del móvil. Immich o Nextcloud, en Docker, sustituyen el servicio de fotos en la nube por uno propio, y son quizá el mejor argumento para tener un servidor en casa.
Copias de seguridad de la biblioteca y de la configuración
Aquí se aplica exactamente 05-08, y hay que hacer una distinción que ahorra dinero:
| Contenido | Insustituible | Estrategia |
|---|---|---|
| Fotos y vídeos familiares | Sí, absolutamente | 3-2-1 completo, fuera de casa |
| Documentos escaneados | Sí | 3-2-1 completo |
| Configuración de Jellyfin (usuarios, vistos, listas) | Costosa de rehacer | Copia diaria, pequeña |
| Música ripeada de CD propios | Recuperable con esfuerzo | Copia local; opcional fuera |
| Películas de discos propios | Recuperable con mucho esfuerzo | Solo RAID; copia si sobra espacio |
Copiar 6 TB de películas a la nube cuesta dinero cada mes y no aporta gran cosa, porque los discos originales siguen en la estantería. Copiar 400 GB de fotos familiares fuera de casa es innegociable, y cuesta poco. Distinguirlo es la decisión que hace el proyecto sostenible.
#!/usr/bin/env bash
#
# respaldo_casa.sh - Copias del servidor de medios domestico
#
# Estrategia:
# - Insustituible (fotos, documentos) -> restic fuera de casa
# - Configuracion de Jellyfin -> restic, diario
# - Peliculas y musica -> instantanea Btrfs local
#
set -euo pipefail
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/lib/comunes.sh"
readonly CASA_MEDIOS="${CASA_MEDIOS:-/srv/medios}"
readonly CASA_SNAP="${CASA_SNAP:-/srv/medios/.instantaneas}"
export RESTIC_REPOSITORY="${RESTIC_REPOSITORY:-b2:midominio-medios:/}"
umask 077
instantanea_local() {
local nombre="${CASA_SNAP}/$(date +%F)"
[[ -d "$nombre" ]] && { log "instantanea de hoy ya existe"; return 0; }
# Instantanea de solo lectura: no ocupa espacio hasta que algo cambia
btrfs subvolume snapshot -r "$CASA_MEDIOS" "$nombre" \
|| morir 73 "no se pudo crear la instantanea"
log "instantanea creada: $nombre"
# Retencion: conservar 7 diarias
local sobrantes
mapfile -t sobrantes < <(find "$CASA_SNAP" -maxdepth 1 -type d -name '20*' \
| sort -r | tail -n +8)
local s
for s in "${sobrantes[@]:-}"; do
[[ -n "$s" ]] || continue
btrfs subvolume delete "$s" && log "instantanea antigua eliminada: $s"
done
}
respaldo_externo() {
requiere_comando restic
RESTIC_PASSWORD="$(pass casa/restic)" \
restic backup \
"${CASA_MEDIOS}/fotos" \
"${CASA_MEDIOS}/documentos" \
/var/lib/jellyfin \
/etc/jellyfin \
--exclude-caches \
--exclude '*/metadata/*' \
--tag casa \
|| morir 74 "restic fallo"
RESTIC_PASSWORD="$(pass casa/restic)" \
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --prune
}
verificar() {
# Una copia no verificada no es una copia (05-08)
RESTIC_PASSWORD="$(pass casa/restic)" \
restic check --read-data-subset=2% || morir 74 "la verificacion de restic fallo"
log "verificacion correcta"
}
main() {
requiere_comando btrfs
instantanea_local
respaldo_externo
verificar
log "respaldo completado"
}
main "$@"Las instantáneas de Btrfs merecen una explicación, porque son la protección contra el error humano que el RAID no da. Una instantánea es una copia instantánea del estado actual que no ocupa espacio: comparte todos los bloques con el original y solo diverge cuando algo cambia. Si mañana borras una carpeta de fotos por error, está en /srv/medios/.instantaneas/2026-08-18/ sin haber costado un byte.
$ sudo btrfs subvolume list /srv/medios
ID 258 gen 4412 top level 5 path .instantaneas/2026-08-16
ID 259 gen 4488 top level 5 path .instantaneas/2026-08-17
ID 260 gen 4501 top level 5 path .instantaneas/2026-08-18
# Recuperar algo borrado por error: es una simple copia
$ sudo cp -a /srv/medios/.instantaneas/2026-08-17/fotos/2026-07 /srv/medios/fotos/Y el aviso: una instantánea no es una copia de seguridad. Vive en los mismos discos. Protege del error humano, no del fallo del hardware ni del incendio. Por eso el script hace las dos cosas.
Consumo, ruido y temperatura
Tres factores que en un centro de datos son problema de otro y en el salón de tu casa son problema tuyo.
El coste eléctrico anual
| Equipo | Potencia media | kWh/año | Coste a 0,24 €/kWh |
|---|---|---|---|
| Raspberry Pi 5 + 1 SSD | 7 W | 61 | 15 € |
| Mini PC N100 + 2 HDD (parados) | 13 W | 114 | 27 € |
| Mini PC N100 + 2 HDD (activos) | 24 W | 210 | 50 € |
| NAS de 4 bahías | 28 W | 245 | 59 € |
| PC reutilizado | 65 W | 569 | 137 € |
| Servidor de segunda mano | 110 W | 964 | 231 € |
La diferencia entre el mini PC y el PC reutilizado son 110 € al año. Ese es el argumento del apartado 3 convertido en dinero.
$ sudo apt install powertop lm-sensors
$ sudo sensors-detect --auto >/dev/null
# Calibrar primero (tarda unos minutos y mide el consumo de cada estado)
$ sudo powertop --calibrate
$ sudo powertop --auto-tune # aplica los ajustes recomendados
$ sudo powertop --time=60 --csv=/tmp/energia.csv >/dev/null
$ grep -A6 'Power est' /tmp/energia.csv | head -8
The battery reports a discharge rate of 12.4 W
Usage Events/s Category Description
6.2 ms/s 14.1 Proceso ffmpeg
1.1 ms/s 8.4 Proceso jellyfinpowertop --auto-tune activa el ahorro de energía de USB, PCIe y SATA. Un aviso: puede desactivar puertos USB que estén en uso ocasional. Para hacerlo persistente, un servicio de systemd, no un rc.local:
# /etc/systemd/system/powertop.service
[Unit]
Description=Ajustes de ahorro energetico de PowerTOP
After=multi-user.target
[Service]
Type=oneshot
ExecStart=/usr/sbin/powertop --auto-tune
RemainAfterExit=true
[Install]
WantedBy=multi-user.targetApagar los discos cuando no se usan
Un HDD de 3,5 pulgadas consume 6-9 W girando y 0,5 W parado. Con dos discos, apagarlos cuando nadie ve nada ahorra unos 30 € al año — y, sobre todo, reduce el ruido de fondo.
$ sudo apt install hdparm
# -B 127: gestion de energia agresiva pero SIN aparcar el cabezal a cada
# rato (los valores < 128 permiten parada; 254 la desactiva)
# -S 120: parar tras 120 x 5 s = 10 minutos de inactividad
$ sudo hdparm -B 127 -S 120 /dev/sda# /etc/hdparm.conf — persistente entre reinicios
/dev/disk/by-id/ata-WDC_WD80EFPX-68C4ZN0_WD-CA0J1234 {
apm = 127
spindown_time = 120
}El compromiso hay que entenderlo: cada arranque desde parado desgasta el disco más que varias horas girando, y añade 5-8 segundos de espera al abrir la biblioteca. Con un tiempo de parada de 10 minutos y uso normal por las tardes, el balance es favorable. Con 5 minutos y alguien que navega la biblioteca a ratos, el disco arranca y para veinte veces por noche, y eso acorta su vida. Nunca bajes de 10 minutos.
Se usa /dev/disk/by-id/ y no /dev/sda por la misma razón que los UUID en fstab: los nombres sdX cambian de orden entre arranques.
Temperatura
$ sensors
coretemp-isa-0000
Package id 0: +42.0°C (high = +100.0°C, crit = +100.0°C)
$ sudo smartctl -A /dev/sda | grep -i temperature
194 Temperature_Celsius 0x0022 118 105 000 Old_age Always - 38| Componente | Ideal | Aceptable | Preocupante |
|---|---|---|---|
| CPU en reposo | < 45 °C | < 60 °C | > 75 °C |
| CPU transcodificando | < 70 °C | < 85 °C | > 90 °C |
| HDD | 30-40 °C | 25-45 °C | > 50 °C o < 20 °C |
| SSD NVMe | < 55 °C | < 70 °C | > 75 °C |
Los estudios de fiabilidad a gran escala son claros: los discos duros duran menos por encima de 45 °C y también por debajo de 20 °C. En un armario cerrado del salón, sin ventilación, dos discos superan los 50 °C con facilidad. Un ventilador de 120 mm a bajas revoluciones es prácticamente inaudible y baja 8-10 °C.
Automatización con Ansible
# ~/casa-infra/roles/medios/defaults/main.yml
---
medios_punto_montaje: /srv/medios
medios_uuid: 8f3a2c11-4d5e-4a91-b7c2-1e9d0f8a3b45
medios_grupo: medios
medios_gid: 1002
medios_red_lan: 192.168.1.0/24
medios_red_vpn: 10.8.0.0/24
medios_usuarios_samba: [marta, luis]
medios_acelerar_hw: true
medios_dispositivo_gpu: /dev/dri/renderD128
medios_hdparm_spindown: 120
medios_subdirectorios: [peliculas, series, musica, fotos, documentos]# ~/casa-infra/roles/medios/tasks/main.yml
---
- name: Instalar paquetes base
ansible.builtin.apt:
name:
- btrfs-progs
- smartmontools
- samba
- hdparm
- powertop
- restic
state: present
update_cache: true
tags: [paquetes]
- name: Paquetes de aceleracion por hardware
ansible.builtin.apt:
name: [vainfo, intel-media-va-driver-non-free]
state: present
when: medios_acelerar_hw | bool
tags: [gpu]
- name: Clave del repositorio de Jellyfin en keyrings
ansible.builtin.get_url:
url: https://repo.jellyfin.org/jellyfin_team.gpg.key
dest: /etc/apt/keyrings/jellyfin.asc
mode: '0644'
tags: [paquetes]
- name: Repositorio de Jellyfin
ansible.builtin.deb822_repository:
name: jellyfin
types: deb
uris: https://repo.jellyfin.org/ubuntu
suites: "{{ ansible_distribution_release }}"
components: main
architectures: amd64
signed_by: /etc/apt/keyrings/jellyfin.asc
register: repo_jellyfin
tags: [paquetes]
- name: Instalar Jellyfin
ansible.builtin.apt:
name: jellyfin
state: present
update_cache: "{{ repo_jellyfin.changed }}"
tags: [paquetes]
# --- Almacenamiento ---
- name: Comprobar que el sistema de ficheros existe antes de montarlo
ansible.builtin.command: "blkid -U {{ medios_uuid }}"
register: blk
changed_when: false
failed_when: blk.rc != 0
- name: Montar la biblioteca por UUID con nofail
ansible.posix.mount:
path: "{{ medios_punto_montaje }}"
src: "UUID={{ medios_uuid }}"
fstype: btrfs
opts: defaults,compress=zstd:3,noatime,nofail,x-systemd.device-timeout=10
state: mounted
- name: Grupo de la biblioteca
ansible.builtin.group:
name: "{{ medios_grupo }}"
gid: "{{ medios_gid }}"
state: present
- name: Estructura de directorios con SGID
ansible.builtin.file:
path: "{{ medios_punto_montaje }}/{{ item }}"
state: directory
owner: root
group: "{{ medios_grupo }}"
mode: '2775'
loop: "{{ medios_subdirectorios }}"
- name: Anadir jellyfin a los grupos necesarios
ansible.builtin.user:
name: jellyfin
groups: "{{ [medios_grupo] + (['render', 'video'] if medios_acelerar_hw else []) }}"
append: true
notify: Reiniciar jellyfin
- name: Drop-in de endurecimiento de jellyfin
ansible.builtin.template:
src: jellyfin-override.conf.j2
dest: /etc/systemd/system/jellyfin.service.d/override.conf
owner: root
group: root
mode: '0644'
notify:
- Recargar systemd
- Reiniciar jellyfin
- name: Configuracion de Samba
ansible.builtin.template:
src: smb.conf.j2
dest: /etc/samba/smb.conf
owner: root
group: root
mode: '0644'
backup: true
validate: 'testparm -s %s' # el nginx -t de Samba (08-01)
notify: Reiniciar samba
- name: Cortafuegos — solo LAN y VPN
community.general.ufw:
rule: allow
src: "{{ item.0 }}"
port: "{{ item.1 }}"
proto: tcp
loop: "{{ [medios_red_lan, medios_red_vpn] | product(['445', '8096']) | list }}"
tags: [firewall]
- name: Timers de barrido, SMART y respaldo
ansible.builtin.copy:
src: "{{ item }}"
dest: "/etc/systemd/system/{{ item }}"
mode: '0644'
loop:
- btrfs-scrub.service
- btrfs-scrub.timer
- respaldo-casa.service
- respaldo-casa.timer
notify: Recargar systemd
- name: Activar los timers
ansible.builtin.systemd:
name: "{{ item }}"
enabled: true
state: started
daemon_reload: true
loop: [btrfs-scrub.timer, respaldo-casa.timer, smartd.service]
# --- Verificacion ---
- name: Verificar que la aceleracion por hardware es visible
ansible.builtin.command: "vainfo --display drm --device {{ medios_dispositivo_gpu }}"
register: va
changed_when: false
failed_when: "'VAProfileH264High' not in va.stdout"
when: medios_acelerar_hw | bool
tags: [verificar]
- name: Verificar que Jellyfin responde
ansible.builtin.uri:
url: "http://127.0.0.1:8096/health"
status_code: 200
retries: 5
delay: 3
tags: [verificar]Ese validate: 'testparm -s %s' es el mismo patrón que nginx -t -c %s en 08-01, y protege del mismo problema: una configuración inválida que impide arrancar el servicio. El patrón se repite porque es correcto, no porque sea una casualidad.
Operación: actualizaciones, salud de discos y qué hacer cuando uno falla
Actualizaciones
# Actualizaciones de seguridad desatendidas (06-06)
$ sudo apt install unattended-upgrades
$ sudo dpkg-reconfigure -plow unattended-upgradesCon una diferencia respecto a srv-tramontana: aquí sí conviene permitir el reinicio automático, porque no hay ventana de mantenimiento que negociar ni nadie a quien avisar.
# /etc/apt/apt.conf.d/50unattended-upgrades
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "05:30";
Unattended-Upgrade::Automatic-Reboot-WithUsers "false";Automatic-Reboot-WithUsers "false" evita reiniciar si hay alguien conectado — pero no detecta a alguien viendo una película, así que las 05:30 son una elección deliberada.
Jellyfin se actualiza con apt como cualquier otro paquete. Antes de una actualización mayor, instantánea:
$ sudo btrfs subvolume snapshot -r /var/lib/jellyfin \
/srv/medios/.instantaneas/jellyfin-pre-$(date +%F)Salud de los discos
$ sudo systemctl enable --now smartd
# Prueba corta semanal, larga mensual, y aviso por correo
$ sudo tee /etc/smartd.conf <<'EOF'
DEVICESCAN -a -o on -S on -n standby,q \
-s (S/../.././02|L/../(01|15)/./03) \
-W 4,45,50 \
-m root -M exec /usr/share/smartmontools/smartd-runner
EOF
$ sudo systemctl restart smartd| Parámetro | Significado |
|---|---|
-n standby,q |
No despertar un disco parado para comprobarlo |
-s (S/../.././02|L/../(01|15)/./03) |
Prueba corta diaria a las 02:00; larga los días 1 y 15 a las 03:00 |
-W 4,45,50 |
Avisar si sube 4 °C de golpe, si pasa de 45 °C, crítico a 50 °C |
Los atributos que de verdad predicen un fallo, según los estudios de fiabilidad a gran escala:
$ sudo smartctl -A /dev/sda | \
awk '$1 ~ /^(5|187|188|197|198)$/ {printf "%-28s %s\n", $2, $10}'
Reallocated_Sector_Ct 0
Reported_Uncorrect 0
Command_Timeout 0
Current_Pending_Sector 0
Offline_Uncorrectable 0| Atributo | Qué indica | Umbral de acción |
|---|---|---|
| 5 Reallocated_Sector_Ct | Sectores dañados reasignados | > 0: vigilar. Creciendo: cambiar |
| 197 Current_Pending_Sector | Sectores sospechosos sin reasignar | > 0: cambiar pronto |
| 198 Offline_Uncorrectable | Sectores ilegibles | > 0: cambiar ya |
| 187 Reported_Uncorrect | Errores no corregibles | > 0: vigilar |
| 188 Command_Timeout | Órdenes agotadas | Suele ser el cable, no el disco |
Un disco con Current_Pending_Sector creciendo está muriendo, aunque SMART diga PASSED. El veredicto global de SMART es notoriamente optimista: mira los atributos, no el resumen.
Cuando un disco falla
# El sintoma: errores en el journal
$ sudo journalctl -k --since today | grep -iE 'ata[0-9]|I/O error|medium error'
kernel: ata2.00: exception Emask 0x0 SAct 0x0 SErr 0x0 action 0x0
kernel: blk_update_request: I/O error, dev sdb, sector 1928374656
$ sudo btrfs device stats /srv/medios
[/dev/sda].write_io_errs 0
[/dev/sda].read_io_errs 0
[/dev/sda].corruption_errs 0
[/dev/sdb].write_io_errs 142
[/dev/sdb].read_io_errs 2891
[/dev/sdb].corruption_errs 18Procedimiento de sustitución con Btrfs RAID 1:
# 1. NO desmontar ni apagar. Con RAID 1 el sistema sigue funcionando.
# Primero, comprobar que hay copia de lo insustituible.
$ RESTIC_PASSWORD=$(pass casa/restic) restic snapshots --tag casa | tail -3
# 2. Si el disco AUN responde: reemplazo en caliente (lo ideal).
# Btrfs copia del disco viejo lo que puede y del espejo lo que no.
$ sudo btrfs replace start -f /dev/sdb /dev/sdc /srv/medios
$ sudo btrfs replace status /srv/medios
0.8% done, 0 write errs, 0 uncorr. read errs
# 3. Si el disco YA NO responde: montar en modo degradado y anadir uno nuevo
$ sudo mount -o degraded,compress=zstd:3 UUID=8f3a2c11-... /srv/medios
$ sudo btrfs device add /dev/sdc /srv/medios
$ sudo btrfs device remove missing /srv/medios
$ sudo btrfs balance start -dconvert=raid1 -mconvert=raid1 /srv/medios
# 4. Verificar que se ha restablecido la redundancia
$ sudo btrfs filesystem df /srv/medios
Data, RAID1: total=1.01TiB, used=1.00TiB
Metadata, RAID1: total=8.00GiB, used=6.12GiB
# 5. Barrido completo para confirmar que todo esta integro
$ sudo btrfs scrub start -B /srv/mediosTres avisos sobre este procedimiento:
btrfs replacees preferible aadd+remove: es más rápido y mantiene la redundancia durante más tiempo del proceso.- El modo degradado tiene un límite: con RAID 1 de dos discos, un segundo fallo durante la reconstrucción lo pierde todo. Es el momento de mayor riesgo del ciclo de vida del conjunto, y la razón por la que las copias externas existen.
- Sustituye el disco cuando SMART avisa, no cuando falla. Un reemplazo en caliente sobre un disco que aún lee es un trámite; una reconstrucción desde un disco muerto es un riesgo.
La rutina doméstica
| Frecuencia | Tarea | Herramienta |
|---|---|---|
| Automático | Actualizaciones de seguridad | unattended-upgrades |
| Automático | Copia diaria de lo insustituible | respaldo_casa.sh + timer |
| Automático | Instantánea diaria | Btrfs |
| Diario | Prueba SMART corta | smartd |
| Semanal (2 min) | Revisar avisos y espacio | btrfs filesystem usage |
| Mensual (10 min) | Barrido de Btrfs y prueba SMART larga | Timers |
| Trimestral (20 min) | Restaurar un fichero de la copia | restic restore |
| Anual | Revisar polvo, ventiladores y temperaturas | Físicamente |
La fila trimestral es la misma de 08-02 y por la misma razón: una copia que nunca se ha restaurado es un fichero del que suponemos cosas. En casa se incumple todavía más que en el trabajo.
Errores Comunes y Consejos
- Creer que el RAID es una copia de seguridad. Protege del fallo de un disco y de nada más: ni de borrados, ni de cifradores, ni de incendios.
- Usar Btrfs en RAID 5 o 6. Siguen considerándose inestables. RAID 1, o
mdadmsi necesitas paridad. - Montar sin
nofail. Un disco desconectado deja el equipo en modo emergencia, sin red ni SSH, y hay que ir con teclado y monitor. - Montar por
/dev/sdX. El orden cambia entre arranques. Siempre UUID. - No comprobar la aceleración por hardware antes de comprar. Es lo que decide si el servidor sirve. La Pi 5 no tiene decodificador H.265.
- Olvidar
DeviceAllowen la unidad endurecida. El usuario está en el gruporendery aun así no ve la GPU, y el desconcierto dura horas. - No poner
ReadOnlyPathssobre la biblioteca. Es una línea que impide que un fallo de la aplicación borre veinte años de fotos. - Permitir invitados en Samba.
map to guest = never. El acceso anónimo es la causa de la mayoría de los incidentes. - Dejar SMB1 activo. Está roto por diseño; es por donde se propagó WannaCry.
- NFS sin
root_squash. Cualquiera con root en un portátil de la red tiene root sobre tus ficheros. - Publicar puertos con Docker creyendo que
ufwlos tapa. Docker escribe sus propias reglas y se las salta. Usa127.0.0.1:puerto:puerto. - Exponer Jellyfin directamente a Internet. Una vulnerabilidad suya se convierte en una vulnerabilidad de tu casa. Usa la VPN de 08-04.
- Poner el tiempo de parada de los discos por debajo de 10 minutos. Arrancan y paran veinte veces por noche, y eso los mata antes.
- Fiarse del veredicto
PASSEDde SMART. Es optimista. Mira los atributos 5, 197 y 198. - Meter el servidor en un armario cerrado. Por encima de 45 °C los discos duran significativamente menos.
- Copiar 6 TB de películas a la nube. Cuesta dinero cada mes y los discos originales siguen en la estantería. Copia lo insustituible.
- Consejo de método. Trata este servidor con el mismo rigor que el del trabajo —Ansible, copias verificadas, unidades endurecidas— y ganarás dos cosas: un servidor que dura, y un laboratorio donde practicar sin riesgo.
Ejercicios
Ejercicio 1
Un familiar se queja de que las películas «se cortan» en el televisor del salón, pero se ven perfectamente en la tableta. Diagnostica el problema de principio a fin y resuélvelo.
Ejercicio 2
Diseña la estrategia de copias completa para un servidor doméstico con 6 TB ocupados: 400 GB de fotos y documentos, 800 GB de música ripeada de CD propios y 4,8 TB de películas de discos comprados. Justifica cada decisión con su coste.
Ejercicio 3
Escribe un script revision_casa.sh con las convenciones del curso que compruebe la salud del servidor doméstico y devuelva 0, 1 o 2.
Soluciones
Solución 1
Diagnóstico paso a paso. El dato de partida es valioso: el mismo contenido va bien en un dispositivo y mal en otro, luego el problema no está en el fichero ni en el disco, sino en la relación entre el fichero y ese cliente concreto.
# 1. ¿Esta transcodificando? El panel de Jellyfin lo dice, y tambien esto:
$ ps -eo pid,pcpu,args | grep '[f]fmpeg' | head -1
8814 191.2 /usr/lib/jellyfin-ffmpeg/ffmpeg -i /srv/medios/peliculas/...
-c:v libx264 -preset veryfast -b:v 3000000 ...191,2 % de CPU y -c:v libx264: está transcodificando por software, saturando los dos núcleos. Ahí está la causa inmediata de los cortes.
# 2. ¿Por que transcodifica? Analizar el fichero
$ ffprobe -v error -show_entries stream=index,codec_type,codec_name,width,height,channels \
-of csv=p=0 "/srv/medios/peliculas/Pelicula (2019)/Pelicula (2019).mkv"
0,video,hevc,3840,2160
1,audio,dts,8
2,subtitle,hdmv_pgs_subtitle
# 3. ¿Que soporta el televisor? En el registro de Jellyfin, tras reproducir:
$ sudo journalctl -u jellyfin --since "10 min ago" | grep -i 'transcod\|reason'
[INF] Transcoding reason: VideoCodecNotSupported, AudioCodecNotSupported,
SubtitleCodecNotSupportedTres motivos acumulados, y hay que atacarlos por separado:
| Motivo | Detalle | Solución |
|---|---|---|
VideoCodecNotSupported |
HEVC 4K; el televisor solo hace H.264 1080p | Aceleración por hardware o segunda versión |
AudioCodecNotSupported |
DTS 7.1; el televisor hace AC3 5.1 | Transcodificar solo el audio (barato) |
SubtitleCodecNotSupported |
PGS: subtítulo gráfico que hay que quemar sobre la imagen | Extraer a .srt externo |
Y la comprobación que descarta el otro sospechoso habitual:
# 4. ¿Es la red? Wi-Fi del salon frente a cable
$ iperf3 -c 192.168.1.45 -t 10 -R
[ 5] 0.00-10.00 sec 58.2 MBytes 48.8 Mbits/sec48,8 Mbit/s bastan para un flujo de 1080p (unos 10 Mbit/s) pero no para 4K sin convertir (40-80 Mbit/s). Así que la red es un factor secundario real: aunque arreglásemos los códecs, el 4K por ese Wi-Fi iría justo.
Resolución, en cuatro medidas ordenadas por relación beneficio/esfuerzo:
# --- MEDIDA 1: activar la aceleracion por hardware (impacto inmediato) ---
$ vainfo --display drm --device /dev/dri/renderD128 | grep -c EncSlice
6
$ sudo usermod -aG render,video jellyfin
$ sudo systemctl restart jellyfin
# Y en el panel: Reproduccion > VAAPI, /dev/dri/renderD128,
# marcando HEVC, HEVC 10 bits, VP9 y "codificacion HEVC"
# Verificar el efecto
$ ps -eo pcpu,args | grep '[f]fmpeg' | awk '{print $1}'
5.8
$ sudo intel_gpu_top -s 1000 | grep Video
Video 74.12% |██████████████ |De 191 % de CPU a 5,8 %. Los cortes desaparecen inmediatamente.
# --- MEDIDA 2: extraer los subtitulos a fichero externo ---
# Los PGS graficos fuerzan la transcodificacion de VIDEO aunque el
# codec sea compatible: hay que "quemarlos" sobre la imagen. Los .srt
# se envian aparte y el cliente los dibuja.
$ ffmpeg -i "Pelicula (2019).mkv" -map 0:s:0 -c:s srt "Pelicula (2019).es.srt"# --- MEDIDA 3: convertir solo el audio problematico, sin tocar el video ---
# -c:v copy no recodifica el video: son segundos, no horas, y sin
# ninguna perdida de calidad de imagen.
$ ffmpeg -i entrada.mkv -c:v copy -c:s copy \
-c:a ac3 -b:a 640k -ac 6 salida.mkv# --- MEDIDA 4: cable Ethernet o PLC hasta el salon ---
$ iperf3 -c 192.168.1.45 -t 10 -R
[ 5] 0.00-10.00 sec 1.09 GBytes 938 Mbits/secEl resultado, medido antes y después:
| Métrica | Antes | Después |
|---|---|---|
CPU de ffmpeg |
191 % | 5,8 % |
| Cortes en 30 min de reproducción | 14 | 0 |
| Flujos simultáneos posibles | 1, con cortes | 3-4 |
| Consumo durante la reproducción | 48 W | 17 W |
| Reproducción directa | No | Sí, si hay versión 1080p H.264 |
Y la lección de fondo, que va más allá de este caso: la mejor transcodificación es la que no ocurre. Para una biblioteca con clientes heterogéneos, la estrategia definitiva es guardar dos versiones del material importante —el original 4K HEVC para el televisor moderno y una versión 1080p H.264 con audio AC3 para todo lo demás—, que Jellyfin ofrece automáticamente. Cuesta un 30 % más de espacio y elimina la conversión por completo.
Solución 2
El principio que organiza toda la estrategia: no todo el contenido vale lo mismo. Copiarlo todo con el mismo criterio es caro y, paradójicamente, suele acabar en que no se copia nada.
| Categoría | Volumen | ¿Se puede recuperar? | Coste de recuperarlo | Valor |
|---|---|---|---|---|
| Fotos y documentos | 400 GB | Nunca | Infinito | Máximo |
| Música de CD propios | 800 GB | Sí, ripeando de nuevo | ~40 h de trabajo | Medio |
| Películas de discos propios | 4,8 TB | Sí, copiando de nuevo | ~200 h de trabajo | Bajo |
| Configuración de Jellyfin | 2 GB | Sí, reconfigurando | ~4 h | Medio |
Estrategia por niveles, aplicando 3-2-1 donde toca:
NIVEL 1 - Insustituible (402 GB): fotos, documentos, configuracion
Copia 1: Btrfs RAID 1 local (el original)
Copia 2: disco externo USB cifrado, mensual, en un cajon
Copia 3: restic en la nube, diaria, cifrada de extremo a extremo
-> 3 copias, 2 medios, 1 fuera de casa. 3-2-1 completo.
NIVEL 2 - Recuperable con esfuerzo (800 GB): musica
Copia 1: Btrfs RAID 1 local
Copia 2: el mismo disco externo USB, mensual
-> Sin copia en la nube: 800 GB al mes no compensan 40 h de trabajo.
NIVEL 3 - Recuperable (4,8 TB): peliculas
Copia 1: Btrfs RAID 1 local
Copia 2: los discos originales en la estanteria (ya la tienes)
-> Sin copia adicional. El RAID cubre el fallo de un disco y los
originales cubren el resto.El cálculo económico, que es el argumento:
| Estrategia | Volumen en la nube | Coste anual (a 0,005 €/GB/mes) | Comentario |
|---|---|---|---|
| Copiarlo todo | 6.000 GB | 360 €/año | Insostenible en casa |
| Solo nivel 1 | 402 GB | 24 €/año | Recomendado |
| Nada en la nube | 0 GB | 0 € | Un incendio se lo lleva todo |
Veinticuatro euros al año protegen lo irreemplazable. Trescientos sesenta protegerían además unas películas que están en la estantería. La diferencia entre ambas cifras es la razón por la que mucha gente acaba sin ninguna copia: intentan copiarlo todo, ven el precio y abandonan.
Implementación:
# --- Nivel 1: restic en la nube, diario ---
$ export RESTIC_REPOSITORY="b2:midominio-medios:/"
$ restic init # una sola vez
# La frase de paso, en pass (06-05). Si se pierde, la copia es
# ILEGIBLE: no hay recuperacion posible. Va tambien en papel, en casa
# de un familiar, dentro de un sobre cerrado.
$ pass generate casa/restic 40# --- Nivel 1 y 2: disco externo USB cifrado, mensual ---
# LUKS (06-05): si pierdes el disco, los datos no son legibles
$ sudo cryptsetup luksFormat /dev/sdd1
$ sudo cryptsetup open /dev/sdd1 respaldo-externo
$ sudo mkfs.btrfs -L respaldo /dev/mapper/respaldo-externo
# Copia mensual. --delete se usa CON CUIDADO: si el origen esta
# desmontado, rsync borraria el destino entero. La comprobacion previa
# no es opcional.
$ mountpoint -q /srv/medios || { echo "origen no montado; abortando"; exit 1; }
$ sudo rsync -aHAX --delete --info=progress2 \
/srv/medios/{fotos,documentos,musica}/ /mnt/respaldo/
$ sudo umount /mnt/respaldo && sudo cryptsetup close respaldo-externoVerificación, sin la cual nada de lo anterior cuenta:
# Automatico, semanal
$ restic check --read-data-subset=5%
# Manual, trimestral: restaurar de verdad un fichero y ABRIRLO
$ restic restore latest --include '/srv/medios/fotos/2019' --target /tmp/prueba
$ sha256sum /tmp/prueba/srv/medios/fotos/2019/IMG_4412.jpg \
/srv/medios/fotos/2019/IMG_4412.jpg
a3f1... /tmp/prueba/srv/medios/fotos/2019/IMG_4412.jpg
a3f1... /srv/medios/fotos/2019/IMG_4412.jpgLa tabla de recuperación, que es lo que hay que tener escrito y guardado fuera del servidor:
| Escenario | Qué se pierde | De dónde se recupera | Tiempo |
|---|---|---|---|
| Falla un disco | Nada | RAID 1: reemplazo en caliente | 6-12 h de reconstrucción |
| Borras una carpeta | Nada | Instantánea Btrfs del día | 2 minutos |
| Se corrompe un fichero | Nada | Btrfs lo repara solo en el barrido | Automático |
| Un cifrador ataca el equipo | Nivel 3 | Nube (nivel 1) + USB (1 y 2) + discos originales | 2-3 días |
| Incendio o robo | Nivel 2 y 3 | Nube: solo el nivel 1 | 1-2 días para 400 GB |
Esa última fila es el momento de la verdad de toda la estrategia, y es honesta: en un incendio se pierden las películas y la música. Se acepta conscientemente, porque los discos originales probablemente ardan también y el coste de evitarlo son 336 € al año. Lo importante —las fotos de la familia— sobrevive, que es exactamente para lo que se diseñó.
Solución 3
#!/usr/bin/env bash
#
# revision_casa.sh - Revision de salud del servidor de medios domestico
#
# Codigos de salida:
# 0 = correcto 1 = aviso 2 = critico
#
# Pensado para ejecutarse desde un timer diario, con silencio si todo
# va bien: solo un estado != 0 produce notificacion.
#
set -euo pipefail
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/lib/comunes.sh"
readonly CASA_MEDIOS="${CASA_MEDIOS:-/srv/medios}"
readonly CASA_DISCOS="${CASA_DISCOS:-/dev/sda /dev/sdb}"
readonly CASA_UMBRAL_ESPACIO="${CASA_UMBRAL_ESPACIO:-85}"
readonly CASA_UMBRAL_TEMP="${CASA_UMBRAL_TEMP:-45}"
readonly CASA_UMBRAL_COPIA_H="${CASA_UMBRAL_COPIA_H:-36}"
readonly CASA_UMBRAL_SCRUB_D="${CASA_UMBRAL_SCRUB_D:-45}"
umask 027
estado_global=0
registrar() {
local nivel="$1" msg="$2"
case "$nivel" in
ok) log "OK $msg" ;;
aviso) error "AVISO $msg"; (( estado_global < 1 )) && estado_global=1 ;;
critico) error "CRITICO $msg"; estado_global=2 ;;
esac
return 0
}
comprobar_montaje() {
if ! mountpoint -q "$CASA_MEDIOS"; then
registrar critico "$CASA_MEDIOS NO esta montado"
return 0
fi
registrar ok "biblioteca montada"
# Con 'nofail', un disco ausente NO impide arrancar: hay que
# comprobar explicitamente que estan los dos dispositivos.
local n
n="$(btrfs filesystem show "$CASA_MEDIOS" | grep -c '^\s*devid')"
if (( n < 2 )); then
registrar critico "solo $n dispositivo(s): el RAID 1 esta DEGRADADO"
else
registrar ok "RAID 1 con $n dispositivos"
fi
}
comprobar_espacio() {
mountpoint -q "$CASA_MEDIOS" || return 0
# 'df' miente con Btrfs en RAID: se usa la salida propia de btrfs
local libre_b total_b usado_pct
libre_b="$(btrfs filesystem usage -b "$CASA_MEDIOS" | \
awk '/Free \(estimated\)/{gsub(/[^0-9]/,"",$3); print $3}')"
total_b="$(btrfs filesystem usage -b "$CASA_MEDIOS" | \
awk '/Device size/{gsub(/[^0-9]/,"",$3); print $3/2}')"
usado_pct=$(( 100 - (libre_b * 100 / total_b) ))
if (( usado_pct >= 95 )); then
registrar critico "biblioteca al ${usado_pct}% ($(formatear_bytes "$libre_b") libres)"
elif (( usado_pct >= CASA_UMBRAL_ESPACIO )); then
registrar aviso "biblioteca al ${usado_pct}%"
else
registrar ok "espacio al ${usado_pct}% ($(formatear_bytes "$libre_b") libres)"
fi
}
comprobar_errores_btrfs() {
mountpoint -q "$CASA_MEDIOS" || return 0
local total
total="$(btrfs device stats "$CASA_MEDIOS" | awk '{s+=$2} END {print s+0}')"
if (( total > 0 )); then
registrar critico "btrfs acumula $total errores de E/S o corrupcion"
btrfs device stats "$CASA_MEDIOS" | awk '$2>0 {print " " $0}' >&2
else
registrar ok "sin errores de E/S ni corrupcion"
fi
}
comprobar_scrub() {
mountpoint -q "$CASA_MEDIOS" || return 0
local fecha dias
fecha="$(btrfs scrub status "$CASA_MEDIOS" | \
awk -F': ' '/Scrub started/{print $2}')" || true
if [[ -z "${fecha:-}" ]]; then
registrar aviso "no consta ningun barrido ejecutado"
return 0
fi
dias=$(( ( $(date +%s) - $(date -d "$fecha" +%s) ) / 86400 ))
if (( dias > CASA_UMBRAL_SCRUB_D )); then
registrar aviso "ultimo barrido hace $dias dias"
else
registrar ok "ultimo barrido hace $dias dias"
fi
}
comprobar_discos() {
requiere_comando smartctl
local d
for d in $CASA_DISCOS; do
[[ -b "$d" ]] || { registrar critico "$d no existe"; continue; }
# -n standby: NO despertar un disco parado solo para mirarlo
local salida
salida="$(smartctl -A -H -n standby "$d" 2>/dev/null)" || true
if grep -q 'STANDBY' <<<"$salida"; then
registrar ok "$d en reposo (no se despierta)"
continue
fi
# Los atributos que predicen fallos, no el veredicto global
local realoc pendientes ilegibles temp
realoc="$(awk '$1==5 {print $10+0}' <<<"$salida")"
pendientes="$(awk '$1==197 {print $10+0}' <<<"$salida")"
ilegibles="$(awk '$1==198 {print $10+0}' <<<"$salida")"
temp="$(awk '$1==194 {print $10+0}' <<<"$salida")"
if (( ${ilegibles:-0} > 0 || ${pendientes:-0} > 0 )); then
registrar critico "$d: $pendientes pendientes, $ilegibles ilegibles: SUSTITUIR"
elif (( ${realoc:-0} > 0 )); then
registrar aviso "$d: $realoc sectores reasignados: vigilar"
else
registrar ok "$d sin sectores dañados"
fi
if (( ${temp:-0} >= 50 )); then
registrar critico "$d a ${temp}C"
elif (( ${temp:-0} >= CASA_UMBRAL_TEMP )); then
registrar aviso "$d a ${temp}C (ideal < 45)"
fi
done
}
comprobar_servicios() {
local s
for s in jellyfin smbd smartd; do
systemctl is-active --quiet "$s" \
&& registrar ok "$s activo" \
|| registrar critico "$s NO esta activo"
done
# Que responda, no solo que el proceso exista
if curl -sf -m 5 -o /dev/null http://127.0.0.1:8096/health; then
registrar ok "jellyfin responde"
else
registrar critico "jellyfin no responde en 8096"
fi
}
comprobar_copias() {
local marca=/var/lib/respaldo-casa/ultima-correcta
if [[ ! -f "$marca" ]]; then
registrar critico "no hay constancia de ninguna copia correcta"
return 0
fi
local horas
horas=$(( ( $(date +%s) - $(stat -c %Y "$marca") ) / 3600 ))
if (( horas > CASA_UMBRAL_COPIA_H * 2 )); then
registrar critico "ultima copia correcta hace ${horas} h"
elif (( horas > CASA_UMBRAL_COPIA_H )); then
registrar aviso "ultima copia correcta hace ${horas} h"
else
registrar ok "ultima copia correcta hace ${horas} h"
fi
}
comprobar_aceleracion() {
[[ -e /dev/dri/renderD128 ]] || { registrar aviso "sin GPU accesible"; return 0; }
if vainfo --display drm --device /dev/dri/renderD128 2>/dev/null | \
grep -q VAEntrypointEncSlice; then
registrar ok "aceleracion por hardware disponible"
else
registrar aviso "la GPU no expone codificacion por hardware"
fi
}
main() {
requiere_comando btrfs
requiere_comando curl
comprobar_montaje
comprobar_espacio
comprobar_errores_btrfs
comprobar_scrub
comprobar_discos
comprobar_servicios
comprobar_copias
comprobar_aceleracion
case "$estado_global" in
0) log "servidor de medios correcto" ;;
1) error "revision con AVISOS" ;;
2) error "revision en estado CRITICO" ;;
esac
return "$estado_global"
}
main "$@"$ shellcheck ~/scripts/revision_casa.sh && echo "sin avisos"
sin avisos
$ ~/scripts/revision_casa.sh; echo "estado: $?"
[2026-08-18 08:00:03] OK biblioteca montada
[2026-08-18 08:00:03] OK RAID 1 con 2 dispositivos
[2026-08-18 08:00:03] OK espacio al 28% (6,3 TiB libres)
[2026-08-18 08:00:04] OK sin errores de E/S ni corrupcion
[2026-08-18 08:00:04] OK ultimo barrido hace 12 dias
[2026-08-18 08:00:04] OK /dev/sda en reposo (no se despierta)
[2026-08-18 08:00:04] OK /dev/sdb en reposo (no se despierta)
[2026-08-18 08:00:05] OK jellyfin activo
[2026-08-18 08:00:05] OK smbd activo
[2026-08-18 08:00:05] OK smartd activo
[2026-08-18 08:00:05] OK jellyfin responde
[2026-08-18 08:00:05] OK ultima copia correcta hace 6 h
[2026-08-18 08:00:06] OK aceleracion por hardware disponible
[2026-08-18 08:00:06] servidor de medios correcto
estado: 0Cinco decisiones de diseño que merecen justificación:
-n standbyensmartctl. Sin esa opción, la revisión diaria despierta los discos todas las mañanas, anulando el ahorro de energía del apartado 13 y añadiendo un ciclo de arranque diario a cada disco. Un script de vigilancia que degrada lo que vigila es un mal script.- Comprobar el número de dispositivos, no solo el montaje. Con
nofail, un disco desconectado no impide arrancar y el sistema funciona con normalidad, degradado y sin redundancia. Es un fallo silencioso: sin esta comprobación, te enteras el día que falla el segundo. - No usar
dfcon Btrfs en RAID. Da cifras engañosas porque no entiende que cada bloque se escribe dos veces. Se usabtrfs filesystem usage -b. - Los atributos SMART, no el veredicto.
smartctl -HdicePASSEDen discos que van a fallar la semana que viene. Los atributos 5, 197 y 198 son los que predicen. - Una marca de fichero para las copias, escrita por
respaldo_casa.shsolo tras verificar. Comprobar que el timer se ejecutó no vale: pudo ejecutarse y fallar. Lo que importa es que hubo una copia correcta.
# /etc/systemd/system/revision-casa.timer
[Unit]
Description=Revision diaria del servidor de medios
[Timer]
OnCalendar=*-*-* 08:00:00
Persistent=true
[Install]
WantedBy=timers.target# /etc/systemd/system/revision-casa.service
[Unit]
Description=Revision de salud del servidor de medios
# Notificar SOLO si falla: silencio si todo va bien
OnFailure=notificar-casa@%n.service
[Service]
Type=oneshot
User=operador
ExecStart=/home/operador/scripts/revision_casa.shCon Type=oneshot y sin redirecciones, la salida va al journal y solo un código distinto de cero dispara OnFailure. Es la convención de «silencio si todo va bien» del curso, y en casa importa aún más: un correo diario que dice «todo bien» se deja de leer en dos semanas, y con él se dejan de leer los que sí importan. Es la fatiga de alertas de 08-06, en versión doméstica.
Conclusión
Has construido un servidor completo desde cero, y por el camino has comprobado la respuesta a la pregunta con la que empezaba la lección: no había nada específico de «servidores de empresa». Montaste por UUID con nofail porque en 05-04 aprendiste que un disco ausente puede dejarte sin acceso remoto. Pusiste la clave del repositorio en keyrings porque en 05-03 aprendiste que apt-key está obsoleto. Endureciste la unidad de systemd hasta bajar la exposición a 2,4, con un ReadOnlyPaths sobre la biblioteca que impide que un fallo de la aplicación borre veinte años de fotos. Validaste la configuración de Samba con testparm -s antes de aplicarla, que es el mismo nginx -t de 08-01 con otro nombre. Y lo dejaste todo en Ansible, reconstruible como srv-tramontana.
Has tomado decisiones justificadas en lugar de copiar recetas. Btrfs en RAID 1 en vez de mdadm, porque el checksumming detecta y repara la corrupción silenciosa que un RAID clásico propaga a las copias — y lo has visto funcionar en un scrub que corrigió dos bloques. Jellyfin en vez de Plex, porque no depende de la nube de nadie y su aceleración por hardware no es de pago. La VPN en vez de exponer el servicio a Internet, porque una vulnerabilidad de Jellyfin no debe ser una vulnerabilidad de tu casa. Y una estrategia de copias por niveles que protege lo insustituible por 24 € al año en lugar de intentar protegerlo todo por 360 y acabar sin proteger nada.
También has aprendido dos cosas que el trabajo no te enseña. Que el consumo eléctrico es una decisión de arquitectura: 110 € al año de diferencia entre un mini PC y un PC reutilizado, que en tres años paga el equipo entero. Y que el RAID cubre exactamente una amenaza de las siete de la tabla, mientras que las instantáneas de Btrfs, las copias verificadas y los discos originales en la estantería cubren las otras seis. Con la frase que hay que llevarse: el RAID protege del fallo de un disco y de nada más.
En 08-04 montas el servidor VPN con WireGuard que esta lección ha recomendado dos veces. Vas a entender por qué WireGuard es un interfaz de red y no un demonio, por qué AllowedIPs es enrutamiento y control de acceso a la vez —el concepto que más se malinterpreta de toda la herramienta—, y cómo decidir entre túnel dividido y túnel completo con criterio en lugar de por costumbre. Al terminar tendrás acceso a la red interna de Tramontana desde cualquier sitio sin exponer un solo puerto TCP, podrás cerrar PostgreSQL y el panel de estadísticas al exterior porque ya no harán falta abiertos, y de paso tu biblioteca de casa será alcanzable desde el hotel con un solo puerto UDP abierto en el router. Y verás por qué una VPN, pese a todo lo bueno, no convierte una red interna en una red segura.
Curso de Linux: De Principiante a Administrador de Sistemas
Módulo 1: Introducción a Linux
- ¿Qué es Linux?
- Historia de Linux
- Distribuciones de Linux
- Instalando Linux
- Primer Contacto con el Sistema
- Estructura del Sistema de Archivos de Linux
Módulo 2: Comandos Básicos de Linux
- Introducción a la Línea de Comandos
- Obtener Ayuda y Documentación del Sistema
- Navegando el Sistema de Archivos
- Operaciones con Archivos y Directorios
- Visualización y Edición de Archivos
- Enlaces Duros y Simbólicos
- Permisos y Propiedad de Archivos
Módulo 3: Habilidades Avanzadas en la Línea de Comandos
- El Entorno del Shell: Variables, Alias e Historial
- Uso de Comodines y Expresiones Regulares
- Búsqueda de Archivos y Contenido: find, locate y grep
- Tuberías y Redirección
- Procesamiento de Texto: cut, sort, uniq, sed y awk
- Gestión de Procesos
- Programación de Tareas con Cron
- Comandos de Redes
Módulo 4: Scripting en Shell
- Introducción al Scripting en Shell
- Variables y Tipos de Datos
- Entrada, Salida y Argumentos de un Script
- Estructuras de Control
- Funciones y Librerías
- Depuración y Manejo de Errores
- Scripts de Producción: Buenas Prácticas
Módulo 5: Administración del Sistema
- Gestión de Usuarios y Grupos
- sudo y Permisos Especiales
- Gestión de Paquetes
- Gestión de Discos
- systemd y la Gestión de Servicios
- Registros del Sistema: journald y syslog
- Monitoreo del Sistema y Optimización del Rendimiento
- Respaldo y Restauración
Módulo 6: Redes y Seguridad
- Configuración de Redes
- SSH y Acceso Remoto
- Firewall y Seguridad Perimetral
- Sistemas de Detección de Intrusos
- Gestión de Secretos y Certificados TLS
- Asegurando Sistemas Linux
Módulo 7: Temas Avanzados
- El Proceso de Arranque y la Recuperación del Sistema
- Diagnóstico Avanzado: strace, perf y eBPF
- Optimización del Kernel de Linux
- Virtualización con Linux
- Contenedores de Linux y Docker
- Automatización con Ansible
- Alta Disponibilidad y Balanceo de Carga
