Sari la conținutul principal

Deployment privat la distanță

Modelul rulează Libre WebUI, Ollama și Cloudflare Tunnel pe un host Docker fără porturi publice. Cloudflare Access este limita externă de identitate, autentificarea Libre cea internă. Work și Watchtower sunt opt-in separate, echivalente root.

Este topologia solo cu o replică: SQLite, blobs criptate local, vectori embedded, coordonare locală și worker embedded împart volumul. Nu o transformați în team prin selectori .env. Team cere docker-compose.team.yml și, cu Work, docker-compose.team.work.yml, care furnizează PostgreSQL/PGVector, S3 versionat, Redis, worker extern și gateway coordonat.

Porniți de la deploy/private/docker-compose.yml, implicit main:

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

dev este doar pentru instanță de dezvoltare opt-in, nu default client.

Model de securitate

  • Cloudflare Access protejează întreg hostname-ul, /api/* și WebSocket upgrades. Fără bypass public.
  • API cere cont curent; lifecycle modele și Work cer rol admin în bază.
  • App, Ollama, SearXNG și cloudflared folosesc numai rețea Compose privată, fără porturi host.
  • SearXNG inclus oferă căutare web, internă și inertă până la activare admin. Setați SEARXNG_SECRET.
  • App rulează non-root, rootfs read-only, fără capabilities, no-new-privileges și limits.
  • Work este off fără override; containerele au read-only root, dropped capabilities, limits, volume și default-deny network.

Stackul de bază nu montează socket. Cu docker-compose.work-proxy.yml, o proxy internă ține socketul și permite numai containers, images, volumes, networks, exec, info; refuză swarm, secrets, build, system. App nu are socket/grup. Proxy reduce suprafața, nu blast radius: cine creează container poate bind-mount host paths. Este hardening, nu izolare multi-tenant.

docker-compose.work.yml raw și Watchtower permit API arbitrar și control host. Read-only mount nu face API read-only. Backup helper refuză raw socket; migrați Work la proxy.

Bootstrap

  1. Creați operator sudo non-root și verificați SSH key înainte de dezactivare root.
  2. Copiați deploy/private/.env.example în /opt/libre-webui/.env, mode 0600, generați secrets și dimensionați BLOB_QUOTA_BYTES_PER_USER. BLOB_QUOTA_RESERVATION_TTL_MS expiră rezervări abandonate, implicit o oră.
  3. Pentru Work setați DOCKER_GID la grupul numeric al /var/run/docker.sock.
  4. Token tunnel în /opt/libre-webui/secrets/tunnel-token, mode 0640 sau mai strict.
  5. Creați aplicație Cloudflare Access pentru hostname complet, sesiune 24h și identități dorite. Activați Protect with Access. Pentru health public, politică separată doar /health/live. Niciodată Bypass global pe aplicația principală.
  6. Păstrați ENABLE_SIGNUP=false. După protecție creați primul admin local; baza goală permite acel bootstrap. Registration numai într-o fereastră deliberată.
  7. Configurați Turnstile și TURNSTILE_EXPECTED_HOSTNAME exact.
cd /opt/libre-webui
docker compose config --quiet
docker compose up -d
docker compose ps

Work cu proxy:

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

Varianta raw rămâne cu consecințele descrise.

Cu Access, testele CLI cer service token în afara istoricului și două headere:

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

Cererea neautentificată trebuie 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

Hardening host

Directorul include sshd drop-in și fail2ban. Verificați altă sesiune sudo și sshd -t. Folosiți UFW default-deny inbound și SSH limitat:

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

Păstrați update-uri de securitate și dezactivați X11, agent, TCP forwarding fără nevoie documentată.

Backup și restaurare

Înainte de backup, rulați inventory read-only în containerul live pentru versiunea, env și volumul exacte. Un checkout host poate inspecta baza sau sursa greșită.

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

Exit 0 fără blocaje, 1 blocaje JSON, 2 nu rulează. Raportul are doar fingerprint și flags de prezență, niciodată secrete. Păstrați cu backup pentru versiune, schema, Work și excluderi.

Generați chei dedicate cu image exact, în afara volumului, și copiați-le protejat:

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

Generarea refuză outputs existente. Nu suprascrieți o serie; pierderea unei chei face dovada inutilă.

Instalați scripts și timer:

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

Unitatea poate citi /etc/libre-webui/backup.env, nu .env aplicației:

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

Acolo setați LIBRE_WEBUI_STACK_DIR, LIBRE_WEBUI_BACKUP_RETENTION_DAYS, LIBRE_WEBUI_CONTAINER_NAME, LIBRE_WEBUI_BACKUP_KEY_DIR, cu owner root/mode 0600.

Schimbarea LIBRE_WEBUI_BACKUP_DIR schimbă limita de scriere. După LIBRE_WEBUI_BACKUP_DIR=/srv/backups/libre-webui în backup.env:

install -d -m 0700 /srv/backups/libre-webui
systemctl edit libre-webui-backup.service
[Service]
ReadWritePaths=/srv/backups/libre-webui
systemctl daemon-reload
systemctl start libre-webui-backup.service

Fără ReadWritePaths=, ProtectSystem=strict blochează corect.

Serviciul permite șase ore. Helperul obține lock host, oprește app numai dacă rula și creează arhiva cu image exact din volum quiesced. Are manifest semnat și payload criptat cu data/runtime/secrets. Verifică independent înainte de publicare atomică. Containerele read-only primesc /tmp tmpfs privat fără plaintext persistent. Copiați off-host.

Cu docker-compose.work-proxy.yml, recovery dovedește toate volumele Work. Citește DOCKER_HOST, găsește proxy în același proiect și unica rețea internă din attachments. Compose prefixează numele, deci nu ghiciți. Numai containerul de arhivă intră în rețea fără raw socket; verificarea are --network none. Proxy lipsă, endpoint neașteptat, rețea externă/ambiguă sau raw socket eșuează înainte de stop/publicare.

Test pe volum nou:

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

Restore refuză țintă existentă, verifică în stocare temporară, copiază date și scrie runtime.json/secrets.json private. Nu reconectează/pornește stack live. Inspectați și testați izolat.

Modelele Ollama se descarcă din nou. Volumele Docker Work, PVC Kubernetes și folderele host sunt în afara data și cer snapshots/retention proprii.

Actualizări

Libre este stateful chiar cu tag mutabil. Compose îl exclude permanent din Watchtower. Upgrade numai coordonat:

  1. Înregistrați image ID și rezolvați în digest imuabil.
  2. Rulați libre-webui recovery-check, backup și cereți arhivă verificată nouă.
  3. Setați LIBRE_WEBUI_IMAGE, pull și recreați numai libre-webui, niciodată data volume.
  4. Cereți /health/ready, login, history, documente și Work tests. Altfel rollback și păstrați starea/backupul.

Secvența host este manuală. Verificați ultimul .lwb/.json:

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}}'

Watchtower override numai pentru sidecars etichetate:

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

Verifică Ollama/SearXNG la 30 minute și păstrează data/config. Nu actualizează Libre, cloudflared, proxy sau sandboxes. Clientul urmează main, experimental poate :dev, dar același upgrade manual condiționat de backup. Nu conectați solo la servicii team; folosiți topologia completă.