🇬🇧 Questo articolo è disponibile anche in inglese.
Un anno fa avevamo pubblicato su MojaLab una guida per costruire uno stack LLM self-hosted con Ollama e LiteLLM.
Un anno nel mondo dell'AI, però, sembra un'era geologica.
I modelli sono cambiati, gli engine sono cambiati, Docker è cambiato e soprattutto è cambiato il modo in cui utilizziamo questi sistemi.
Questa volta siamo partiti da una domanda diversa:
Posso prendere una GPU VPS a noleggio, trasformarla rapidamente in una mia API LLM compatibile OpenAI e, quando ho finito, distruggere la VM senza perdere modelli, configurazioni e dati?
La risposta è sì.
Ed è esattamente quello che fa questo progetto:
https://github.com/doradame/GPU_VPS_LiteLLM
L'idea fondamentale è molto semplice:
la GPU VPS è temporanea. Il disco no.
La macchina può nascere oggi, morire stasera ed essere sostituita domani da un'altra.
Tutto ciò che conta — modelli, database, configurazioni, certificati e runtime Docker — vive invece su un volume separato, persistente e cifrato con LUKS.
In pratica possiamo noleggiare la GPU solo quando ci serve.
1. Perché costruire questo stack
Prima ancora di aprire il terminale vale la pena fare una domanda molto meno tecnica:
ha davvero senso self-hostare un LLM?
Le API commerciali sono eccellenti. Non dobbiamo installare driver NVIDIA, non dobbiamo preoccuparci della VRAM, non dobbiamo aggiornare container e soprattutto paghiamo soltanto quello che consumiamo.
Se ci serve saltuariamente il miglior modello disponibile sul mercato, probabilmente la scelta più razionale è utilizzare un'API commerciale.
Nel 2026 c'è inoltre un altro punto abbastanza evidente: i veri modelli frontier o non sono disponibili per il self-hosting oppure richiedono hardware molto più importante della classica GPU che possiamo affittare per qualche euro l'ora.
Ci sono però almeno tre casi in cui il discorso cambia.
I dati
Ci sono situazioni in cui semplicemente non vogliamo che prompt e documenti escano dal nostro perimetro.
Può essere una necessità aziendale, normativa o semplicemente una scelta progettuale.
Con questo stack la richiesta arriva direttamente a una macchina che controlliamo noi:
flowchart TD
C([client]) -->|HTTPS| CA[Caddy]
CA --> L[LiteLLM]
L --> M[Ollama / vLLM]
Il modello gira sulla GPU che abbiamo affittato e lo stato del sistema vive su un disco cifrato di cui possediamo la chiave.
Non è un sistema air-gapped e non pretende di esserlo, ma sappiamo esattamente dove passano i dati.
Il volume di elaborazione
Le API commerciali si pagano a token.
Se dobbiamo fare qualche centinaio di richieste al giorno va benissimo. Se invece stiamo producendo dataset sintetici, eseguendo migliaia di valutazioni o facendo girare pipeline che macinano milioni di token, la matematica può cambiare parecchio.
Una GPU affittata per alcune ore con un modello open-weight quantizzato può diventare economicamente molto interessante.
Non esiste una regola universale: bisogna fare i conti caso per caso.
Il controllo
Self-hosting significa anche scegliere esattamente quale modello utilizzare, quale quantizzazione, quale versione dell'engine, quanto contesto allocare, quante richieste concorrenti permettere e quando aggiornare.
Nessun rate limit cambiato improvvisamente e nessuna versione del modello sostituita sotto i nostri piedi.
Possiamo anche servire un nostro fine-tuning o un modello che nessun provider commerciale espone.
Naturalmente c'è il rovescio della medaglia: la GPU costa anche quando non lavora e aggiornamenti, sicurezza e backup diventano un problema nostro.
Lo stack che vedremo cerca di ridurre il più possibile questa parte operativa.
Ed è proprio per questo che la macchina è stata progettata per essere sacrificabile.
2. L'idea di fondo: la VM è temporanea, i dati no
Chi lavora con l'infrastruttura conosce bene il principio:
cattle, not pets.
Le macchine dovrebbero essere bestiame, non animali domestici.
Con le GPU VPS questo principio diventa ancora più importante, perché una GPU potente accesa senza fare nulla continua comunque a generare una fattura.
Lo stack parte quindi da una decisione abbastanza netta.
Sul disco di sistema della VM vivono soltanto:
- sistema operativo;
- driver NVIDIA;
- Docker.
Tutto il resto vive su un secondo disco.
flowchart TD
NET([Internet]) -->|HTTPS| CA[Caddy]
CA --> L[LiteLLM]
L --> O[Ollama]
L --> V[vLLM]
L --> PG[(PostgreSQL)]
subgraph VOL["🔒 volume LUKS persistente"]
direction TB
ST["stack/ — compose · config · certificati · PostgreSQL"]
DK["docker/ · containerd/"]
OL["ollama/ — modelli"]
VL["vllm/ — cache HuggingFace"]
end
O -.-> OL
V -.-> VL
PG -.-> ST
Il volume è cifrato con LUKS2.
Se distruggiamo la VM, quel disco rimane.
Quando ci serve nuovamente capacità GPU creiamo una nuova VPS, ricolleghiamo il volume e ricostruiamo soltanto la parte sacrificabile.
Questo è il vero cuore del progetto.
LiteLLM, Ollama e vLLM sono quasi una conseguenza.
3. Scegliere l'infrastruttura
Prima di pensare ai modelli c'è da scegliere la macchina, e qui vale la pena chiarire subito una distinzione che nel mercato GPU Cloud non è sempre evidente, perché con nomi molto simili vengono venduti servizi profondamente diversi.
GPU pod o GPU VM?
Da una parte ci sono i GPU pod o GPU container, ambienti containerizzati nei quali il provider ci mette a disposizione una GPU e uno spazio di lavoro già pronto.
Sono economici, velocissimi da accendere e perfetti per un notebook, un training o un workload temporaneo, ma non sono necessariamente vere macchine virtuali: spesso non abbiamo systemd, non controlliamo realmente il sistema operativo e soprattutto non abbiamo a disposizione un vero disco a blocchi sul quale costruire il nostro volume cifrato.
Per questo progetto ci serve invece una GPU VM vera, cioè una macchina sulla quale abbiamo accesso root, un sistema operativo completo e la possibilità di collegare almeno un secondo volume persistente.
La checklist minima diventa quindi:
- una VM vera, sulla quale abbiamo accesso root;
- una GPU NVIDIA con abbastanza VRAM per i modelli che vogliamo utilizzare;
- un secondo volume a blocchi persistente, oppure comunque uno storage che il provider ci permetta di scollegare dalla VM e successivamente collegare a un'altra istanza;
- un IP pubblico raggiungibile almeno sulle porte 80 e 443;
- la possibilità di creare, presso un qualsiasi provider DNS sotto il nostro controllo, un record
Ache faccia puntare un nostro dominio o sottodominio all'indirizzo IP pubblico della VPS.
Quest'ultimo punto è importante: il dominio e il DNS non devono necessariamente essere forniti dalla stessa società che ci affitta la GPU VPS.
Possono tranquillamente stare altrove.
Quello che serve allo stack è semplicemente un nome DNS del tipo:
llm.example.com
che possiamo far puntare all'IP della macchina. Caddy utilizzerà quel nome per ottenere e rinnovare automaticamente il certificato TLS.
Lo storage persistente
Il secondo disco merita qualche considerazione in più, perché è ciò che rende possibile l'intero modello operativo del progetto.
Lo storage aggiuntivo dovrebbe poter sopravvivere alla cancellazione della VM ed essere ricollegato successivamente a un'altra macchina, almeno all'interno della stessa region del provider.
È lì che conserveremo modelli, database, configurazioni, certificati, immagini Docker e tutto ciò che costa tempo ricostruire.
La VM, al contrario, deve poter sparire senza rimorsi.
Quale GPU scegliere?
Se dobbiamo scegliere una GPU per servire un LLM, la prima cifra da guardare rimane la VRAM, perché stabilisce banalmente se il modello può entrare oppure no.
Ma non è l'unica.
Una volta stabilito che il modello entra in memoria, iniziano a pesare anche la banda della memoria, la generazione della GPU, i Tensor Core disponibili e il supporto hardware alle precisioni utilizzate dall'engine, per esempio FP8 o FP4.
Due GPU con la stessa quantità di VRAM possono quindi ospitare grossomodo la stessa classe di modelli ma avere prestazioni di inferenza sensibilmente diverse.
Una mappa molto approssimativa, valida ad agosto 2026, può essere questa:
| GPU | VRAM | Come la vedrei in questo stack |
|---|---|---|
| NVIDIA L4 | 24 GB | Entry level interessante per 7B–14B quantizzati e workload non troppo concorrenti |
| GeForce RTX 5090 | 32 GB | Molta potenza per euro quando disponibile sui provider, con più margine rispetto alle classiche 24 GB |
| NVIDIA L40S | 48 GB | Uno dei tagli più comodi per questo progetto: 32B quantizzati, contesti più ampi o più workload |
| NVIDIA H100 | 80/94 GB | Fascia decisamente superiore, utile quando throughput e concorrenza iniziano a diventare importanti |
| RTX PRO 6000 Blackwell | 96 GB | Molta VRAM e architettura Blackwell, interessante per modelli grandi e inference moderna |
| NVIDIA H200 | 141 GB | Permette di spostarsi verso modelli molto più grandi senza ricorrere immediatamente al multi-GPU |
| NVIDIA B200 | 180 GB | Classe datacenter Blackwell, decisamente oltre le necessità del piccolo laboratorio ma parte del panorama cloud |
Questa non vuole essere una classifica e soprattutto non significa che una H100 sia sempre una scelta più intelligente di una L40S.
Il prezzo orario conta almeno quanto le prestazioni.
Per il genere di utilizzo che stiamo descrivendo, spesso la domanda migliore non è:
Qual è la GPU più veloce?
ma:
Qual è la GPU meno costosa che contiene comodamente il mio modello e mi dà il throughput che mi serve?
Come regola pratica molto spannometrica, con quantizzazione a 4 bit i soli pesi richiedono qualcosa più della metà della dimensione nominale del modello espressa in miliardi di parametri.
Un 32B finisce quindi facilmente nell'ordine dei 18–20 GB.
Ma non possiamo riempire la scheda con i soli pesi, perché dobbiamo lasciare spazio alla KV cache, agli overhead dell'engine e alle richieste concorrenti.
Per questo, invece di ragionare sul limite teorico, ragionerei per fasce:
24 GB — 7B e 14B sono il territorio naturale; qualcosa di più grande può entrare con quantizzazioni aggressive, ma il margine diventa piccolo.
32 GB — iniziamo ad avere più libertà con modelli nell'area 20B–30B quantizzati e con il contesto.
48 GB — è probabilmente il punto più flessibile per un laboratorio serio: un 32B diventa molto più comodo e possiamo iniziare a pensare a due modelli più piccoli sulla stessa GPU.
80–96 GB — si apre la possibilità di modelli sensibilmente più grandi, oppure di molta più concorrenza e KV cache.
140 GB e oltre — entriamo in una classe in cui anche modelli molto grandi diventano gestibili su una singola GPU, naturalmente con costi orari di tutt'altro livello.
Sono indicazioni, non una tabella di compatibilità: architettura del modello, quantizzazione, context window ed engine possono cambiare parecchio i numeri.
Ed è proprio qui che torna utile l'architettura che abbiamo scelto: la GPU non fa parte dello stato persistente.
Possiamo iniziare oggi con una L40S, spegnere tutto, e domani ricostruire la macchina usando una RTX PRO 6000, una H100 o qualsiasi altra GPU che il provider renda conveniente, purché abbia abbastanza VRAM e sia supportata dal nostro stack software.
Il volume cifrato non deve sapere quale scheda c'è dall'altra parte.
4. Costruire lo stack: da zero a /v1/chat/completions
Una volta ottenuta la macchina, il deployment è volutamente molto poco magico.
Niente Kubernetes.
Niente Terraform.
Niente Ansible.
Una serie di script Bash numerati.
./scripts/00-preflight.sh
sudo ./scripts/01-nvidia-driver.sh
sudo reboot
sudo ./scripts/02-luks-volume.sh
sudo ./scripts/03-docker.sh
sudo ./scripts/04-nvidia-toolkit.sh
sudo ./scripts/05-stack-config.sh
sudo ./scripts/06-pull-models.sh
sudo ./scripts/07-stack-up.sh
sudo ./scripts/08-test.sh
Ogni script fa una cosa sola.
Ogni passaggio lascia un marker e lo script successivo si rifiuta di partire se quello precedente non è stato completato.
Gli step distruttivi richiedono inoltre di digitare esplicitamente:
YES
prima di procedere.
Per una singola macchina abbiamo preferito Bash leggibile a livelli ulteriori di astrazione.
Quando qualcosa si rompe possiamo aprire lo script e vedere esattamente cosa stava cercando di fare.
00 — Preflight
Il primo script è un'intervista.
Chiede quale disco utilizzare, dove montarlo, quale dominio configurare, quali IP autorizzare, quali engine utilizzare e quali modelli caricare.
Il risultato finisce in:
config.env
È il centro di configurazione dell'intero stack.
Ha permessi 600 e naturalmente non finisce nel repository Git.
01 — NVIDIA
Installa il driver NVIDIA sull'host e prepara la macchina per utilizzare la GPU.
È l'unico passaggio che richiede un reboot.
Molti provider consegnano già immagini con i driver installati, ma abbiamo preferito mantenere lo step esplicito e riproducibile.
02 — LUKS
Il disco secondario viene partizionato, cifrato con LUKS2, formattato, montato e registrato in crypttab e fstab.
Lo stack utilizza una passphrase più un keyfile.
Questo permette anche lo sblocco automatico della macchina al boot.
Tra poco torneremo sul relativo threat model.
03 — Docker
Docker viene installato e il suo storage viene spostato sul disco cifrato.
Non soltanto container e volumi.
Anche le immagini.
Quest'ultimo dettaglio ci ha fatto perdere qualche ora durante i test, ma ci torniamo dopo.
Il risultato che vogliamo è semplice:
il disco di sistema deve rimanere sacrificabile.
04 — NVIDIA Container Toolkit
È il ponte tra Docker e la GPU.
Lo script termina facendo realmente partire un container ed eseguendo:
nvidia-smi
Se il container non vede la GPU, la procedura si ferma lì.
Meglio scoprirlo adesso che cinque script dopo.
05 — Generazione dello stack
A questo punto config.env viene trasformato nel progetto Docker Compose vero e proprio.
Vengono generati:
docker-compose.yml;- configurazione LiteLLM;
- Caddyfile;
- segreti;
- configurazione PostgreSQL;
- backup del database.
Se una master key LiteLLM o la password PostgreSQL non esistono, vengono generate automaticamente.
06 — Download dei modelli
I modelli vengono scaricati prima ancora di accendere definitivamente lo stack.
La cache vive sul volume persistente.
Questo passaggio può durare parecchio perché stiamo parlando facilmente di decine di GB.
È anche interrompibile e riprendibile.
07 — Accensione
Finalmente:
docker compose up -d
Partono Caddy, LiteLLM, PostgreSQL, Ollama e uno o due vLLM, a seconda della configurazione.
08 — Smoke test
Non ci limitiamo a controllare che i container risultino "running".
Lo script verifica liveness, lista dei modelli, una vera chat completion, ogni modello configurato e il comportamento della whitelist di Caddy.
Alla fine abbiamo qualcosa del genere:
curl https://llm.example.com/v1/chat/completions \
-H "Authorization: Bearer sk-..." \
-H "Content-Type: application/json" \
-d '{
"model": "qwen",
"messages": [
{"role": "user", "content": "Ciao!"}
]
}'
Ed è qui che arriva uno dei vantaggi principali del progetto.
Questa è un'API compatibile OpenAI.
Per moltissimi client significa cambiare soltanto:
base_url
api_key
e continuare a usare SDK e librerie già esistenti.
5. Dentro lo stack: componenti e responsabilità
A questo punto gli script hanno finito il loro lavoro e sulla VPS abbiamo finalmente uno stack funzionante.
Vale quindi la pena smettere per un momento di guardare come lo abbiamo installato e guardare invece che cosa abbiamo costruito.
I componenti sono pochi e hanno responsabilità abbastanza nette:
flowchart TD
NET([Internet]) -->|HTTPS| CA["Caddy<br/>TLS + allowlist IP"]
CA --> L["LiteLLM<br/>una sola API OpenAI · chiavi · budget"]
L --> O["Ollama<br/>flessibilità · GGUF · cambio modelli"]
L --> V["vLLM<br/>throughput · modello residente"]
L --> PG[("PostgreSQL<br/>chiavi · config · utilizzo")]
O -.-> VOL[["🔒 volume LUKS — sopravvive alla VM"]]
V -.-> VOL
PG -.-> VOL
L'idea è che ogni pezzo faccia un mestiere solo: Caddy protegge ed espone il servizio, LiteLLM presenta ai client un'unica API, Ollama e vLLM eseguono realmente i modelli, PostgreSQL conserva lo stato applicativo e il volume LUKS fa sì che tutto ciò che conta sopravviva alla macchina.
Caddy: il confine con Internet
Caddy è l'unico componente dello stack che espone direttamente porte pubbliche.
Termina HTTPS, ottiene e rinnova automaticamente il certificato TLS e applica l'allowlist degli indirizzi IP autorizzati.
Un client che non proviene da una rete ammessa non arriva nemmeno a LiteLLM: riceve semplicemente un 403 Forbidden.
Questo primo livello non sostituisce l'autenticazione applicativa, ma riduce parecchio la superficie esposta.
LiteLLM: un endpoint, molti modelli
Dietro Caddy troviamo LiteLLM, che è il vero punto di ingresso applicativo.
Il client vede un'API compatibile con quella di OpenAI e specifica semplicemente il modello desiderato nella richiesta; LiteLLM decide a quale engine inoltrarla.
Da fuori quindi possiamo avere:
{
"model": "teacher"
}
oppure:
{
"model": "critic"
}
senza che il client debba sapere se teacher gira su vLLM e critic su Ollama.
LiteLLM gestisce inoltre le API key e persiste utenti, budget e statistiche di utilizzo su PostgreSQL, permettendoci di distribuire chiavi diverse ad applicazioni o persone senza consegnare a tutti la master key.
PostgreSQL: lo stato del servizio
PostgreSQL conserva ciò che rende LiteLLM qualcosa di più di un semplice reverse proxy: chiavi, configurazioni e dati di utilizzo.
Il database vive interamente sul volume persistente e viene sottoposto ogni notte a un pg_dump compresso con retention automatica.
Ollama: flessibilità
Ollama è la scelta comoda quando vogliamo sperimentare, cambiare spesso modello o utilizzare quantizzazioni GGUF.
È semplice, permette di mantenere più modelli disponibili e rende molto facile sostituirli senza ripensare il resto dell'architettura.
vLLM: throughput
vLLM affronta il problema da un'altra prospettiva.
Il modello tende a essere stabile, caricato in GPU e servito con continuous batching e una gestione della KV cache progettata per sostenere più richieste contemporanee.
Per questo lo utilizziamo quando non ci interessa tanto cambiare modello ogni cinque minuti quanto far lavorare bene quello che abbiamo scelto.
Lo stack permette di avviare anche due istanze vLLM distinte sulla stessa GPU, se la memoria lo consente.
Il volume LUKS: ciò che deve sopravvivere
Infine c'è il componente meno appariscente ma probabilmente più importante di tutti: il disco.
Compose, database, modelli, cache HuggingFace, immagini Docker, certificati e configurazioni vivono sul volume cifrato.
È questo che permette di trattare realmente la VPS come compute temporaneo.
Se domani la macchina non esiste più, l'architettura non è andata persa.
È semplicemente in attesa di una nuova GPU.
6. Quando una GPU deve ospitare più modelli
Lo stack supporta anche un pattern che stiamo utilizzando sempre più spesso:
un modello genera, un altro controlla.
Per esempio:
flowchart LR
P([prompt]) --> T["teacher<br/>genera"]
T --> CR["critic<br/>valuta"]
CR --> R([accetta / rifiuta])
Oppure generazione + verifica, LLM-as-judge, costruzione di dataset o distillazione.
Con vLLM ogni processo serve un modello.
Due modelli significano quindi due container.
E qui abbiamo incontrato una delle parti più interessanti del progetto: la gestione della VRAM.
Un modello non occupa soltanto la memoria necessaria ai pesi.
vLLM prealloca anche spazio per la KV cache.
Entra quindi in gioco:
--gpu-memory-utilization
Il problema è che il comportamento di questo parametro è cambiato tra versioni dell'engine.
Con alcune versioni il secondo processo ragiona su un limite cumulativo della GPU.
Con altre ragiona sulla propria quota confrontandola con la memoria libera.
Il risultato sono due errori molto diversi:
No available memory for the cache blocks
oppure:
Free memory on startup is less than desired
Lo abbiamo imparato nel modo tradizionale:
facendo crashare entrambi i container.
C'è però una regola che rimane sempre valida:
l'ordine di avvio conta.
Per questo il secondo vLLM parte soltanto quando il primo è diventato healthy e ha già allocato la propria memoria.
7. Il Day-2: usare e mantenere lo stack
Far partire qualcosa una volta è relativamente facile.
La domanda interessante è:
come si gestisce domani?
La maggior parte delle operazioni passa da config.env.
Per aggiungere un modello Ollama:
nano config.env
sudo ./scripts/06-pull-models.sh
sudo ./scripts/05-stack-config.sh
docker compose --env-file .env restart litellm
Per cambiare gli IP autorizzati:
nano config.env
sudo ./scripts/05-stack-config.sh
docker compose --env-file .env up -d caddy
Per controllare lo stato:
docker compose --env-file .env ps
Per guardare i log:
docker compose --env-file .env logs -f
Ogni servizio ha un healthcheck.
Quindi docker compose ps non ci dice soltanto che un processo esiste.
Ci dice se il servizio è realmente pronto.
Questo è particolarmente utile con vLLM, dove caricare decine di GB di pesi in GPU può richiedere diversi minuti.
Backup
LiteLLM utilizza PostgreSQL per conservare configurazioni e chiavi.
Ogni notte viene eseguito automaticamente un pg_dump compresso.
Vengono mantenute le ultime quattordici copie.
Il backup vive sul volume cifrato insieme al resto dello stack.
È utile soprattutto per errori logici o problemi del database.
Naturalmente non sostituisce un vero backup off-site del volume se il nostro threat model comprende anche la perdita completa dello storage del provider.
8. Collaudato rompendolo
Il repository non è nato così.
All'inizio era perfettamente lintato.
Shellcheck passava.
YAML valido.
docker compose config contento.
Poi lo abbiamo installato su una GPU VPS vera.
E una VPS vera è un ottimo strumento per distruggere le illusioni.
Il quoting che rompeva config.env
Il primo generatore scriveva alcune variabili senza quoting corretto.
Appena una configurazione conteneva spazi — per esempio una lista di IP o modelli — Bash interpretava il file come comandi distinti.
Risultato: gli script successivi morivano immediatamente.
Il codice era perfettamente lintato.
Ed era perfettamente inutilizzabile con alcuni dei default proposti dallo stesso programma.
Docker che riempiva il disco sbagliato
Avevamo spostato con cura il Docker data-root sul volume cifrato.
Controllato.
Stampato.
Tutto perfetto.
Poi abbiamo scaricato l'immagine vLLM.
Il disco di sistema è arrivato quasi al 100%.
Le versioni recenti di Docker utilizzano anche il containerd image store e le immagini non stavano finendo dove pensavamo.
Lo script ci mostrava il data-root corretto.
Ma mostrarlo non significava verificarlo.
Da allora il test non si limita a leggere la configurazione.
Scarica realmente un'immagine e controlla su quale filesystem finiscono i byte.
Il chown assassino
Il renderer dello stack eseguiva:
chown -R root:root
sull'intera directory.
Peccato che dentro quella directory ci fosse anche:
pgdata/
PostgreSQL nel container utilizza il proprio utente.
Rigenerare la configurazione significava quindi cambiare ownership ai file vivi del database mentre PostgreSQL li stava usando.
Risultato:
Permission denied
Questo bug oggi ha un test specifico.
La CI crea un file-esca con l'ownership del database e fallisce se lo script lo modifica.
Ed è probabilmente questa la lezione più utile dell'intero progetto:
Il gap fra "lintato" e "collaudato" si chiude soltanto eseguendo davvero il software.
Ogni bug trovato sul campo dovrebbe lasciare dietro di sé due cose:
una fix e un test.
9. Sicurezza e threat model
Il volume è cifrato con LUKS.
Per permettere lo sblocco automatico al boot utilizziamo un keyfile presente sulla VM.
È un compromesso deliberato.
Protegge bene nel caso in cui qualcuno ottenga:
- il disco dati dismesso;
- uno snapshot del solo volume;
- il volume montato separatamente dalla VM.
Non protegge invece da:
- un attaccante con root sulla macchina accesa;
- uno snapshot completo della VM che comprenda anche il keyfile.
Se questo non è sufficiente per il vostro scenario, la soluzione è diversa:
- passphrase richiesta al boot;
- remote unlocking;
- sistemi come Clevis/Tang.
Il repository non pretende che la soluzione scelta risolva minacce che non può risolvere.
Per il nostro caso l'obiettivo è soprattutto evitare che un disco contenente modelli, database e dati applicativi possa essere montato altrove in chiaro.
E soprattutto:
keyfile e passphrase vanno salvati fuori dalla VPS.
Se perdiamo entrambi, il volume cifrato non è particolarmente sicuro.
È semplicemente molto, molto vuoto dal nostro punto di vista.
10. Disaster recovery: distruggere la VM senza paura
Arriviamo alla parte che mi interessa di più.
La sera abbiamo finito di lavorare.
La GPU continua a costare.
Quindi premiamo:
Destroy.
La VM sparisce.
Con lei spariscono sistema operativo, driver e disco di sistema.
Il volume cifrato invece rimane.
La mattina dopo creiamo una nuova GPU VPS, possibilmente nella stessa region, agganciamo il disco e aggiorniamo il record DNS.
Poi cloniamo nuovamente il repository:
git clone https://github.com/doradame/GPU_VPS_LiteLLM
cd GPU_VPS_LiteLLM
Ripristiniamo dal password manager:
config.env
keyfile LUKS
Poi:
sudo ./scripts/01-nvidia-driver.sh
sudo reboot
sudo ./scripts/02-luks-volume.sh
sudo ./scripts/03-docker.sh
sudo ./scripts/04-nvidia-toolkit.sh
Lo script LUKS riconosce che il volume esiste già.
Non formatta nulla.
Lo apre e lo monta.
A quel punto:
cd /srv/llm/stack
docker compose --env-file .env up -d
I modelli sono ancora lì.
PostgreSQL è ancora lì.
Le API key sono ancora lì.
I certificati sono ancora lì.
Le immagini Docker sono ancora lì.
Non dobbiamo riscaricare decine di GB.
Non dobbiamo ricostruire il database.
Abbiamo semplicemente cambiato il computer sotto allo stack.
Ed è esattamente il comportamento che volevamo ottenere.
E se domani cambio GPU?
Nessun problema particolare.
Il volume non è legato alla scheda.
Se la nuova GPU ha una quantità simile di VRAM, possiamo normalmente ripartire con la stessa configurazione.
Se ne ha di più possiamo aumentare context window, concorrenza e budget della KV cache.
Se ne ha meno dobbiamo invece ridimensionare la configurazione o scegliere un modello più piccolo.
Una cosa ovviamente non cambia:
se i pesi del modello non entrano fisicamente nella VRAM, nessun parametro magico li farà entrare.
Se invece la GPU è molto più recente e la versione pinnata di vLLM non supporta correttamente l'architettura, basta aggiornare la versione dell'immagine e rigenerare lo stack.
11. Cosa questo stack non è
Non è Kubernetes.
Non è serverless.
Non è una piattaforma hyperscale.
Non è progettato per centinaia di tenant.
È uno stack pensato per una persona, un laboratorio, un piccolo team, applicazioni interne o pipeline AI controllate.
Non sostituisce neppure le API commerciali quando ci serve il miglior frontier model disponibile.
Risponde a una domanda diversa:
Voglio eseguire questi modelli open-weight, sotto il mio controllo, su hardware che posso noleggiare soltanto quando serve?
Se la risposta è sì, allora questo approccio comincia ad avere parecchio senso.
12. Perché ne è valsa la pena
Avremmo potuto acquistare un endpoint gestito e avere un modello disponibile in pochi minuti.
Ma non era quello l'esperimento.
Volevamo costruire una ricetta ripetibile per trasformare una GPU VPS abbastanza generica in:
una API LLM privata
+
compatibile OpenAI
+
con più engine
+
con storage persistente
+
cifrato
+
ricostruibile
Adesso quella ricetta esiste.
E soprattutto è stata provata nel modo che preferiamo:
rompendola.
Il disco di sistema riempito quasi completamente.
Il chown che uccide PostgreSQL.
Le differenze di comportamento fra versioni di vLLM.
Sono tutte cose molto più interessanti di uno screenshot in cui docker compose ps mostra cinque righe verdi.
Oggi quelle cicatrici sono diventate test automatici.
Il repository è qui:
https://github.com/doradame/GPU_VPS_LiteLLM
Licenza MIT.
Dentro trovate gli script, la documentazione completa, la procedura di disaster recovery e la CI.
Ma il risultato che mi interessava davvero era molto più semplice.
Ieri sera ho finito di utilizzare la GPU.
Ho controllato che il volume fosse a posto.
Ho premuto:
Destroy.
E sono andato a dormire.
Made in MojaLab.
Nessun modello è stato maltrattato durante i test. Un paio sono stati riavviati in loop, ma se lo meritavano.
