Problema
Al montar varios servicios (gateway LLM, agente de mensajería, servidor multimedia y escritorio remoto) en una única VM de 4 vCPU, es frecuente encontrarse con que los contenedores arrancan, los puertos parecen abiertos y, sin embargo, nada responde. Los logs aparecen vacíos o con mensajes genéricos como “command not found” o “permission denied”. La raíz suele ser una combinación de reglas de firewall invisibles, políticas de reinicio que no reintentan, y permisos de usuario que no coinciden con los archivos creados durante la instalación.
Este patrón se repite en cualquier stack auto‑hosteado que combine Docker, herramientas de red tipo Tailscale y procesos que dependen de usuarios no‑root. El síntoma típico es:
- Puertos accesibles desde el exterior pero tráfico bloqueado internamente.
- Contenedores que fallan al iniciar y no se vuelven a lanzar.
- Servicios que lanzan errores de “npm no está disponible” o “node: command not found” sin referencia a permisos.
- Transcodificaciones inesperadas en servidores multimedia.
Causa
1. Firewall y cadena FORWARD ignorada por UFW
UFW solo inspecciona la cadena INPUT. Docker crea sus propias reglas en la cadena DOCKER y redirige tráfico a través de FORWARD. Si UFW está activo con política deny en INPUT, el tráfico que pasa por FORWARD sigue fluyendo, creando una falsa sensación de seguridad.
2. Bind a 0.0.0.0 sin restricción de interfaz
Usar -p 8080:8080 expone el puerto en todas las interfaces, incluida la interfaz de Docker (docker0). Cuando la VM tiene una interfaz de túnel (Tailscale) sin dirección asignada aún, el contenedor intenta enlazar esa IP y falla, provocando un exit 128 sin reintentos.
3. Política de reinicio unless-stopped
Docker solo reinicia contenedores que hayan arrancado correctamente. Si el contenedor falla en la fase de creación (por ejemplo, por una IP no disponible), no se cuenta como “started” y la política no lo vuelve a lanzar.
4. Permisos de usuario y rutas de instalación
Instalar npm o node como root genera enlaces simbólicos en /root/.npm con permisos 700. Cuando el contenedor o el servicio corre bajo otro UID, el binario no es visible, generando “command not found”. Lo mismo ocurre con directorios creados por scripts que terminan con mkdir sin los permisos adecuados.
5. Cadenas de iptables de Tailscale
Tailscale inserta la cadena ts-input al principio de INPUT y acepta todo el tráfico del tailnet. Si una regla personalizada se coloca después, nunca se evalúa. Además, la cadena ts-forward puede interferir con la NAT de Docker.
6. Media server y transcodificación inesperada
Los archivos H.264 con audio E‑AC3 son reproducibles en navegadores que no soportan ese codec. El servidor (Plex, Jellyfin) decide transcodificar el audio, consumiendo CPU sin necesidad. La falta de inspección previa del contenedor de medios lleva a sobrecarga inesperada.
7. Trampas de shell comunes
rmdir -pque sube niveles y borra directorios críticos.- Expansión de palabras que rompe rutas con espacios (
$DIR/*sin comillas). pkill -fque coincide con la propia línea de comando SSH y corta la sesión.
Solución
A. Alinear firewall con Docker
- Desactivar UFW o configurarlo para que también inspeccione FORWARD:
ufw default allow forward ufw reload - Alternativamente, crear reglas explícitas en la cadena DOCKER antes de que Docker las sobrescriba:
iptables -I DOCKER-USER -i tailscale0 -j ACCEPT
B. Enlazar a interfaces específicas
En los docker run o en docker‑compose.yml usar la opción --network host solo cuando sea necesario, o especificar la IP de la interfaz pública:
ports:
- "192.168.1.10:8080:8080"
Esto evita que Docker intente enlazar una IP que aún no existe (por ejemplo, la de Tailscale).
C. Política de reinicio robusta
Utilizar restart: on-failure o crear una unidad systemd que vigile la disponibilidad de la IP y lance el contenedor cuando sea válida:
[Unit]
Description=LLM gateway con espera de Tailscale IP
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/wait‑tailscale‑ip.sh
ExecStartPost=/usr/bin/docker start llm-gateway
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
wait‑tailscale‑ip.sh:
#!/usr/bin/env bash
while ! ip -4 addr show tailscale0 | grep -q "inet "; do
sleep 2
done
D. Uniformizar usuarios y rutas
Instalar node y npm mediante nvm o paquetes del sistema que se ubiquen en /usr/local/bin. Evitar ejecutar npm install -g como root. Si se necesita root, crear enlaces en /usr/local/bin con permisos 755.
E. Posicionar reglas de Tailscale
Insertar reglas personalizadas en la posición 1 de INPUT para que se evalúen antes que ts-input:
iptables -I INPUT 1 -p tcp --dport 8080 -j ACCEPT
Comprobar contadores con iptables -L -v -n.
F. Pre‑chequeo de medios
Antes de lanzar el contenedor de Jellyfin, escanear la biblioteca y generar un informe de codecs:
ffprobe -v error -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 "$file"
Re‑encode solo los archivos que realmente lo requieran, evitando transcodificaciones en tiempo real.
G. Buenas prácticas de shell
- Siempre citar variables que contengan rutas:
"$DIR/*". - Usar
rm -r -- "$DIR"en lugar dermdir -p. - Limitar
pkilla procesos específicos:pkill -f "^ssh .*@myhost$".
Cuándo aplicar esta solución
- Síntomas de firewall invisible: puertos accesibles externamente pero servicios no responden,
ufw statusmuestradefault denymientrasnetstat -tlnplista los puertos. - Contenedores que no reinician: logs con
exit code 128y políticaunless-stopped. - Errores de comando no encontrado después de una instalación como root.
- Transcodificación constante en servidores de medios sin razón aparente.
- Fallos intermitentes al usar Tailscale: la IP del túnel aparece después de que el contenedor ya intentó iniciar.
No aplica cuando la arquitectura no usa Docker (por ejemplo, VMs con systemd‑nspawn) o cuando el firewall se gestiona exclusivamente con iptables sin UFW.
Código
# 1. Permitir forward en UFW
ufw default allow forward
ufw reload
# 2. Regla iptables antes de ts-input
iptables -I INPUT 1 -p tcp --dport 8080 -j ACCEPT
# 3. docker‑compose fragmento con bind a IP fija
services:
llm-gateway:
image: my/llm-gateway
ports:
- "203.0.113.5:8000:8000"
restart: on-failure
# 4. systemd unit para esperar IP de Tailscale
cat > /etc/systemd/system/llm-gateway-wait.service <<'EOF'
[Unit]
Description=Wait for Tailscale IP and start LLM gateway
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/wait-tailscale-ip.sh
ExecStartPost=/usr/bin/docker start llm-gateway
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
EOF
chmod +x /usr/local/bin/wait-tailscale-ip.sh
systemctl enable llm-gateway-wait
systemctl start llm-gateway-wait
Verificación
- Firewall:
iptables -L INPUT -v -ndebe mostrar contadores incrementándose en la regla de puerto 8080. - Contenedor:
docker ps -adebe mostrar el contenedor en estado running y sinRestartCount. - Permisos:
which nodeynode -vdeben ejecutarse bajo el UID del contenedor. - Media: reproducir un archivo con audio E‑AC3 y confirmar que el servidor no inicia transcodificación (monitorizar CPU).
- Tailscale:
tailscale statusdebe listar la IP asignada yping <ip>desde otro nodo del tailnet debe responder.
Notas adicionales
- Cuando se usa
docker compose up -d, Docker crea la cadenaDOCKER-USER. Colocar reglas allí garantiza que se apliquen antes de la NAT de Docker. - En entornos con varios túneles (WireGuard, Tailscale), asignar direcciones estáticas en la configuración del túnel simplifica los binds.
- Los contenedores que dependen de GPUs pueden fallar al iniciar si la interfaz de red no está disponible; aplicar la misma lógica de espera de IP evita errores de inicialización.
- Mantener un script de auditoría (
audit.sh) que recorradocker inspect,iptables -Lyufw statusayuda a detectar desalineaciones antes de que el stack entre en producción.