Saltar al contenido principal

Despliegue remoto privado

Este patrón ejecuta Libre WebUI, Ollama y Cloudflare Tunnel en un host Docker sin publicar los puertos de la aplicación ni de Ollama. Cloudflare Access es el límite de identidad exterior; la autenticación de Libre WebUI permanece como límite interior. Work y Watchtower son opciones separadas, equivalentes a root.

La plantilla corresponde a la topología solo de una réplica: SQLite, blobs locales cifrados, vectores integrados, coordinación local y un worker integrado comparten el volumen de datos. No la conviertas en un despliegue de equipo cambiando selectores del backend en .env. Los despliegues de equipo deben usar docker-compose.team.yml (y docker-compose.team.work.yml con Work), que provisiona PostgreSQL/PGVector, almacenamiento S3 versionado, Redis, un worker externo y la puerta como una topología coordinada.

Usa deploy/private/docker-compose.yml como punto de partida. Usa la imagen main por defecto:

LIBRE_WEBUI_IMAGE=ghcr.io/libre-webui/libre-webui:main

La etiqueta dev sirve para una instancia de desarrollo elegida expresamente, no para el cliente predeterminado.

Modelo de seguridad

  • Cloudflare Access protege todo el nombre de host, incluido /api/* y las actualizaciones WebSocket. No añadas rutas públicas de omisión.
  • Libre WebUI exige una cuenta actual para las API. Las operaciones de modelos y Work requieren que el rol actual sea administrador.
  • La aplicación, Ollama, SearXNG y cloudflared solo usan una red Compose privada. El host no publica puertos de la aplicación.
  • El servicio SearXNG integrado impulsa la búsqueda web opcional. Es interno e inerte hasta que un administrador la habilita; define SEARXNG_SECRET en .env antes de iniciar la pila.
  • La aplicación se ejecuta sin root, con raíz de solo lectura, sin capacidades Linux, no-new-privileges y límites de CPU, memoria y PID.
  • Work está desactivado salvo que se incluya una de sus sustituciones. Al activarlo, sus contenedores añaden raíz de solo lectura, retirada de capacidades, límites, volumen de trabajo y política de red que deniega por defecto.

La pila base no monta ningún socket. Activar Work con docker-compose.work-proxy.yml lo mantiene así: un proxy interno conserva el socket y solo remite las secciones usadas (contenedores, imágenes, volúmenes, redes, exec e información); swarm, secretos, compilación y sistema se deniegan, y la aplicación no necesita montar el socket ni pertenecer a su grupo. El proxy reduce la superficie de la API, no el alcance de daño de lo que permite: quien crea contenedores aún puede montar rutas del host. Trátalo como refuerzo real, no como aislamiento multiinquilino.

Las alternativas de socket sin filtrar siguen siendo el mayor límite de confianza: docker-compose.work.yml y Watchtower permiten llamadas arbitrarias a Docker que pueden controlar el host. Un montaje de solo lectura no hace que la API sea de solo lectura. El asistente de copias integrado se niega a heredar un socket sin filtrar; migra Work al proxy antes de confiar en copias programadas.

Preparación inicial

  1. Crea un operador sudo sin root y verifica el acceso SSH por clave antes de desactivar SSH para root.
  2. Copia deploy/private/.env.example a /opt/libre-webui/.env, establece el modo 0600, genera secretos únicos y dimensiona BLOB_QUOTA_BYTES_PER_USER. BLOB_QUOTA_RESERVATION_TTL_MS hace caducar reservas abandonadas; el valor predeterminado es una hora.
  3. Si habilitarás Work, define DOCKER_GID como el grupo numérico propietario de /var/run/docker.sock.
  4. Guarda el token del túnel en /opt/libre-webui/secrets/tunnel-token con modo 0640 o más estricto.
  5. Crea una aplicación autoalojada de Cloudflare Access para el nombre completo, usa una sesión de 24 horas y permite solo las identidades previstas. Activa Protect with Access en la ruta. Si la supervisión requiere un control público, crea una política separada limitada a /health/live. Nunca añadas una política Bypass general: las coincidencias derrotan la política Allow.
  6. Mantén ENABLE_SIGNUP=false. Una vez protegido el host, crea el primer administrador local; una base vacía permite automáticamente esa única cuenta inicial. Habilita el registro solo durante una ventana deliberada posterior.
  7. Configura las restricciones de Turnstile y define TURNSTILE_EXPECTED_HOSTNAME como el nombre público exacto.

Inicia y verifica:

cd /opt/libre-webui
docker compose config --quiet
docker compose up -d
docker compose ps

Para activar Work, incluye expresamente la sustitución del proxy:

docker compose -f docker-compose.yml -f docker-compose.work-proxy.yml up -d

La variante con socket sin filtrar (docker-compose.work.yml) sigue disponible con las consecuencias de confianza descritas.

Cuando Access esté activo, las pruebas por línea de comandos requieren un token de servicio salvo que la ruta tenga una omisión estrecha. Guarda las credenciales fuera del historial y envía ambas cabeceras:

curl --fail --silent --show-error \
-H "CF-Access-Client-Id: $CF_ACCESS_CLIENT_ID" \
-H "CF-Access-Client-Secret: $CF_ACCESS_CLIENT_SECRET" \
https://your-hostname.example/api/auth/system-info

Una solicitud no autenticada a una API protegida debe devolver 401:

curl --output /dev/null --write-out '%{http_code}\n' \
-H "CF-Access-Client-Id: $CF_ACCESS_CLIENT_ID" \
-H "CF-Access-Client-Secret: $CF_ACCESS_CLIENT_SECRET" \
https://your-hostname.example/api/work/tasks

Refuerzo del host

El directorio contiene una inclusión de sshd y una cárcel de fail2ban. Antes de aplicarla, verifica otra sesión sudo sin root en una terminal distinta. Prueba la configuración con sshd -t antes de recargar SSH.

Usa UFW o un cortafuegos equivalente para denegar por defecto y permitir solo SSH con velocidad limitada. Docker no publica puertos de servicio en esta plantilla:

ufw default deny incoming
ufw default allow outgoing
ufw limit OpenSSH
ufw enable

Mantén activadas las actualizaciones de seguridad desatendidas. Desactiva el reenvío X11, de agente y TCP salvo necesidad documentada.

Copias de seguridad y recuperación

Antes de copiar, ejecuta el inventario de recuperación de solo lectura dentro del contenedor desplegado. Así usa exactamente la versión, entorno y volumen montado. Un comando desde un checkout del host podría inspeccionar otra base o código distinto.

docker exec libre-webui \
libre-webui recovery-check --json --data-dir /app/backend/data

El estado 0 indica que no hay bloqueos, 1 que el informe JSON contiene bloqueos y 2 que el comando no pudo ejecutarse. El informe solo incluye la huella de la clave y señales de presencia de secretos; nunca imprime valores. Consérvalo con la copia para comparar versión, huella del esquema, recursos Work esperados y exclusiones.

Crea claves de cifrado y firma dedicadas con la imagen exacta. Mantén el directorio fuera del volumen y copia la clave de cifrado y la privada de firma a otro lugar:

install -d -m 0700 /etc/libre-webui/backup-keys
image_ref=$(docker inspect libre-webui --format '{{.Image}}')
docker run --rm --user 0:0 --read-only --network none --cap-drop ALL \
--security-opt no-new-privileges \
--mount type=bind,src=/etc/libre-webui/backup-keys,dst=/backup-keys \
--entrypoint /usr/local/bin/libre-webui "$image_ref" \
backup keygen \
--directory /backup-keys

La generación rechaza archivos existentes. Nunca generes claves nuevas sobre un conjunto existente: perder la clave del archivo o la identidad de firma inutiliza la prueba de recuperación.

Instala los scripts y unidades systemd y activa el temporizador:

install -d -m 0700 /var/backups/libre-webui
install -m 0750 deploy/private/libre-webui-backup \
/usr/local/sbin/libre-webui-backup
install -m 0750 deploy/private/libre-webui-restore \
/usr/local/sbin/libre-webui-restore
install -m 0644 deploy/private/libre-webui-backup.{service,timer} \
/etc/systemd/system/
systemctl daemon-reload
systemctl enable --now libre-webui-backup.timer

La unidad puede leer sustituciones exclusivas de mantenimiento desde /etc/libre-webui/backup.env; no carga .env de la aplicación. Crea el archivo como root solo si hace falta:

install -d -m 0750 /etc/libre-webui
install -m 0600 /dev/null /etc/libre-webui/backup.env

Se pueden definir LIBRE_WEBUI_STACK_DIR, LIBRE_WEBUI_BACKUP_RETENTION_DAYS, LIBRE_WEBUI_CONTAINER_NAME y LIBRE_WEBUI_BACKUP_KEY_DIR. Mantén el archivo como root y modo 0600. Un directorio personalizado debe ser legible por root dentro del aislamiento systemd.

Cambiar LIBRE_WEBUI_BACKUP_DIR también cambia el límite de escritura de systemd. El directorio debe existir y la unidad necesita una inclusión coincidente. Tras definir LIBRE_WEBUI_BACKUP_DIR=/srv/backups/libre-webui en backup.env:

install -d -m 0700 /srv/backups/libre-webui
systemctl edit libre-webui-backup.service

Añade esta ruta exacta y recarga:

[Service]
ReadWritePaths=/srv/backups/libre-webui
systemctl daemon-reload
systemctl start libre-webui-backup.service

Sin ReadWritePaths= coincidente, ProtectSystem=strict impide correctamente escribir en la ubicación personalizada.

El servicio permite hasta seis horas. El asistente obtiene un bloqueo del host, detiene la aplicación solo si ya estaba activa y crea el archivo sobre el volumen en reposo con la imagen exacta. El archivo incluye un manifiesto firmado, carga cifrada, el directorio y la configuración necesaria. Luego verifica independientemente todo antes de publicar el informe de forma atómica. Los contenedores de mantenimiento de solo lectura reciben un tmpfs /tmp privado para SQLite y la verificación; no se conserva texto sin cifrar en la capa. Copia ambos archivos y las claves fuera del host.

Cuando Work usa docker-compose.work-proxy.yml, la recuperación debe probar que existe cada volumen referido por la base. El asistente lee DOCKER_HOST, localiza el proxy del mismo proyecto y descubre su única red interna compartida a partir de las conexiones reales. Compose antepone el nombre del proyecto: no codifiques un nombre supuesto. Solo el contenedor creador se une a esa red y alcanza el proxy; no recibe socket sin filtrar. La verificación independiente usa --network none. La ausencia del proxy, un endpoint inesperado, una red externa o ambigua o un montaje sin filtrar fallan antes de detener la aplicación o publicar el archivo.

Prueba la recuperación en un volumen nuevo sin sustituir el activo:

LIBRE_WEBUI_RESTORE_IMAGE="$image_ref" \
libre-webui-restore \
/var/backups/libre-webui/libre-webui-integrated-YYYYMMDDTHHMMSSZ.lwb \
libre-webui-restore-drill

El asistente rechaza un volumen o destino existente, verifica el archivo y su inventario en almacenamiento desechable, copia los datos y escribe runtime.json y secrets.json con permisos privados. Nunca reconecta ni inicia la pila activa. Inspecciona la configuración, actualiza deliberadamente valores específicos y prueba el volumen con una pila aislada.

Los modelos Ollama pueden descargarse de nuevo. Los volúmenes Work, PVC y carpetas vinculadas al host están fuera del directorio y necesitan instantáneas y conservación coordinadas propias.

Actualizaciones

Libre WebUI tiene estado aunque la etiqueta de imagen sea mutable. Compose excluye permanentemente la aplicación de Watchtower. Actualízala solo de forma coordinada:

  1. Registra el ID de la imagen y resuelve el sustituto revisado a un digest inmutable.
  2. Ejecuta libre-webui recovery-check, inicia la copia y exige un archivo e informe nuevos antes de continuar.
  3. Define LIBRE_WEBUI_IMAGE con el digest, descárgalo y recrea solo libre-webui. No elimines ni recrees el volumen.
  4. Exige que pasen /health/ready, inicio de sesión, sesión e historial, recuperación de documentos y pruebas de Work. Si no, vuelve al digest registrado y conserva el estado fallido y la copia para diagnóstico.

La secuencia del host es manual. Sustituye el digest solo después de revisarlo e inspecciona el par .lwb y .json más reciente antes de descargar:

docker inspect libre-webui --format '{{.Config.Image}} {{.Image}}'
docker exec libre-webui \
libre-webui recovery-check --json --data-dir /app/backend/data
systemctl start libre-webui-backup.service
systemctl --no-pager --full status libre-webui-backup.service
ls -lt /var/backups/libre-webui/libre-webui-integrated-* | head

# Set LIBRE_WEBUI_IMAGE=ghcr.io/libre-webui/libre-webui@sha256:REVIEWED_DIGEST
# in the root-owned .env, then recreate only the application.
docker compose pull libre-webui
docker compose up -d --no-deps libre-webui
docker inspect libre-webui --format '{{.State.Health.Status}} {{.Image}}'

La sustitución opcional de Watchtower con socket queda disponible solo para los sidecars etiquetados expresamente:

docker compose \
-f docker-compose.yml \
-f docker-compose.watchtower.yml \
up -d

Watchtower comprueba Ollama y SearXNG cada 30 minutos. Los modelos de Ollama permanecen en su volumen y la configuración de SearXNG en su montaje. No actualiza Libre WebUI, cloudflared, el proxy ni los entornos de Work. Un cliente sigue main; una instancia experimental puede elegir :dev, pero aún exige la misma actualización manual condicionada por la copia. Nunca conectes esta pila individual a servicios de persistencia de equipo; despliega la topología completa.