Problema

Al montar un servidor de medios con Jellyfin dentro de un contenedor Docker, muchos administradores de homelab quieren que el tráfico pase por Tailscale para disponer de una red privada sin abrir puertos públicos. En la práctica, el proceso suele tropezar con dos obstáculos recurrentes:

  1. Conflicto de puertos: Docker reclama puertos que ya están reservados por servicios del host (por ejemplo, 443 para HTTPS).
  2. Resolución DNS interna: Cuando se asigna un nombre de dominio a la IP local del servidor, la resolución a través de Tailscale puede quedar atrapada en una entrada estática que apunta a la IP de la LAN, impidiendo que el contenedor se comunique correctamente durante la construcción o el arranque.

El resultado típico es un error al intentar compilar o iniciar la imagen de Jellyfin, con mensajes que indican que el puerto 443 está “restricted” o que la conexión a la red Tailscale falla. El problema no es exclusivo de Jellyfin; cualquier servicio que requiera puertos estándar y dependa de DNS interno sufre el mismo patrón.

Causa

1. Puertos reservados por el host

Docker, por defecto, necesita permisos de root para enlazar puertos por debajo del 1024. Si el usuario que ejecuta Docker no pertenece al grupo docker o no tiene privilegios de CAP_NET_BIND_SERVICE, el intento de publicar -p 443:443 falla. En muchos sistemas Debian‑based, la solución rápida es añadir el usuario al grupo docker o usar usermod -aG docker $USER.

2. Entrada DNS estática que sobrescribe la resolución de Tailscale

Tailscale crea su propio DNS interno (100.x.x.x) y, al mismo tiempo, permite mapear dominios personalizados a esa IP. Si previamente se creó una entrada en /etc/hosts o en el DNS local que apunta el dominio (por ejemplo, media.mydomain.com) a la IP de la LAN (192.168.1.10), cualquier petición dentro del contenedor seguirá esa ruta, ignorando la red Tailscale. El contenedor, al intentar descargar capas o resolver nombres externos, se queda atrapado.

3. Falta de configuración de red en Docker para usar la interfaz Tailscale

Docker crea una red bridge aislada que no incluye la interfaz tailscale0. Sin una conexión explícita, los contenedores no pueden alcanzar la red Tailscale, lo que provoca fallos al intentar comunicarse con servicios externos o al exponer puertos a través de la VPN.

Solución

Paso 1 – Garantizar permisos de puerto bajo 1024

sudo usermod -aG docker $(whoami)
newgrp docker

Reiniciar la sesión o el daemon Docker para que el grupo tenga efecto. Alternativamente, usar setcap en el binario Docker:

sudo setcap 'cap_net_bind_service=+ep' /usr/bin/docker

Paso 2 – Eliminar o sobrescribir entradas DNS locales

Revisa /etc/hosts y cualquier zona DNS local (por ejemplo, dnsmasq). Busca líneas como:

192.168.1.10 media.mydomain.com

Elimínalas o comenta la línea. Si necesitas mantener la resolución para la LAN, crea una regla específica en Tailscale:

tailscale set --advertise-routes=192.168.1.0/24
tailscale set --accept-dns=true
tailscale set --hostname=media

Luego, en la consola de Tailscale, asigna el dominio a la IP de la red Tailscale (ejemplo 100.101.102.103). De esta forma, cualquier contenedor que resuelva media.mydomain.com obtendrá la IP de Tailscale y no la de la LAN.

Paso 3 – Conectar Docker a la interfaz Tailscale

Crea una red Docker que use la interfaz tailscale0 como puente:

docker network create \
  --driver=bridge \
  --subnet=100.101.102.0/24 \
  --gateway=100.101.102.1 \
  tsnet

Al lanzar el contenedor Jellyfin, enlázalo a esa red:

docker run -d \
  --name=jellyfin \
  --network=tsnet \
  -p 443:443 \
  -v /srv/jellyfin/config:/config \
  -v /srv/jellyfin/cache:/cache \
  -v /media:/media \
  jellyfin/jellyfin:latest

Con la red tsnet, el contenedor hereda la ruta a tailscale0 y puede comunicarse con otros nodos Tailscale sin interferir con la tabla de rutas del host.

Paso 4 – Configurar nginx como reverse proxy sobre Tailscale

Instala nginx en el host (no dentro de Docker) y apunta al contenedor usando la IP interna de la red tsnet:

server {
    listen 443 ssl;
    server_name media.mydomain.com;

    ssl_certificate /etc/letsencrypt/live/media.mydomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/media.mydomain.com/privkey.pem;

    location / {
        proxy_pass http://100.101.102.2:8096;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Reemplaza 100.101.102.2 por la IP asignada al contenedor (puedes obtenerla con docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' jellyfin). Reinicia nginx y verifica que el dominio responde a través de Tailscale.

Cuándo aplicar esta solución

  • Síntomas: errores al iniciar contenedores que exponen puertos 443/80, mensajes de “port is restricted”, o fallos de resolución DNS que apuntan a la IP local en vez de la de Tailscale.
  • Entorno: servidores Debian/Ubuntu con Docker y Tailscale instalados, uso de dominios personalizados para acceso externo, y necesidad de mantener la infraestructura dentro de un homelab.
  • Exclusiones: si la arquitectura no incluye un reverse proxy o si el dominio se resuelve exclusivamente a través de un proveedor DNS externo sin entradas locales, el paso de eliminación de /etc/hosts puede no ser necesario.

Código

# 1. Añadir usuario al grupo docker
sudo usermod -aG docker $(whoami)
newgrp docker

# 2. Limpiar entradas DNS locales
sudo sed -i '/media\.mydomain\.com/d' /etc/hosts

# 3. Configurar Tailscale para aceptar DNS y rutas
tailscale set --advertise-routes=192.168.1.0/24
tailscale set --accept-dns=true

# 4. Crear red Docker que use la subred Tailscale
docker network create \
  --driver=bridge \
  --subnet=100.101.102.0/24 \
  --gateway=100.101.102.1 \
  tsnet

# 5. Lanzar Jellyfin en la red tsnet
docker run -d \
  --name=jellyfin \
  --network=tsnet \
  -p 443:443 \
  -v /srv/jellyfin/config:/config \
  -v /srv/jellyfin/cache:/cache \
  -v /media:/media \
  jellyfin/jellyfin:latest

Verificación

  1. Comprobar que el contenedor tiene IP en la subred Tailscale

    docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' jellyfin
    

    La salida debe pertenecer a 100.101.102.0/24.

  2. Probar conectividad desde otro nodo Tailscale

    curl -k https://media.mydomain.com
    

    Debería devolver la página de inicio de Jellyfin sin errores de certificado.

  3. Validar que nginx está escuchando en 443

    sudo ss -tlnp | grep ':443'
    

    La línea debe mostrar nginx y la dirección 0.0.0.0:443.

  4. Revisar logs de Docker y nginx para asegurarse de que no aparecen errores de “port already in use” ni de DNS.

Notas adicionales

  • Persistencia de DNS en Tailscale: después de eliminar la entrada en /etc/hosts, puede tardar unos segundos en propagarse la nueva resolución a través del daemon de Tailscale. Un reinicio de tailscaled (sudo systemctl restart tailscaled) acelera el proceso.
  • Firewall del host: si usas ufw o iptables, abre explícitamente el puerto 443 en la interfaz tailscale0 para evitar bloqueos inesperados.
  • Actualizaciones de Jellyfin: al actualizar la imagen, verifica que la IP del contenedor no cambie; si lo hace, actualiza la directiva proxy_pass en nginx.
  • Backup de volúmenes: los volúmenes /srv/jellyfin/config y /srv/jellyfin/cache pueden ser respaldados con docker run --rm -v /srv/jellyfin/config:/src -v $(pwd):/backup alpine tar czf /backup/jellyfin-config.tar.gz -C /src . para mantener la configuración entre reinstalaciones.