🌐 Exponer de forma segura
Esta guía muestra cómo acceder a LibreFolio fuera de casa sin abrir ningún puerto en tu router, usando Tailscale, una VPN de malla segura que es gratuita para uso doméstico. Los mismos pasos funcionan para cualquier otro servicio en tu red local.
Necesitas el Paso 0 más el nivel que te corresponda. Los niveles 3 y 4 también necesitan la configuración única de Funnel en la consola.
| Nivel | Ideal para | Tailscale en tu teléfono/PC | Dirección HTTPS pública |
|---|---|---|---|
| 🏃 1. VPN privada | Solo para ti, con la configuración más rápida | Necesario | No |
| 🥉 2. Router de subred | Todos los dispositivos de tu LAN doméstica, no solo LibreFolio | Necesario | No |
| 🥈 3. Funnel | Una dirección pública para LibreFolio, e instalarlo como app | No necesario | Sí |
| 🥇 4. Multi-Funnel con Docker | Una dirección pública por servicio, cada uno en su propio contenedor | No necesario | Sí |
⭐ Nuestra recomendación: Nivel 4
El Nivel 4 necesita poco más de configuración que el Nivel 3, mantiene cada servicio aislado en su propio contenedor y le da a cada uno su propia dirección pública. Los niveles 1–3 son alternativas más sencillas y muestran el camino que lleva hasta allí.
En todo lo que sigue, 6040 es el puerto predeterminado de LibreFolio: si configuras un PORT diferente en tu .env, usa ese número en su lugar. LibreFolio sirve la aplicación web, su API y la documentación integrada en este único puerto, por lo que es el único puerto que hay que exponer.
🔒 ¿Por qué no usar simplemente el reenvío de puertos?
La forma clásica es abrir un puerto en tu router doméstico y apuntar un nombre de DNS dinámico (como DuckDNS) a tu IP pública. Funciona, pero:
- Todo internet puede verlo: cualquiera puede escanear tu IP pública y atacar el puerto abierto.
- HTTPS es tu responsabilidad: debes ejecutar un proxy inverso (Nginx, Caddy…) y mantener renovados sus certificados SSL.
- Sin HTTPS, los datos viajan en claro: tu contraseña y tus datos financieros pueden ser interceptados en el camino.
graph LR
User["👤 External User<br>(Away from Home)"] --- Cloud["☁️ Internet / Router (Public IP / DDNS?)"]
Cloud --- Server["🖥️ Local Server<br>(Port 6040)"]
Tailscale evita las tres cosas: no se abre ningún puerto del router, el tráfico entre tus dispositivos está cifrado y Funnel (Niveles 3 y 4) añade HTTPS con certificados que Tailscale gestiona por ti.
🏁 Paso 0: Instalar Tailscale en tus dispositivos
Tailscale es una VPN de malla construida sobre el protocolo WireGuard: tus dispositivos se unen a una red privada (tu tailnet) y se comunican entre sí a través de túneles cifrados. Se ejecuta en Linux, macOS, Windows, iOS y Android, en un NAS o dentro de Docker, y su plan Personal gratuito es suficiente para uso doméstico (consulta los precios de Tailscale para conocer los límites actuales).
Instálalo e inicia sesión en al menos dos dispositivos: el servidor que ejecuta LibreFolio y un cliente, como tu teléfono o portátil.
Ejecuta el comando de instalación oficial en el servidor:
Para más detalles, consulta la Guía de instalación genérica.
Instala la aplicación oficial desde la Mac App Store o usa Homebrew:
Para más detalles, consulta la Guía de instalación genérica.
Descarga el instalador oficial desde el portal de Tailscale y sigue el asistente de inicio de sesión.
Para más detalles, consulta la Guía de instalación para Windows.
Instala la aplicación oficial desde Google Play Store.
Instala la aplicación oficial desde la Apple App Store.
🔑 Mantén el servidor conectado — desactiva la caducidad de la clave
De forma predeterminada, Tailscale pide a cada dispositivo que vuelva a iniciar sesión después de 180 días. Un servidor no debería desconectarse de tu tailnet por eso, así que desactívalo para el servidor:
- En la página Máquinas de la consola de administración, localiza tu servidor.
- Haz clic en el icono de tres puntos (...) a la derecha de la fila del dispositivo.
- Selecciona la opción Desactivar caducidad de clave.
🏃 Nivel 1: VPN privada punto a punto
Ideal solo para ti, con la configuración más rápida. Tu teléfono o portátil accede a LibreFolio a través de tu tailnet privada y nada queda expuesto a internet.
graph LR
Client["👤 Client (VPN active)<br>(100.x.y.z)"] -->|Direct VPN Connection| Server["🖥️ Server (VPN active)<br>(100.a.b.c:6040)"]
subgraph LAN ["Local LAN Network"]
Server -->|Local access| LibreFolio["📊 LibreFolio (Local)"]
end
style LibreFolio fill:#d4edda,stroke:#28a745,stroke-width:2px;
▶️ Paso 1: Comprobar que LibreFolio responde en su puerto
No hay nada que compartir: con Tailscale activado en el servidor, el puerto 6040 de LibreFolio ya es accesible desde tu tailnet en la IP de Tailscale del servidor.
¿Prefieres una dirección HTTPS dentro de tu tailnet?
Ejecuta esto en el servidor y luego abre https://<server-name>.your-tailnet.ts.net en lugar de la IP:
--bg lo mantiene en ejecución después de cerrar la terminal; tailscale serve reset lo elimina. La primera vez, Tailscale puede pedirte que habilites los certificados HTTPS para tu tailnet. La dirección sigue siendo privada: solo los dispositivos de tu tailnet pueden abrirla.
📱 Paso 2: Abrir LibreFolio desde tu dispositivo
Con Tailscale activado en tu teléfono o PC, escribe la IP de Tailscale del servidor (o su nombre MagicDNS) seguida del puerto en el navegador, por ejemplo http://100.a.b.c:6040.
Para comprobar el resultado, consulta Comprobar la seguridad de la conexión.
⚠️ Límites: cada dispositivo desde el que te conectes necesita Tailscale activado, y solo accedes al servidor, no al resto de tu LAN doméstica (el Nivel 2 añade eso).
🥉 Nivel 2: Router de subred para toda tu LAN doméstica
Ideal para acceder a todos los dispositivos de casa, no solo a LibreFolio. El servidor se convierte en un router de subred: con Tailscale activado, tu cliente abre cualquier IP local como si estuviera en casa, por ejemplo http://192.168.1.2:6040 para LibreFolio.
graph LR
Client["👤 Client (VPN active)<br>(100.x.y.z)"] -->|WireGuard Tunneling| Server["🖥️ Server (Subnet Router)<br>(100.a.b.c)"]
subgraph LAN ["Local LAN Network (192.168.1.0/24)"]
Server -->|Local forwarding| LibreFolio["📊 LibreFolio<br>(e.g. 192.168.1.2:6040)"]
Server -->|Local forwarding| OtherDevice["🖨️ Other Devices/Services<br>(e.g. 192.168.1.100)"]
end
style LibreFolio fill:#d4edda,stroke:#28a745,stroke-width:2px;
🔀 Paso 1: Habilitar el enrutamiento de subred en el servidor
Habilita el reenvío de IP a nivel del kernel:
echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
echo 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
sudo sysctl -p /etc/sysctl.d/99-tailscale.conf
Empieza a anunciar la subred (reemplaza el rango de IP por tu red local, p. ej., 192.168.1.0/24):
Usa la ruta del ejecutable de Tailscale para anunciar la subred local:
✅ Paso 2: Aprobar la ruta en la consola de administración
- Ve a la Consola de administración de Tailscale.
- Haz clic en los tres puntos junto a tu servidor -> Editar configuración de ruta.
- Habilita la subred anunciada.
Un router de subred forma parte de la infraestructura de tu red: si te lo saltaste, desactiva ahora la caducidad de la clave para el servidor (consejo al final del Paso 0).
⚠️ Límites: el cliente sigue necesitando Tailscale activado, debes conocer las IP locales de tus dispositivos y, dentro de tu LAN, el tráfico viaja como HTTP sin cifrar.
🔑 Habilitar Funnel y ACLs en la consola
Configuración única, necesaria para los Niveles 3 y 4. Permite Funnel en las reglas de control de acceso (ACLs) de toda tu tailnet.
🔐 Antes de hacerlo público
Con Funnel, cualquiera en internet puede abrir tu página de inicio de sesión de LibreFolio. Crea tu propia cuenta primero: la primera cuenta registrada se convierte en administradora. Los registros permanecen abiertos después de eso (Habilitar registro, enable_registration, está activado de forma predeterminada): desactívalo en la Configuración global si no quieres que extraños se registren (consulta Configuración global).
- Visita la página Controles de acceso en la consola de administración de Tailscale.
- Haz clic en el botón Añadir atributo de nodo.
- Completa el formulario:
- Objetivos: los nodos autorizados a usar Funnel. Sugerimos
tag:external_access(darás esta etiqueta a los contenedores Docker del Nivel 4) oautogroup:member(todos los dispositivos registrados en tu cuenta personal). - Atributos: introduce
funnel. - Nota: unas palabras sobre por qué existe la regla.
- Grupos de IP, App, Capacidad, etc.: no son necesarios aquí; déjalos vacíos o con sus valores predeterminados.
- Objetivos: los nodos autorizados a usar Funnel. Sugerimos

Esta regla no es una clave de autenticación: las claves de autenticación (Nivel 4) solo registran un nuevo dispositivo o contenedor en tu tailnet.
📄 Ver la configuración JSON completa de ACL para habilitar Funnel
Si prefieres editar el archivo de políticas directamente, este ejemplo funcional habilita Funnel para tus propios dispositivos y para los contenedores etiquetados con tag:external_access:
{
// Declaration of authorized tags
"tagOwners": {
"tag:external_access": ["autogroup:admin"]
},
// Standard access rules
"acls": [
// Allows all nodes in your private network to communicate
{"action": "accept", "src": ["*"], "dst": ["*:*"]}
],
"ssh": [
{
"action": "check",
"src": ["autogroup:member"],
"dst": ["autogroup:self"],
"users": ["autogroup:nonroot", "root"]
}
],
// Enabling Funnel on specific nodes or tags
"nodeAttrs": [
{
"target": ["autogroup:member"],
"attr": ["funnel"]
},
{
"target": ["tag:external_access"],
"attr": ["funnel"]
}
]
}
🥈 Nivel 3: Dirección HTTPS pública con Tailscale Funnel
Ideal para una dirección pública, sin VPN en el cliente. Funnel publica LibreFolio en una dirección segura https://<server-name>.your-tailnet.ts.net que cualquiera puede abrir sin instalar Tailscale. HTTPS también es lo que te permite instalar LibreFolio como app (PWA) en tu teléfono.
Antes de empezar: completa la configuración única de Funnel y ACL en la consola.
graph LR
User["👤 User (No VPN)"] -->|HTTPS Request| Funnel["☁️ Tailscale Funnel Ingress<br>(Tailscale Public Server)"]
Funnel -->|WireGuard Tunneling| Server["🖥️ Local Server (tailscaled)<br>(100.a.b.c)"]
subgraph LAN ["Local LAN Network"]
Server -->|Local forwarding| LibreFolio["📊 LibreFolio (Port 6040)"]
Server -.->|"<font color='red'><b>Cannot expose</b></font>"| Other["🔌 Other Local Services (Different ports)"]
end
style LibreFolio fill:#d4edda,stroke:#28a745,stroke-width:2px;
style Other fill:#f8d7da,stroke:#dc3545,stroke-width:2px;
linkStyle 3 stroke:#dc3545,stroke-width:2px;
▶️ Paso 1: Iniciar el Funnel en el servidor
En el servidor, publica el puerto local de LibreFolio:
Funnel lo sirve en https://<server-name>.your-tailnet.ts.net en el puerto 443; --bg lo mantiene en ejecución después de cerrar la terminal, y tailscale funnel reset lo detiene. Aquí no se necesita ninguna clave de autenticación: el servidor ya se unió a tu tailnet en el Paso 0.
Tampoco hay nada que configurar en LibreFolio: Funnel envía X-Forwarded-Proto: https, así que con el valor predeterminado SESSION_COOKIE_SECURE=auto LibreFolio marca su cookie de sesión como Secure. never es solo para un proxy que declara HTTPS a un navegador en HTTP sin cifrar.
✅ Paso 2: Aprobar y esperar la propagación
La primera vez, la terminal advierte de que Funnel todavía no está permitido para este nodo y muestra un enlace como este:
Funnel is enabled, but the list of allowed nodes in the tailnet policy file does not include the one you are using.
To give access to this node you can edit the tailnet policy file, or visit:
https://login.tailscale.com/f/funnel?node=xxxxxx
- Abre el enlace en tu navegador, inicia sesión en Tailscale y aprueba Funnel para este nodo.
- La terminal mostrará entonces tu URL pública.
- Espera unos minutos a que se propaguen los registros MagicDNS antes de abrirla desde una red externa.
Para comprobar el resultado, consulta Comprobar la seguridad de la conexión.
⚠️ Límites: una máquina tiene un solo nombre público, que deben compartir todos los servicios que publiques desde ella. El Nivel 4 le da a cada servicio el suyo.
🥇 Nivel 4: Multi-Funnel con sidecars de Docker
Ideal para usuarios de Docker que quieren una dirección pública por servicio. Cada servicio obtiene un pequeño contenedor de Tailscale (un sidecar) que se une a tu tailnet como su propio nodo, con su propia dirección https://<name>.your-tailnet.ts.net. Un script de inicio instala socat en el sidecar, y socat reenvía el tráfico de Funnel a la IP estática de la LAN del servicio.
Antes de empezar: completa la configuración única de Funnel y ACL en la consola.
🧰 ¿Qué es socat?
socat (SOcket CAT) es una pequeña herramienta de línea de comandos que retransmite datos entre dos conexiones. Aquí funciona como un mini reenviador: escucha en un puerto dentro del contenedor de Tailscale y pasa todo lo que recibe al puerto real del servicio en tu LAN.
Añade un sidecar por cada servicio que quieras publicar, en un host o en varios; el único límite es el número de dispositivos etiquetados que permita tu plan de Tailscale. En este ejemplo, dos hosts ejecutan dos sidecars cada uno:
graph LR
User["👤 External User"] -->|HTTPS| Funnel1["☁️ Funnel 1<br>(librefolio.yourtailnet.ts.net)"]
User -->|HTTPS| Funnel2["☁️ Funnel 2<br>(service1.yourtailnet.ts.net)"]
User -->|HTTPS| Funnel3["☁️ Funnel 3<br>(service3.yourtailnet.ts.net)"]
User -->|HTTPS| Funnel4["☁️ Funnel 4<br>(service4.yourtailnet.ts.net)"]
Funnel1 -->|WireGuard| TSC1["🐳 Tailscale Container 1<br>(100.101.x.x)"]
Funnel2 -->|WireGuard| TSC2["🐳 Tailscale Container 2<br>(100.102.x.x)"]
Funnel3 -->|WireGuard| TSC3["🐳 Tailscale Container 3<br>(100.103.x.x)"]
Funnel4 -->|WireGuard| TSC4["🐳 Tailscale Container 4<br>(100.104.x.x)"]
subgraph LAN ["Local LAN Network (192.168.1.0/24)"]
subgraph Host2 ["Server 2 (e.g. Mini PC - 192.168.1.10)"]
TSC3 -->|socat: TCP/8080| Service3["🔌 Service 3<br>(192.168.1.10:8080)"]
TSC4 -->|socat: TCP/9000| Service4["🔌 Service 4<br>(192.168.1.10:9000)"]
end
subgraph Host1 ["Server 1 (e.g. NAS - 192.168.1.20)"]
TSC1 -->|socat: TCP/6040| LibreFolio["📊 LibreFolio<br>(192.168.1.20:6040)"]
TSC2 -->|socat: TCP/80| Service1["🔌 Service 1<br>(192.168.1.20:80)"]
end
end
style LibreFolio fill:#d4edda,stroke:#28a745,stroke-width:2px;
📁 Paso 1: Preparar la carpeta y el script
Crea una carpeta en el servidor, por ejemplo donde guardas tus volúmenes persistentes de Docker:
# Create a folder for the Tailscale nodes and enter it
mkdir -p <path_chosen>/tailscale-nodes
cd <path_chosen>/tailscale-nodes
Luego descarga el script de inicio custom_startup.sh en ella:
# Download the script from the official repository
wget https://raw.githubusercontent.com/Librefolio/LibreFolio/main/mkdocs_src/docs/static/tailscale-guide/custom_startup.sh
# Make the script executable
chmod +x custom_startup.sh
🔄 ¿Configuraste el sidecar antes? Actualiza tu copia del script
El script actual también funciona como watchdog e incluye una comprobación de estado de Docker (consulta el Paso 2). Si tu sidecar ejecuta una copia antigua, actualízala:
-
Descarga el script de nuevo en la misma carpeta. La opción
-O custom_startup.shsobrescribe el archivo antiguo (sin ella,wgetguarda la descarga comocustom_startup.sh.1). Luego asegúrate de que el script sea ejecutable: -
Actualiza tu archivo compose: añade
TS_ENABLE_HEALTH_CHECK,TS_LOCAL_ADDR_PORTy, opcionalmente,STARTUP_TIMEOUTal servicio de Tailscale, junto con el bloquehealthcheck, exactamente como en el Paso 2. -
Vuelve a crear el contenedor: reiniciar no aplica los cambios del compose. Ejecuta
docker compose up -den la carpeta de tudocker-compose.yml(vuelve a crear los servicios cuya configuración cambió), o usa la acción Recreate / volver a desplegar de Portainer o CasaOS. -
Comprueba el registro con
docker logs -f tailscale-librefolio: deberías verTailscale is running., luegoStarting the funnel on port 6040...yAvailable on the internet:con tu URL pública. Dentro delstart_periodde 2 minutos de la comprobación de estado, Docker muestra el contenedor como healthy (docker ps, Portainer, CasaOS). Si en cambio se reinicia continuamente, consulta el panel de solución de problemas en el Paso 3.
🐳 Paso 2: Configurar Docker Compose
Añade el servicio de Tailscale al mismo docker-compose.yml que el servicio que expone (por ejemplo, LibreFolio), para que ambos permanezcan juntos:
services:
tailscale-librefolio:
image: tailscale/tailscale:latest
container_name: tailscale-librefolio
hostname: tailscale-librefolio
restart: unless-stopped
privileged: false
network_mode: bridge
cap_add:
- NET_ADMIN
- NET_RAW
devices:
- /dev/net/tun:/dev/net/tun
command:
- /custom_startup.sh
environment:
- HOST_IP=192.168.1.20 # Local IP of the service to expose (e.g. Server 1)
- HOST_PORT=6040 # Real port of the service to expose
- TAILSCALE_FUNNEL_PORT=6040 # Internal Funnel port
- TS_HOSTNAME=librefolio # Custom public hostname (e.g. librefolio)
- TS_AUTHKEY=tskey-auth-... # Authentication key generated by Tailscale
- TS_ACCEPT_DNS=true
- TS_STATE_DIR=/var/lib/tailscale
- TS_USERSPACE=false
- TS_ENABLE_HEALTH_CHECK=true # Expose /healthz for the healthcheck below (Tailscale ≥ 1.78)
- TS_LOCAL_ADDR_PORT=127.0.0.1:9002 # Where /healthz listens: inside the container only
- STARTUP_TIMEOUT=180 # Optional: seconds to reach the Running state (default 180)
volumes:
- <path_chosen>/tailscale-nodes/tailscale-librefolio/state:/var/lib/tailscale
- <path_chosen>/tailscale-nodes/custom_startup.sh:/custom_startup.sh
- /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro
# Shows healthy/unhealthy in Docker, Portainer or CasaOS; the restart itself comes from custom_startup.sh exiting
healthcheck:
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:9002/healthz"]
interval: 30s
timeout: 5s
retries: 3
start_period: 120s
Luego establece estos valores para tu red:
| Valor | Qué poner |
|---|---|
<path_chosen> |
La ruta absoluta elegida en el Paso 1, donde viven el script y los datos de estado (p. ej., /home/user/docker). |
HOST_IP |
La IP estática de la LAN de la máquina que ejecuta el servicio. |
HOST_PORT |
El puerto real del servicio en esa máquina: para LibreFolio, el PORT de tu .env (6040 de forma predeterminada). |
TAILSCALE_FUNNEL_PORT |
El puerto en el que escucha el contenedor y que publica a través de Funnel. Establécelo con el mismo valor que HOST_PORT. |
TS_HOSTNAME |
El nombre del nodo: la dirección pública pasa a ser https://TS_HOSTNAME.your-tailnet.ts.net. |
TS_AUTHKEY |
La clave de autenticación que registra el contenedor en tu tailnet (ver más abajo). |
Para obtener la clave de autenticación para TS_AUTHKEY:
- Ve a Claves de configuración de administración de Tailscale.
- En la sección Claves de autenticación (no en la sección de tokens de acceso a la API), haz clic en el botón Generar clave de autenticación....
- Activa el interruptor Etiquetas y selecciona tu etiqueta (p. ej.,
tag:external_access). Añade una descripción que reconozcas, comodocker-librefolio-funnel. - Haz clic en Generar y copia la clave (
tskey-auth-...).
Una vez iniciado el contenedor, la clave de un solo uso se consume: desaparece de la lista Claves y el nuevo dispositivo aparece en Máquinas.
🩺 Watchdog y comprobación de estado — qué hace la configuración adicional
El script de inicio también funciona como watchdog, mientras que el bloque healthcheck hace visible el estado del contenedor:
- Watchdog (reinicio automático): si Tailscale no puede iniciarse (por ejemplo, no hay conexión a internet al arrancar o una opción incorrecta), no alcanza el estado Running dentro de
STARTUP_TIMEOUTsegundos, o si Tailscale, socat o el Funnel se detienen más tarde, el script sale con un error y Docker reinicia el contenedor (restart: unless-stopped). El contenedor nunca se queda "en ejecución" sin nada detrás, y undocker stopsigue apagando todo limpiamente. - Comprobación de estado (solo estado):
TS_ENABLE_HEALTH_CHECK=trueactiva el endpoint/healthzde Tailscale (Tailscale 1.78 o posterior), que responde200mientras el nodo tiene una dirección IP de Tailscale y503en caso contrario. Docker lo consulta regularmente y marca el contenedor como healthy o unhealthy; Portainer y CasaOS muestran el mismo estado.
Docker simple (fuera del modo Swarm) no reinicia un contenedor marcado como unhealthy: el reinicio proviene de que el script salga, así que no necesitas un contenedor "autoheal" adicional (tales ayudantes también necesitan el socket de Docker, lo que implica control total del host).
| Opción de configuración | Qué hace |
|---|---|
TS_LOCAL_ADDR_PORT |
Donde escucha /healthz. El endpoint no necesita autenticación, y el valor predeterminado de Tailscale, [::]:9002, escucha en todas las interfaces: 127.0.0.1:9002 lo mantiene dentro del contenedor. Si lo cambias, actualiza también la URL de la prueba healthcheck. |
STARTUP_TIMEOUT |
Segundos que el script espera al estado Running (predeterminado 180) antes de salir y que Docker reinicie el contenedor. Auméntalo solo en un servidor muy lento. |
DEBUG |
No está en el ejemplo anterior. DEBUG=1 imprime cada comando que ejecuta el script en el registro del contenedor, para solución de problemas. Desactivado de forma predeterminada para mantener el registro legible. |
📄 Ver el archivo Docker Compose de producción completo (LibreFolio + Tailscale)
Un docker-compose.yml completo: el servicio librefolio del docker-compose.prod.yml oficial, con el sidecar de Tailscale al lado:
# =============================================================================
# LibreFolio — Production Docker Compose
# =============================================================================
# Optimized for end-users running the official pre-built image from GHCR.
# =============================================================================
services:
librefolio:
image: ${LIBREFOLIO_IMAGE:-ghcr.io/librefolio/librefolio:latest}
container_name: librefolio
restart: unless-stopped
ports:
- "${PORT:-6040}:6040"
volumes:
- ./LibreFolio-data:/app/backend/data/prod-docker
env_file: .env
environment:
- LIBREFOLIO_DATA_DIR=/app/backend/data/prod-docker
- HOST=0.0.0.0
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:6040/api/v1/system/health')"]
interval: 30s
timeout: 10s
start_period: 15s
retries: 3
tailscale-librefolio:
image: tailscale/tailscale:latest
container_name: tailscale-librefolio
hostname: tailscale-librefolio
restart: unless-stopped
privileged: false
network_mode: bridge
cap_add:
- NET_ADMIN
- NET_RAW
devices:
- /dev/net/tun:/dev/net/tun
command:
- /custom_startup.sh
environment:
- HOST_IP=192.168.1.20 # Local IP of the service to expose (e.g. Server 1)
- HOST_PORT=6040 # Real port of the service to expose
- TAILSCALE_FUNNEL_PORT=6040 # Internal Funnel port
- TS_HOSTNAME=librefolio # Custom public hostname (e.g. librefolio)
- TS_AUTHKEY=tskey-auth-... # Replace with your generated key
- TS_ACCEPT_DNS=true
- TS_STATE_DIR=/var/lib/tailscale
- TS_USERSPACE=false
- TS_ENABLE_HEALTH_CHECK=true # Expose /healthz for the healthcheck below (Tailscale ≥ 1.78)
- TS_LOCAL_ADDR_PORT=127.0.0.1:9002 # Where /healthz listens: inside the container only
- STARTUP_TIMEOUT=180 # Optional: seconds to reach the Running state (default 180)
volumes:
- /DATA/AppData/tailscale-nodes/tailscale-librefolio/state:/var/lib/tailscale
- /DATA/AppData/tailscale-nodes/custom_startup.sh:/custom_startup.sh
- /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro
# Shows healthy/unhealthy in Docker, Portainer or CasaOS; the restart itself comes from custom_startup.sh exiting
healthcheck:
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:9002/healthz"]
interval: 30s
timeout: 5s
retries: 3
start_period: 120s
🚀 Paso 3: Iniciar y aprobar el Funnel
Inicia el stack, el servicio y su sidecar de Tailscale juntos:
Luego sigue el registro del contenedor de Tailscale:
En el primer inicio, el registro muestra el enlace de aprobación para el nuevo nodo:
Funnel is enabled, but the list of allowed nodes in the tailnet policy file does not include the one you are using.
To give access to this node you can edit the tailnet policy file, or visit:
https://login.tailscale.com/f/funnel?node=nsKGo6k9ZF11CNTRL
- Abre el enlace en tu navegador, inicia sesión en Tailscale y aprueba la activación de Funnel.
- Justo después de la aprobación, el registro confirma la URL pública y el proxy local:
Available on the internet:
https://librefolio.yourtailnet.ts.net/
|-- proxy http://127.0.0.1:6040
Press Ctrl+C to exit.
El servicio ya está en línea: espera unos minutos a que se propaguen los registros MagicDNS y luego abre la URL.
No hay nada que configurar en LibreFolio: Funnel envía X-Forwarded-Proto: https y socat lo transmite, así que con el valor predeterminado SESSION_COOKIE_SECURE=auto LibreFolio marca su cookie de sesión como Secure. never es solo para un proxy que declara HTTPS a un navegador en HTTP sin cifrar.
Para comprobar el resultado, consulta Comprobar la seguridad de la conexión.
🛠️ El contenedor se reinicia en bucle o está marcado como unhealthy
Un problema persistente se manifiesta como un contenedor que se reinicia continuamente (Docker, Portainer o CasaOS también pueden marcarlo como unhealthy), no como uno que parece "en ejecución" pero no funciona. Lee el registro con docker logs tailscale-librefolio: antes de cada reinicio, el script imprime lo que salió mal, normalmente seguido de Exiting so Docker restarts the container. Las causas más comunes:
- Una opción en
TS_EXTRA_ARGSno tiene valor. Esta variable opcional (no está en el compose anterior) pasa opciones adicionales atailscale up, divididas por espacios. Una opción sin su valor hace quetailscale upfalle: el motivo está en la línea justo después deRunning 'tailscale up', encima del texto de ayuda que empieza porUSAGE(por ejemplo,flag needs an argument: -advertise-tags), seguido defailed to auth tailscale: … tailscale up failed: exit status 2ycontainerboot exited before Tailscale was running.Escribe cada valor como--flag=valueo--flag value:--advertise-tags=tag:containery--advertise-tags tag:containerfuncionan ambos. - Un panel de gestión cortó el valor. El editor de variables de entorno de CasaOS (y de paneles similares) trunca un valor en su segundo
=:TS_EXTRA_ARGS=--advertise-tags=tag:containerse convierte en--advertise-tags, lo que falla como se describió anteriormente. En estos paneles, escribe los valores de las opciones con un espacio:--advertise-tags tag:container. - Falta una variable obligatoria: el script se detiene de inmediato con
HOST_IP is not set(o el mismo mensaje paraHOST_PORToTAILSCALE_FUNNEL_PORT). - Sin conexión a internet al arrancar: el contenedor se reinicia continuamente hasta que Tailscale puede iniciarse, y luego funciona con normalidad. Esto es esperado.
- Inicio muy lento: el registro muestra
Tailscale is not running after 180s.; aumentaSTARTUP_TIMEOUT. - Unhealthy, pero sin reiniciarse: la comprobación de estado no obtiene una respuesta correcta de
/healthz. Si acabas de añadirla, asegúrate de queTS_ENABLE_HEALTH_CHECK=trueesté establecido y de que la URL de la pruebahealthcheckcoincida conTS_LOCAL_ADDR_PORT; de lo contrario, el nodo no tiene una dirección IP de Tailscale en ese momento.
Para rastrear cada comando del script, añade DEBUG=1 a la sección environment, vuelve a crear el contenedor y lee el registro de nuevo.
Como su clave de autenticación lleva una etiqueta, Tailscale desactiva la caducidad de la clave para el contenedor de forma predeterminada. Con una clave sin etiqueta, desactívala como para el servidor (consejo al final del Paso 0).
⚠️ Límites: requiere una terminal y algo de edición de archivos Docker Compose.
🔮 MagicDNS y dominios personalizados
MagicDNS da a cada dispositivo de tu tailnet un nombre: en lugar de una IP como 100.110.x.x, puedes escribir http://your-server en el navegador. Las direcciones públicas de Funnel terminan en .ts.net (por ejemplo, https://librefolio.your-tailnet.ts.net, donde librefolio es el TS_HOSTNAME del Nivel 4).
¿Prefieres tu propio dominio, como librefolio.mydomain.com? Dos métodos funcionan para acceso privado, a través de la VPN:
🌍 Método 1 — Un registro DNS público que apunta a la IP de Tailscale (el más simple)
- Inicia sesión en la consola de tu registrador de dominios (p. ej., Cloudflare, GoDaddy, Namecheap).
- Crea un registro DNS de tipo A (o AAAA para IPv6) para el subdominio elegido (p. ej.,
librefolio.mydomain.com). - Apunta el registro directamente a la IP privada de Tailscale de tu servidor (p. ej.,
100.77.x.x).
Las direcciones de la red 100.64.0.0/10 no son enrutables en internet, por lo que el nombre funciona solo mientras estás conectado a tu tailnet: ningún externo puede alcanzar ni escanear el servicio. Para más detalles, consulta la Documentación oficial sobre configuración de DNS.
🧭 Método 2 — Split DNS con tu propio servidor DNS
Para registros internos que gestionas tú mismo y nunca publicas en internet:
- Configura un servidor DNS privado en tu LAN (como Pi-hole, AdGuard Home o CoreDNS).
- Añade registros locales de tu dominio apuntándolos a tus IP de Tailscale.
- En la consola de administración de Tailscale, ve a DNS -> Servidores de nombres -> Añadir servidor de nombres y añade la IP de Tailscale de tu DNS privado como servidor de nombres global o restringido a tu dominio. Para más detalles, consulta la Documentación oficial sobre Split DNS.
Para acceso público, conserva la dirección *.ts.net: Funnel la sirve con un certificado firmado para ese nombre, así que apuntar tu propio dominio a ella (CNAME) provoca errores SSL/TLS en los navegadores, a menos que añadas tu propio proxy inverso (como Caddy o Nginx) con certificados para tu dominio.