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

  1. Advertencia legal: qué contenido es legítimo
  2. Objetivo, requisitos y decisiones de diseño
  3. Elegir el hardware
  4. Almacenamiento: RAID, Btrfs y la verdad sobre las copias
  5. Jellyfin frente a Plex y Emby
  6. Instalación y unidad de systemd endurecida
  7. Aceleración por hardware para transcodificación
  8. Organización de la biblioteca y nombres de fichero
  9. Compartir: Samba para todo, NFS para Linux
  10. Acceso desde fuera de casa: tres opciones y su riesgo
  11. Descargas y automatización
  12. Copias de seguridad de la biblioteca y de la configuración
  13. Consumo, ruido y temperatura
  14. Automatización con Ansible
  15. 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.01TiB

Fí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: 0

Corrected: 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=idle

RAID 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:

  1. 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.
  2. 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.
  3. 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
active

Comprobació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]
      04B7DA6B1FE10D5E76D9D5A6F5B2F0D5A8E5D6C3

Esa 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.

$ sudo systemctl edit jellyfin
# /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 OK

De 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 lectura

Aceleració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            : VAEntrypointVLD

Có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 jellyfin

Y 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 .srt externos 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"
  done

Permisos, 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           Disk

Tres 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/24

Acceso 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-stopped

Dos observaciones aplicables independientemente del uso:

  • ports: "127.0.0.1:8989:8989" es el detalle crítico. Docker crea sus propias reglas de iptables y se salta ufw, como viste en 07-05: publicar 8989:8989 a secas expone el puerto a toda la red aunque ufw diga 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

Coste anual = Potencia media (W) x 8760 h / 1000 x precio del kWh
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        jellyfin

powertop --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.target

Apagar 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-upgrades

Con 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  18

Procedimiento 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/medios

Tres avisos sobre este procedimiento:

  • btrfs replace es preferible a add + 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 mdadm si 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 DeviceAllow en la unidad endurecida. El usuario está en el grupo render y aun así no ve la GPU, y el desconcierto dura horas.
  • No poner ReadOnlyPaths sobre 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 ufw los tapa. Docker escribe sus propias reglas y se las salta. Usa 127.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 PASSED de 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,
      SubtitleCodecNotSupported

Tres 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/sec

48,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/sec

El 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-externo

Verificació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.jpg

La 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: 0

Cinco decisiones de diseño que merecen justificación:

  1. -n standby en smartctl. 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.
  2. 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.
  3. No usar df con Btrfs en RAID. Da cifras engañosas porque no entiende que cada bloque se escribe dos veces. Se usa btrfs filesystem usage -b.
  4. Los atributos SMART, no el veredicto. smartctl -H dice PASSED en discos que van a fallar la semana que viene. Los atributos 5, 197 y 198 son los que predicen.
  5. Una marca de fichero para las copias, escrita por respaldo_casa.sh solo 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.sh

Con 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

Módulo 2: Comandos Básicos de Linux

Módulo 3: Habilidades Avanzadas en la Línea de Comandos

Módulo 4: Scripting en Shell

Módulo 5: Administración del Sistema

Módulo 6: Redes y Seguridad

Módulo 7: Temas Avanzados

Módulo 8: Proyectos Prácticos

© Copyright 2026. Todos los derechos reservados