Problema
En entornos de auto‑hosting es frecuente necesitar acceder a una aplicación web que corre dentro de Docker desde fuera de la red local. La solución típica es abrir puertos en el router o instalar el cliente VPN de Tailscale directamente en el host. Ambas opciones tienen inconvenientes: los puertos expuestos quedan visibles a Internet y la instalación del cliente VPN modifica la pila de red del sistema operativo, provocando conflictos con otras VPN, con iCloud Private Relay o con la configuración DNS del host. El objetivo es obtener un endpoint HTTPS accesible solo desde dispositivos autorizados, sin tocar la configuración de red del host y sin exponer la máquina completa a la tailnet.
Causa
Los problemas aparecen por tres motivos recurrentes:
- VPN a nivel de host – El cliente Tailscale crea una interfaz de red que se vuelve la ruta predeterminada para todo el tráfico. Cuando otro cliente VPN (por ejemplo, un cliente corporativo) se conecta, macOS permite que solo una VPN tenga prioridad, lo que desconecta Tailscale sin aviso.
- DNS global – La opción “Use Tailscale DNS” altera la resolución de nombres del sistema, rompiendo servicios que dependen de DNS local o de resolvers personalizados.
- Exposición de puertos – Publicar puertos en el router o usar
docker run -pexpone la aplicación a cualquier dirección IP, lo que incrementa la superficie de ataque y elimina el aislamiento entre la LAN y la tailnet.
Solución
Ejecutar Tailscale dentro de un contenedor “sidecar” que comparte la red del contenedor de la aplicación. El sidecar maneja la autenticación en la tailnet, genera un certificado HTTPS automático y sirve la aplicación exclusivamente a través de la dirección app.<tailnet>.ts.net. La aplicación sigue escuchando en 127.0.0.1:PORT, por lo que el host no necesita ninguna regla de firewall ni cambios de DNS.
Pasos generales:
- Crear una clave de autenticación en la consola de Tailscale con la etiqueta
tag:containery sin expiración. - Añadir un archivo
docker‑compose.override.ymlque define el contenedortailscaley reconfigura la aplicación para usarnetwork_mode: service:tailscale. - Montar directorios persistentes (
/var/lib/tailscaley/config) para que el dispositivo conserve su estado entre reinicios. - Configurar
serve.jsonpara que Tailscale sirva HTTPS en el dominio de la tailnet y haga proxy ahttp://127.0.0.1:PORT. - Desplegar con
docker compose up -d. El contenedor se registra automáticamente en la tailnet, solicita el certificado y publica el endpoint. - Opcional: anunciar rutas LAN si la aplicación depende de recursos internos (por ejemplo, back‑ends que usan la IP local). Se habilita mediante
TS_ROUTES=192.168.x.x/32y se aprueba la ruta en la consola de Tailscale.
Este patrón funciona en cualquier host Docker (macOS con OrbStack, Docker Desktop, Linux, Raspberry Pi) y es independiente del gestor de orquestación que se use.
Cuándo aplicar esta solución
- Necesitas exponer un servicio Docker a dispositivos personales sin abrir puertos públicos.
- El host ya ejecuta otras VPN o servicios que no pueden ser alterados por la instalación de Tailscale.
- Quieres que la aplicación siga siendo accesible en la LAN (por ejemplo, desde una TV) mientras mantiene un endpoint seguro para acceso remoto.
- No deseas que todo el host aparezca en la tailnet; solo el servicio concreto debe ser accesible.
No es apropiado cuando:
- El entorno requiere salida a Internet a través de Tailscale (exit node) para todo el host.
- La aplicación necesita escuchar en múltiples interfaces externas diferentes a
127.0.0.1. - No se dispone de persistencia de volúmenes (por ejemplo, en contenedores efímeros sin almacenamiento).
Código
# 1. Variables de entorno (añadir a .env)
printf 'TS_AUTHKEY=%s\n' 'tskey-auth-XXXXXXXXXXXXXXXX' >> .env
chmod 600 .env
# 2. serve.json (montado en ./ts/config)
cat > ts/config/serve.json <<'EOF'
{
"TCP": {
"443": {
"HTTPS": true
}
},
"Web": {
"${TS_CERT_DOMAIN}:443": {
"Handlers": {
"/": {
"Proxy": "http://127.0.0.1:PORT"
}
}
}
}
}
EOF
# 3. docker‑compose.override.yml
cat > docker-compose.override.yml <<'EOF'
services:
ts-APP:
image: tailscale/tailscale:latest
container_name: ts-APP
hostname: APP
environment:
- TS_AUTHKEY=${TS_AUTHKEY}
- TS_EXTRA_ARGS=--advertise-tags=tag:container
- TS_STATE_DIR=/var/lib/tailscale
- TS_SERVE_CONFIG=/config/serve.json
# - TS_ROUTES=192.168.1.0/24 # opcional
volumes:
- ./ts/state:/var/lib/tailscale
- ./ts/config:/config
ports:
- "PORT:PORT"
networks:
- default
restart: unless-stopped
APP:
network_mode: service:ts-APP
depends_on:
ts-APP:
condition: service_started
ports: !reset []
networks: !reset []
EOF
Reemplaza APP por el nombre del servicio original y PORT por el puerto interno que la aplicación escucha.
## Verificación
1. Esperar 15 s después del despliegue y ejecutar:
```bash
docker exec ts-APP tailscale status
La salida debe mostrar una dirección 100.x.x.x y el estado Running.
-
Confirmar que el servicio está sirviendo HTTPS:
docker exec ts-APP tailscale serve statusDebería aparecer
https://APP.<tailnet>.ts.net (tailnet only). -
Desde un dispositivo autorizado, abrir el URL en un navegador. El certificado debe ser válido y la aplicación debe cargar sin redirecciones a la LAN.
-
Con el VPN de Tailscale desactivado en el cliente móvil, intentar acceder al mismo URL. El tráfico debe fallar, confirmando que solo la tailnet está en uso.
-
Probar que la LAN sigue funcionando: en la misma red local, acceder a
http://<host‑lan‑ip>:PORT. La aplicación debe responder.
Notas adicionales
- Persistencia del estado: si el directorio
ts/statese pierde, el contenedor aparecerá como un nuevo dispositivo y la clave de autenticación volverá a consumirse. Mantén copias de seguridad del volumen. - Política de acceso: la política por defecto permite cualquier dispositivo en la tailnet a conectarse al puerto 443. Refina la política JSON en la consola de Tailscale para limitar el acceso a los grupos o tags que necesites.
- Actualizaciones:
docker compose pull && docker compose up -dincorpora automáticamente el override. Si tu pipeline usa-f docker-compose.yml, agrega-f docker-compose.override.yml. - Reinicios ordenados: primero reinicia el sidecar (
docker compose restart ts-APP) y luego la aplicación. De lo contrario la app quedará sin red y fallará al iniciar. - Monitorización: un simple health‑check puede ser:
docker exec ts-APP tailscale status --json | grep -q '"BackendState": *"Running"' && echo ok - Seguridad de puertos: verifica que no haya escuchas inesperadas con
netstat -an -p tcp | grep LISTEN. Desactiva UPnP en el router para evitar aperturas automáticas.
Con este enfoque, cualquier servicio Docker puede recibir acceso remoto seguro a través de Tailscale sin tocar la configuración de red del host, manteniendo la LAN intacta y evitando interferencias con otras VPN o con iCloud Private Relay.