./run wiki-portabile-bundle-openwebui-docker
Ho congelato una wiki intera in un bundle Docker — e un URL dentro SQLite ha quasi rovinato tutto
Come ho reso portabile un'intera knowledge base OpenWebUI: bundle self-contained, vettori pre-calcolati, tool server OpenAPI e il port-pinning che tiene in piedi tutto.
- status
- draft
- project
- wiki-openwebui
- updated
- 2026-07-07
- tags
Volevo una cosa apparentemente banale: prendere la mia knowledge base interna — chat OpenWebUI, tool, wiki e vettori — e farla viaggiare tra host Docker senza ricostruire nulla. Copio una cartella su un’altra macchina, lancio uno script, e ho lo stesso identico assistente che risponde solo sul mio corpus. Niente reindicizzazione, niente riconfigurazione a mano dei tool, niente “ah ma qui manca la knowledge”.
Il piano ingenuo era: “faccio un tar del volume dati di OpenWebUI e via”. Ha funzionato per il 90%. Il restante 10% mi ha fatto perdere un pomeriggio, e riguarda una cosa che non ti aspetti: un URL assoluto sepolto dentro lo SQLite di OpenWebUI.
L’idea: un bundle, tre pezzi, zero segreti
Il corpus in sé è un progetto riservato — qui non c’entra e non lo tocco. Parlo solo della tecnica di impacchettamento. Il bundle è una cartella con tre ingredienti:
- Il wikipack — l’albero markdown della wiki + l’indice vettoriale LanceDB già calcolato + un
manifest.jsoncon la config (non-segreta) dell’embedding. Vettori congelati: all’import non si ri-embedda niente. - Il tool server
wiki-tools— un piccolo servizio FastAPI che espone la wiki come tool server OpenAPI (search / read / links / graph), così la chat naviga la wiki invece di fare RAG cieco. - Lo stato di OpenWebUI — uno snapshot del volume dati, cioè
webui.db(SQLite) con dentro tutto: utenti, chat, modelli registrati, knowledge e — soprattutto — la registrazione del tool server.
Lo snapshot è tre righe di export-bundle.sh, e la parte importante è che gira dentro un container alpine usando il volume, senza toccare i file sotto:
VOLUME="wiki-openwebui_openwebui-data"
docker run --rm -v "$VOLUME":/data -v "$OUT":/backup alpine \
tar czf /backup/openwebui-data.tgz -C /data .
# .env template col segreto STRAPPATO via sed
sed 's/^GATEWAY_API_KEY=.*/GATEWAY_API_KEY=/' "$HERE/.env" > "$OUT/.env.template"
Regola d’oro: la chiave del gateway non entra mai nel bundle. Viene rimossa dal template con sed, e l’import-bundle.sh la chiede in modo interattivo alla prima esecuzione. Stessa disciplina nel wikipack: il make_wikipack.py legge endpoint e modello dell’embedding dallo stato dell’app ma non scrive mai una API key nel manifest.
Il manifest: il contratto dei vettori
Ecco il cuore del “vettori portabili”. Il manifest.json (sanitizzato) è minuscolo:
{
"wikipackVersion": 1,
"title": "<wiki-interna>",
"createdAt": null,
"embedding": {
"endpoint": "https://<GATEWAY>/v1/embeddings",
"model": "Qwen3-Embedding-8B",
"outputDimensionality": 4096
},
"chunkCount": 2468,
"pageCount": 244,
"markdownFiles": 249
}
Perché è il contratto? Perché l’indice viaggia già vettorizzato a 4096 dimensioni con un modello preciso. Al momento della query, il tool server deve embeddare la domanda con lo stesso identico modello e la stessa dimensione, altrimenti il confronto tra vettori è pura spazzatura. Ho reso questa invariante esplicita nel codice — se non torna, esplode subito con un errore comprensibile invece di dare risposte silenziosamente sbagliate:
if self.expected_dim is not None and len(vec) != self.expected_dim:
raise EmbeddingError(
f"embedding dim mismatch: got {len(vec)}, index expects {self.expected_dim}. "
f"Query embedder must use the SAME model+dim as the shipped vectors."
)
Nota sul createdAt: null: è voluto. Non lo timbro a build-time per non introdurre nondeterminismo — due export dello stesso stato producono lo stesso file. Lo può timbrare la CI se serve.
Prima di spedire, il make_wikipack chiama table.optimize() di LanceDB sulla copia (mai sull’originale): compatta i frammenti e pota le versioni vecchie, così il bundle è snello. Dettaglio da nerd che ho scoperto guardando i file: il nome del manifest di versione è 18446744073709551428.manifest — è 2^64 − 188, LanceDB numera le versioni a ritroso da UINT64_MAX. Innocuo, ma la prima volta ti fa dubitare di aver corrotto qualcosa.
La spina: l’URL che non voleva viaggiare
Ecco dove il piano “basta il tar” si è rotto. OpenWebUI, quando registri un tool server, salva l’URL così com’è dentro webui.db. Nel mio caso http://localhost:8100. Quando importi il bundle su un altro host e OpenWebUI riparte, riprova a chiamare esattamente quell’URL. Se sulla macchina nuova il tool server è finito su un’altra porta, la registrazione è morta: la chat non ha più tool, e lo capisci solo perché le risposte peggiorano.
La cura non è elegante ma è solida: inchiodare le porte. Il compose generato nel bundle non lascia scegliere a caso — pinna HOST_PORT e TOOLS_PORT, e sono gli stessi valori con cui l’URL è stato salvato nello SQLite:
wiki-tools:
ports:
- "${TOOLS_PORT:-8100}:8000" # deve combaciare con l'URL salvato in webui.db
open-webui:
ports:
- "${HOST_PORT:-8300}:8080"
È questo il prezzo della portabilità in questo design: la registrazione del tool è un URL assoluto congelato nel DB, quindi le porte diventano parte del contratto. Tienile uguali e il bundle “si ricollega da solo”; cambiale e ti tocca ri-registrare il tool a mano. Sul mio cluster k8s ho scelto la strada opposta — lì inietto la stessa registrazione via variabile d’ambiente (TOOL_SERVER_CONNECTIONS) invece che via snapshot SQLite, così l’URL è configurabile. Due mondi, due strategie, stessa lezione: lo stato che rende comodo l’import è anche quello che ti incastra.
Gotcha onesti
- Lo snapshot “impostazioni” pesa 272 MB, il wikipack 13. Sorpresa contro-intuitiva: la parte pesante non è il corpus con i suoi vettori (13 MB), ma lo stato di OpenWebUI (
openwebui-data.tgz, 272 MB). Le “impostazioni” sono più grandi della conoscenza. Bundle totale ~286 MB in versione “light” (ricostruisce l’immaginewiki-toolse pulla OpenWebUI all’import); con--with-imagesdiventa molto più grosso ma gira offline. - Il compose del bundle perde l’auth del tool server. Nel compose di sviluppo passo
TOOLS_API_KEY; l’heredoc che genera il compose del bundle non lo include. Risultato: nel bundle il tool server parte con auth disabilitata (_load_keys()vuoto → nessun controllo). Comodo per una demo interna, ma è drift reale tra i due file, e su una rete non fidata è un buco. Da sistemare. - Il nome del volume è hardcoded. Sia export che import assumono
wiki-openwebui_openwebui-datae--project-name wiki-openwebui. Rinomina il progetto e il restore punta al volume sbagliato in silenzio. - Il path sorgente dell’export è assoluto. Lo script di export ha il percorso della macchina di build inchiodato dentro: si esporta solo da lì. Il bundle è portabile; lo script che lo crea no.
- README/realtà divergono sulle porte di default. Il compose “dev” del repo usa 3000/8000, quello generato nel bundle 8300/8100. Se leggi il README sbagliato colleghi la porta sbagliata.
Come va
È shipped e usato come knowledge base interna. Il ciclo export → copia → import-bundle.sh → chiave → su gira, e la chat risponde sul corpus con i tool pre-registrati. C’è pure un endpoint /reload key-protected per ricaricare il wikipack aggiornato senza riavviare il container. I punti ruvidi (auth persa nel bundle, path hardcoded, nome volume fisso) sono note nel taccuino, non bloccanti per l’uso interno.
Cosa ho imparato
Che la portabilità non è “copiare i file”, è capire dove lo stato tiene i riferimenti assoluti. Un vettore a 4096 dimensioni viaggia benissimo — è un array di float, non ha opinioni. È l’http://localhost:8100 sepolto in uno SQLite che ti tradisce, perché presume un mondo che sull’host di destinazione non esiste più. La soluzione vera non è stata scrivere più codice: è stata rendere le porte parte del contratto e scriverlo nero su bianco nel README. A volte “portabile” significa semplicemente essere onesti su cosa deve restare uguale.