Déploiement distant privé
Cette architecture exécute Libre WebUI, Ollama et Cloudflare Tunnel sur un même hôte Docker sans publier les ports de l’application ni d’Ollama. Cloudflare Access constitue la frontière d’identité externe ; l’authentification Libre WebUI reste la frontière interne. Work et Watchtower sont des options distinctes, équivalentes à un accès root.
Ce modèle correspond à la topologie solo monoréplique : SQLite, blobs locaux
chiffrés, vecteurs intégrés, coordination locale et worker de tâches intégré
partagent le volume de données de l’application. N’en faites pas un déploiement en
équipe en modifiant les sélecteurs du backend dans .env. Les déploiements en
équipe doivent utiliser le fichier docker-compose.team.yml du dépôt (et
docker-compose.team.work.yml lorsque Work est activé), qui provisionne comme une
topologie coordonnée PostgreSQL/PGVector, un stockage S3 versionné, Redis, un worker
externe et la passerelle.
Prenez comme point de départ
deploy/private/docker-compose.yml.
Il utilise par défaut l’image main :
LIBRE_WEBUI_IMAGE=ghcr.io/libre-webui/libre-webui:main
L’étiquette dev convient à une instance de développement explicitement choisie,
pas à l’instance cliente par défaut.
Modèle de sécurité
- Cloudflare Access protège l’intégralité du nom d’hôte, notamment
/api/*et les mises à niveau WebSocket. N’ajoutez aucun chemin public de contournement. - Libre WebUI exige un compte valide pour les API de l’application. Les opérations de cycle de vie des modèles et de Work exigent que le rôle actuel dans la base de données soit administrateur.
- L’application, Ollama, SearXNG et cloudflared utilisent uniquement un réseau Compose privé. L’hôte ne publie aucun port de l’application.
- Le service SearXNG intégré alimente la recherche web facultative.
Il est exclusivement interne et reste inactif jusqu’à ce qu’un administrateur
active la recherche sous Paramètres > Recherche ; définissez
SEARXNG_SECRETdans.envavant de démarrer la pile. - L’application s’exécute sans root, avec un système de fichiers racine en lecture seule, aucune capacité Linux, no-new-privileges et des limites de CPU, de mémoire et de PID.
- Work est désactivé sauf si l’un de ses remplacements est inclus. Lorsqu’il est activé, ses conteneurs ajoutent leur propre système de fichiers racine en lecture seule, la suppression des capacités, des limites de ressources, un volume d’espace de travail et une politique réseau de refus par défaut.
La pile de base ne monte aucun socket Docker. L’activation de Work avec
docker-compose.work-proxy.yml préserve ce principe : un proxy de socket sur un
réseau interne détient le socket et ne transmet que les sections de l’API utilisées
par Work (conteneurs, images, volumes, réseaux, exec, informations) ; les points de
terminaison liés à swarm, aux secrets, au build et au système sont refusés par le
proxy, et l’application n’a besoin ni de monter le socket ni d’appartenir à son
groupe. Le proxy réduit la surface de l’API Docker, mais pas l’impact potentiel de
ce qu’il transmet : quiconque peut créer des conteneurs peut encore monter des
chemins de l’hôte. Considérez-le donc comme une véritable couche de renforcement,
et non comme une isolation multilocataire.
Les solutions de remplacement avec socket brut restent la plus grande frontière de
confiance : les remplacements docker-compose.work.yml et Watchtower donnent à un
processus de conteneur la possibilité d’émettre n’importe quel appel à l’API Docker,
et donc de contrôler l’hôte. Monter le socket en lecture seule ne rend pas l’accès à
l’API Docker lui-même accessible en lecture seule. L’outil de sauvegarde intégré
refuse d’hériter d’un socket Docker brut ; migrez Work vers le proxy filtré avant de
vous fier aux sauvegardes intégrées planifiées.
Amorçage
- Créez un opérateur sudo sans root et vérifiez la connexion SSH par clé avant de désactiver la connexion SSH de root.
- Copiez
deploy/private/.env.examplevers/opt/libre-webui/.env, appliquez le mode0600, générez des secrets uniques et dimensionnezBLOB_QUOTA_BYTES_PER_USERpour l’hôte.BLOB_QUOTA_RESERVATION_TTL_MSfait expirer les réservations de téléversement abandonnées ; sa valeur par défaut est d’une heure. - Si Work doit être activé, définissez
DOCKER_GIDsur le groupe numérique propriétaire de/var/run/docker.sock. - Stockez le jeton Cloudflare Tunnel dans
/opt/libre-webui/secrets/tunnel-token, avec le mode0640ou plus strict. - Créez une application auto-hébergée Cloudflare Access pour le nom d’hôte complet,
utilisez une session de 24 heures et n’autorisez que les identités prévues.
Activez Protect with Access sur la route du tunnel. Si la surveillance exige
un contrôle d’état public, créez une application ou une politique distincte,
limitée uniquement au chemin
/health/live. N’ajoutez jamais une politique Bypass générale à l’application principale : les politiques Bypass correspondantes neutralisent sa politique Allow. - Conservez
ENABLE_SIGNUP=false. Une fois le nom d’hôte protégé par la liste d’autorisation Access, créez le premier administrateur local ; une base de données vide autorise automatiquement cet unique compte d’amorçage. N’activez l’inscription que pour une future période d’inscription délibérée. - Configurez les restrictions de nom d’hôte Turnstile et définissez
TURNSTILE_EXPECTED_HOSTNAMEsur le nom d’hôte public exact.
Démarrez et vérifiez :
cd /opt/libre-webui
docker compose config --quiet
docker compose up -d
docker compose ps
Pour activer Work, incluez délibérément le remplacement par proxy de socket :
docker compose -f docker-compose.yml -f docker-compose.work-proxy.yml up -d
La variante avec socket brut (docker-compose.work.yml) reste disponible pour les
déploiements qui en ont besoin, avec les conséquences de confiance décrites plus
haut.
Une fois Access actif, les tests rapides en ligne de commande nécessitent un jeton de service Cloudflare Access, sauf si le chemin exact dispose d’un contournement étroit. Stockez les identifiants hors de l’historique du shell et envoyez les deux en-têtes :
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
Une requête non authentifiée à une API d’application protégée doit renvoyer 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
Renforcement de l’hôte
Le répertoire comprend un fragment de configuration sshd et une prison fail2ban.
Avant d’appliquer le fragment sshd, vérifiez dans un autre terminal une session sudo
distincte et sans root. Testez la configuration avec sshd -t avant de recharger
SSH.
Utilisez UFW (ou un pare-feu équivalent) pour refuser par défaut le trafic entrant et n’autoriser que SSH avec limitation du débit. Dans ce modèle, Docker ne publie aucun port de service :
ufw default deny incoming
ufw default allow outgoing
ufw limit OpenSSH
ufw enable
Maintenez les mises à niveau de sécurité automatiques activées. Désactivez les transferts X11, d’agent et TCP, sauf nécessité documentée du déploiement.
Sauvegardes et reprise
Avant une sauvegarde, exécutez l’inventaire de reprise en lecture seule dans le conteneur du déploiement actif. Vous utiliserez ainsi exactement la version de l’application déployée, son environnement et son volume de données monté. Une commande lancée depuis une copie de travail sur l’hôte peut inspecter la mauvaise base de données ou exécuter un code source différent de l’image déployée.
docker exec libre-webui \
libre-webui recovery-check --json --data-dir /app/backend/data
L’état de sortie 0 indique qu’aucun obstacle à la préparation de la reprise n’a
été détecté ; 1 signifie que le rapport JSON contient des obstacles et 2 que la
commande n’a pas pu s’exécuter. Le rapport contient uniquement une empreinte de la
clé de chiffrement et des indicateurs de présence des secrets ; il n’affiche jamais
une clé ni la valeur d’un autre secret. Conservez l’inventaire avec la sauvegarde
correspondante afin que les opérateurs puissent comparer la version de
l’application, l’empreinte du schéma, les ressources Work attendues et les
exclusions avant une restauration.
Créez des clés dédiées de chiffrement et de signature des sauvegardes au moyen de l’image exactement déployée. Conservez ce répertoire hors du volume de l’application et copiez la clé de chiffrement et la clé privée de signature vers un emplacement de reprise protégé distinct :
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 génération de clés refuse les fichiers de sortie existants. Ne générez jamais de nouvelles clés sur un jeu de sauvegardes existant : la perte de la clé de chiffrement des archives ou de l’identité de signature rend la preuve de reprise correspondante inutilisable.
Installez les scripts de sauvegarde et de restauration ainsi que les unités systemd fournis, puis activez le minuteur :
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
L’unité lit facultativement les remplacements réservés à la maintenance depuis
/etc/libre-webui/backup.env ; elle ne charge pas le fichier .env de
l’application. Ne créez le fichier en tant que root que si un remplacement est
nécessaire :
install -d -m 0750 /etc/libre-webui
install -m 0600 /dev/null /etc/libre-webui/backup.env
LIBRE_WEBUI_STACK_DIR, LIBRE_WEBUI_BACKUP_RETENTION_DAYS,
LIBRE_WEBUI_CONTAINER_NAME et LIBRE_WEBUI_BACKUP_KEY_DIR peuvent y être
définis directement. Conservez le fichier appartenant à root et avec le mode
0600. Un répertoire de clés personnalisé doit rester lisible par root dans le bac
à sable systemd.
Modifier LIBRE_WEBUI_BACKUP_DIR modifie aussi la frontière d’écriture de systemd.
Le répertoire doit exister avant le démarrage du service et l’unité a besoin d’un
fragment correspondant. Par exemple, après avoir défini
LIBRE_WEBUI_BACKUP_DIR=/srv/backups/libre-webui dans backup.env :
install -d -m 0700 /srv/backups/libre-webui
systemctl edit libre-webui-backup.service
Ajoutez ce chemin exact dans l’éditeur, puis rechargez l’unité :
[Service]
ReadWritePaths=/srv/backups/libre-webui
systemctl daemon-reload
systemctl start libre-webui-backup.service
Sans l’entrée ReadWritePaths= correspondante, ProtectSystem=strict empêche à
juste titre le minuteur d’écrire dans un emplacement personnalisé.
Le service de sauvegarde dispose d’un délai maximal de six heures pour les grandes
archives. L’outil d’assistance acquiert un verrou sur l’hôte, n’arrête l’application
que si elle était déjà en cours d’exécution et crée l’archive à partir du volume au
repos avec l’image exactement déployée. L’archive possède un manifeste signé et une
charge utile chiffrée par l’opérateur ; elle comprend le répertoire de données ainsi
que la configuration d’exécution et les secrets nécessaires à l’ouverture de cet
état. L’outil vérifie ensuite indépendamment l’intégralité de l’archive avant de
publier atomiquement son rapport de métadonnées. Ses conteneurs de maintenance en
lecture seule disposent d’un tmpfs /tmp privé et inscriptible pour l’inspection de
SQLite et la vérification authentifiée de l’archive ; aucun texte en clair
temporaire ne persiste dans la couche du conteneur. Copiez hors de l’hôte les deux
fichiers et les clés de reprise protégées séparément.
Lorsque Work utilise docker-compose.work-proxy.yml, la reprise doit aussi prouver
que chaque volume Work référencé par la base de données existe encore. L’outil lit
le DOCKER_HOST de l’application déployée, localise le service de proxy de socket
dans le même projet Compose actif et découvre leur unique réseau interne partagé à
partir des connexions réseau réelles de Docker. Compose préfixe ce réseau avec le
nom du projet : ne configurez ni n’inscrivez donc en dur un nom de réseau supposé.
Seul le conteneur de création de l’archive rejoint ce réseau interne et peut
atteindre le proxy filtré ; il ne reçoit aucun socket brut. La vérification
indépendante de l’archive continue avec --network none. Un proxy absent, un point
de terminaison inattendu, un réseau partagé externe ou ambigu, ou un montage de
socket brut provoque un échec avant l’arrêt de l’application et avant la publication
d’une archive.
Testez la reprise vers un nouveau volume sans remplacer le volume actif :
LIBRE_WEBUI_RESTORE_IMAGE="$image_ref" \
libre-webui-restore \
/var/backups/libre-webui/libre-webui-integrated-YYYYMMDDTHHMMSSZ.lwb \
libre-webui-restore-drill
L’outil de restauration refuse un volume ou une cible de configuration existants,
vérifie l’archive et son inventaire de reprise interne dans un stockage jetable,
puis copie les données vers le nouveau volume et écrit les fichiers récupérés
runtime.json et secrets.json avec des autorisations privées. Il ne recâble ni ne
démarre jamais la pile active. Examinez la configuration récupérée, mettez à jour
délibérément les valeurs propres au déploiement et testez le volume restauré avec
une pile isolée.
Les modèles Ollama peuvent être récupérés de nouveau. Les volumes Work Docker, les PVC Work Kubernetes et les dossiers Work liés à l’hôte ne font pas partie du répertoire de données de l’application et exigent leurs propres instantanés coordonnés et leur propre politique de conservation.
Mises à jour
Libre WebUI possède un état, même lorsque l’étiquette de son image est modifiable. Le fichier Compose de base marque en permanence l’application comme exclue de Watchtower. Ne la mettez à niveau que dans le cadre d’une action coordonnée de l’opérateur :
- Notez l’identifiant de l’image en cours d’exécution et résolvez le remplacement vérifié vers un digest immuable.
- Exécutez
libre-webui recovery-check, démarrez le service de sauvegarde et exigez la création d’une archive et d’un rapport de vérification avant de poursuivre. - Définissez
LIBRE_WEBUI_IMAGEsur le digest vérifié, récupérez-le et recréez uniquementlibre-webuiavec Docker Compose. Ne supprimez ni ne recréez son volume de données. - Exigez la réussite de
/health/ready, de la connexion, de la session et de l’historique, de la récupération des documents et des tests rapides Work. Sinon, revenez au digest de l’image enregistré ; conservez l’état en échec et la sauvegarde vérifiée pour le diagnostic.
La séquence côté hôte est volontairement manuelle. Ne remplacez le digest qu’après
l’avoir examiné et inspectez la paire .lwb et .json la plus récente avant la
récupération :
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}}'
Le remplacement Watchtower facultatif portant le socket reste disponible uniquement pour les sidecars explicitement étiquetés dans le fichier de base :
docker compose \
-f docker-compose.yml \
-f docker-compose.watchtower.yml \
up -d
Watchtower vérifie Ollama et SearXNG toutes les 30 minutes. Les données des modèles
Ollama restent dans leur volume nommé et la configuration SearXNG reste dans son
montage lié. Il ne met pas à jour Libre WebUI, cloudflared, le proxy du socket Work
ni les bacs à sable Work. Un déploiement client suit main ; une instance
expérimentale peut sélectionner :dev, mais l’application exige toujours la même
mise à niveau manuelle conditionnée par une sauvegarde. Ne rattachez jamais cette
pile privée individuelle à des services de persistance d’équipe ; déployez plutôt
la topologie d’équipe complète.