LLM self-hosted su VPS Hetzner con Ollama: deployment in produzione per PMI con vincoli di data sovereignty

LLM self-hosted su VPS Hetzner con Ollama: deployment in produzione per PMI con vincoli di data sovereignty

Nella mia sandbox di ricerca applicata, qualche mese fa, ho preso una decisione: smettere di fare solo benchmark di confronto tra Claude e i modelli frontier via API, e iniziare a usare un LLM self-hosted come strumento di lavoro vero per le mansioni ripetitive dove il costo per token diventa significativo. Server scelto: un dedicato Hetzner con GPU, a Falkenstein, datacenter tedesco ISO/IEC 27001:2022 certificato (ente SOCOTEC, scope esercizio dei park di Norimberga, Falkenstein e Helsinki), nessun dato che esce dall'UE. Stack iniziale: Ubuntu 24.04 LTS, Docker, Ollama con quantizzazioni GGUF, reverse proxy Caddy con mTLS, firewall nftables, VPN WireGuard per l'accesso amministrativo. Lavoro che il server esegue oggi: classificazione di alcune migliaia di documenti PDF scansionati al giorno, generazione di abstract in italiano, embedding per un indice vettoriale pgvector su una knowledge base tecnica interna. Costo variabile di inferenza: zero per token. Costo fisso: il canone del dedicato, più una quota di Claude Sonnet che tengo a budget per i task che richiedono davvero il frontier model, così da avere un confronto onesto dentro lo stesso workflow.

Dopo diversi mesi di esercizio posso confrontare in modo strutturato quattro percorsi che vedo proposti alle PMI italiane come "soluzioni AI": managed API pura (Claude/OpenAI), ibrido cloud con provider europeo, self-hosted su VPS o dedicato con GPU come il mio setup, infrastruttura on-premise. Non sono equivalenti e non servono gli stessi casi d'uso. In questo articolo li metto in tabella con i parametri che contano per un decision maker PMI (costo fisso e variabile, latenza, giurisdizione, rischio operativo, tempo di implementazione) e ti spiego quando ciascuno vince. Ma soprattutto ti racconto la scelta tecnica che ha cambiato il rendimento del mio setup a parità di hardware: non è il modello, è il motore di inferenza. Ollama è il punto di partenza giusto, ma in produzione l'ho sostituito con llama.cpp governato da llama-swap, e qui spiego il perché tecnico.

Perché parliamo di self-hosted nel 2026 e non nel 2023

Nel 2023 il ragionamento prevalente era "i modelli a pesi aperti non reggono il confronto con GPT-4, meglio pagare i token". Nel 2026 quel ragionamento è obsoleto su tre dimensioni. La prima è la qualità: famiglie come Llama 4, Mistral Large 3, Qwen3.5 e DeepSeek V4 coprono il grosso dei task di dominio ristretto (classificazione, estrazione di entità, riassunto italiano, generazione strutturata). Non sostituiscono un Claude Sonnet nei ragionamenti complessi multi-step, ma coprono una larga fetta dei casi d'uso aziendali reali, dove la difficoltà non è il modello, è l'architettura intorno. La seconda dimensione è il costo hardware: una GPU con 48 GB di VRAM, sufficiente a far girare un 70B quantizzato a 4 bit in modo fluido, oggi in hosting dedicato europeo costa una frazione di quanto costava due anni fa. Nel mio confronto tra GPU cloud per inference LLM self-hosted per PMI ho valutato quattro provider alternativi con parametri di costo, giurisdizione e tempi di provisioning. La terza dimensione, quella che pesa di più per il decisore italiano, è regolatoria.

Il report Deloitte State of AI in the Enterprise 2026, presentato a Davos il 21 gennaio 2026 su un campione di 3.235 leader in 24 paesi, riporta che l'83% delle aziende considera la sovereign AI strategica: dati sotto giurisdizione europea, vendor valutati per country of origin, controllo diretto sulla supply chain AI. L'AI Act europeo ha già il regime GPAI in enforcement dalla Commissione dal 2 agosto 2026, con obblighi di trasparenza e tracciabilità. La direttiva NIS2, recepita in Italia, impone requisiti specifici di gestione della supply chain IT che includono anche i fornitori di servizi cloud. Per una PMI italiana che rientra nel perimetro NIS2 (e sono molte più di quelle che lo sanno), inviare i payload dei ticket a un'API negli Stati Uniti è un rischio di compliance concreto, non teorico.

Se questo tema ti interessa anche dal punto di vista architetturale, nel mio hub dedicato all'integrazione AI trovi articoli su RAG privato, MCP server custom che collegano gestionali esistenti a modelli self-hosted, e pattern di deployment per stack ibridi PHP+Python, dove ciascun linguaggio fa quello che fa meglio.

Quattro percorsi di deployment a confronto

Ecco la tabella che uso nei miei assessment quando aiuto una PMI a scegliere il percorso AI più adatto. I costi sono ordini di grandezza su un carico mensile di alcune decine di milioni di token complessivi (input + output), che corrisponde a un'integrazione AI seria su un'azienda da qualche decina di dipendenti con workflow parzialmente automatizzati. I prezzi dei provider non li riporto puntuali: invecchiano e restano nelle cache dei crawler per settimane, quindi ragiono per classi.

CriterioManaged API puraIbrido cloud EUSelf-hosted GPUOn-premise dedicato
Costo fisso/mesenulloclasse bassa (cloud EU)classe media (dedicato GPU EU)classe alta (ammortamento + rack)
Costo variabilea token, anche elevatoa token, ridottonullo (dopo il canone)nullo (dopo l'hardware)
Latenza p50rete pubblicarete pubblicabassa (rete privata)minima (LAN)
Giurisdizione datiUSA / multiplaUE (contratto dedicato)UE (server tuo)nazionale
Data sovereigntyvia contrattovia contratto EUtotale (nulla esce)totale
Tempo implementazionegiorniuna/due settimanedue/quattro settimanemesi
Competenze richiestedev backenddev + cloud architectdev + DevOps + GPUdev + DevOps + ops fisico
Modello top disponibilefrontier (Opus, GPT-5.5)frontier via marketplacepesi aperti (70B e oltre)stesso self-hosted
Rischio operativobasso (gestisce il vendor)basso-mediomedio (gestisci la GPU)alto (gestisci tutto)

Due letture strategiche. Il self-hosted con GPU dedicata è il percorso con il miglior rapporto tra sovranità, costo totale e tempo di implementazione per PMI che hanno già un minimo di competenze DevOps interne o un consulente senior che gestisce l'infrastruttura. L'on-premise dedicato ha senso solo per vincoli regolatori speciali (difesa, sanità, bancario con deroghe) o per volumi tali da ammortizzare il CapEx. La managed API pura resta razionale per pilot brevi, sperimentazione e i task che richiedono davvero il frontier model. C'è però una variabile che la tabella nasconde e che a parità di GPU sposta il rendimento più di quanto si creda: lo strato software che serve il modello.

Perché in produzione passo da Ollama a llama.cpp con llama-swap?

Ollama è un ottimo punto di partenza: in pochi comandi scarichi un modello, lo servi via API e prototipi. Ma in produzione, sullo stesso hardware, llama.cpp con llama-swap mi dà più throughput e molto più controllo, e il motivo è strutturale. Ollama non è un motore di inferenza alternativo: è un wrapper attorno a llama.cpp. Internamente chiama la libreria C/C++ di llama.cpp via CGo ed esegue una versione del suo server, come documenta anche Red Hat Developer, che scrive nero su bianco che il motore sottostante di llama.cpp è ciò che abilita strumenti come Ollama. Lo stesso motore, quindi, con uno strato di gestione in più.

Ollama incapsula llama.cpp e ci aggiunge un registry dei modelli, i download automatici, una REST API e i Modelfile. Comodo per partire, ma quello strato di management ha un costo: in produzione paghi overhead che non controlli, e perdi l'accesso diretto ai parametri che contano davvero.

Quell'overhead è misurabile e ripetibile: i benchmark indipendenti che ho incrociato riportano per Ollama una penalità tipicamente del 5-15% sul throughput in token rispetto a llama.cpp puro sullo stesso modello e sulla stessa GPU, con punte fino a circa il 30% in configurazioni meno favorevoli. Non è la matematica dell'inferenza a cambiare (è la stessa, condividono il backend), ma il Go API layer di Ollama, le scelte di parametri di default e la gestione della memoria. Per un singolo utente la differenza è impercettibile, ma su batch processing, pipeline CI e più consumer concorrenti diventa reale e si traduce in canone che paghi senza usarlo.

Il punto vero, però, non è il 10% di throughput. È il controllo. llama.cpp espone llama-server, che la documentazione ufficiale descrive come "a lightweight, OpenAI API compatible, HTTP server for serving LLMs" (vedi il repository ufficiale ggml-org/llama.cpp): un binario C/C++ senza dipendenze, con supporto a quantizzazione da 1.5 a 8 bit e inferenza ibrida CPU+GPU per modelli che eccedono la VRAM. Servendo direttamente llama-server decido io il livello di quantizzazione, la dimensione del contesto (--ctx-size), il dimensionamento della KV cache, il numero di layer da offloadare sulla GPU (--n-gpu-layers), il batching. Con Ollama questi parametri sono astratti dietro default che vanno bene per giocare, non per dimensionare un workload di produzione su una GPU che hai pagato per saturare bene.

Resta il problema che Ollama risolveva e che non voglio perdere: lo swap dei modelli on-demand. Qui entra llama-swap. Il suo repository ufficiale lo descrive senza giri di parole: "Run multiple generative AI models on your machine and hot-swap between them on demand." È un proxy OpenAI-compatibile che, quando arriva una richiesta, estrae il campo model e carica al volo la configurazione del server giusto, spegnendo quello sbagliato se serve. Tradotto sul mio caso: una GPU da 48 GB non tiene in VRAM contemporaneamente un 70B per i riassunti, un modello da embedding e un piccolo modello veloce per il routing. Con llama-swap non devo: dichiaro le configurazioni in un file, e il primo che chiede llama3.3:70b lo fa caricare, il primo che chiede l'embedding model fa lo swap. Tutto dietro un singolo endpoint OpenAI-compatibile, con cui il mio backend parla senza sapere quale processo llama-server c'è dietro in quel momento. È esattamente il comportamento "magico" di Ollama, ma con il motore nudo sotto e il controllo dei parametri in mano a me.

La regola che applico: Ollama per il PoC, llama.cpp con llama-swap per la produzione. Lo stesso GGUF, lo stesso endpoint OpenAI-compatibile per le applicazioni, ma sotto un motore che spremi e parametri che governi, più lo swap on-demand che fa stare in VRAM solo ciò che serve, quando serve.

Questa scelta di runtime non è banale e non è universale: per alta concorrenza multi-tenant né llama-server né Ollama sono lo strumento, e lì il discorso si sposta su serving ad alto throughput. Ho approfondito il confronto runtime per scenario nell'articolo dedicato a vLLM, Ollama e llama.cpp come runtime di inference self-hosted: la sintesi è che llama.cpp vince sul controllo e sull'hardware modesto, vLLM sul throughput concorrente, e Ollama resta il modo più rapido per iniziare.

Il mio setup Hetzner in dettaglio operativo

Il server che descrivo è quello che gira nella mia sandbox di ricerca. Non è l'unica configurazione possibile né la migliore in assoluto: è una configurazione che funziona, replicabile da un consulente senior in qualche giornata di lavoro, hardening e testing inclusi.

Dimensionamento hardware e provider

Per la GPU su Hetzner il riferimento di catalogo è la GPU-Line (GEX) dei dedicati, accanto alle linee EX (Intel), AX (AMD) e RX (Arm64). La GPU che monto ha 48 GB di VRAM, abbastanza per caricare un 70B in Q4_K_M (circa 42 GB) lasciando margine per KV cache e batching. La quantizzazione non è gratis: nel mio benchmark interno su classificazione italiana, la Q4_K_M mantiene una qualità sostanzialmente allineata al riferimento Claude Sonnet, mentre scendere a Q3_K o IQ2 per far stare il modello in una GPU più piccola degrada la qualità in modo misurabile. Quando dimensiono lo faccio sulla quantizzazione che regge il task, non su quella che ci sta e basta.

Per un'infrastruttura cloud europea self-managed con ottimo rapporto prezzo/prestazioni e conformità GDPR, Hetzner resta un riferimento solido (datacenter UE, ISO/IEC 27001:2022). Per le PMI italiane che vogliono invece un'infrastruttura gestita, con disaster recovery e il dato che non lascia l'Italia, la prima scelta è RHX (datacenter Milano, adiacente al MiX, e Padova, gestione sistemistica inclusa, registrar .it accreditato).

Non tutti i casi d'uso richiedono modelli da 70B. Se il workload è classificazione, estrazione, NER italiano, riassunto breve, un modello da circa 24B quantizzato Q4_K_M occupa una manciata di GB di VRAM e gira su una GPU da 24 GB, abbattendo il canone. Con llama-swap posso anche tenere configurati entrambi e lasciare che sia la richiesta a decidere quale caricare. Il dimensionamento va fatto sul carico reale, non sull'ambizione.

Rete, sicurezza, isolamento

La prima cosa che ho fatto dopo il provisioning è stata chiudere quasi tutto: nftables con policy default drop, solo SSH su porta custom esposto al mondo (dietro Fail2ban con regole adattive), tutto il resto raggiungibile esclusivamente via VPN WireGuard. Il tunnel WireGuard termina sul server e serve sia per il mio accesso amministrativo sia per le applicazioni backend che consumano l'inferenza: queste non parlano con il server via internet pubblico, ma via rete privata. Anche se un attaccante scoprisse l'IP e trovasse un 0day sul motore di inferenza, la superficie esposta resterebbe la sola porta SSH hardened.

Il processo llama-server (o, in fase di prototipo, il daemon Ollama) è bindato esplicitamente su 127.0.0.1, mai sull'interfaccia pubblica. llama-swap ascolta solo sull'interfaccia WireGuard. Caddy fa da reverse proxy TLS davanti, anch'esso solo sulla WireGuard. Le applicazioni client (backend PHP Laravel su altri VPS) si connettono al peer WireGuard con certificato client mTLS obbligatorio: senza certificato la connessione viene chiusa al livello TLS, prima ancora di toccare l'endpoint di inferenza. Questo è coerente con LLM03 - Supply Chain e LLM02 - Sensitive Information Disclosure della OWASP Top 10 for LLM Applications 2025: tengo il log-level minimale, ruoto i log, non persisto i prompt su disco, e verifico lo SHA-256 dei GGUF contro i manifesti dei maintainer originali prima di caricarli, perché un modello scaricato da un repository non firmato non è sempre quello che dichiara di essere.

Integrazione con backend PHP

Il codice Laravel che consuma l'LLM è scritto come se parlasse con una qualunque API HTTP OpenAI-compatibile: niente accoppiamento al runtime sottostante, solo un'interfaccia LlmClientContract con implementazioni intercambiabili. Questo è il vantaggio architetturale del passaggio a llama-server con llama-swap: l'endpoint resta OpenAI-compatibile, quindi il client PHP non cambia di una riga quando sotto sostituisco Ollama con il motore nudo. Routare un task al modello più economico che soddisfa i requisiti di qualità diventa una scelta di configurazione, non di codice.

interface LlmClientContract
{
    public function complete(string $prompt, array $options = []): LlmResponse;
    public function embed(string $text): EmbeddingVector;
}

final class OpenAiCompatibleClient implements LlmClientContract
{
    public function __construct(
        private readonly HttpClient $http,
        private readonly string $model = 'llama3.3-70b-q4',
        private readonly string $endpoint = 'https://llm-01.internal:443'
    ) {}

    public function complete(string $prompt, array $options = []): LlmResponse
    {
        $start = hrtime(true);

        // llama-swap legge il campo "model" e carica al volo il server giusto:
        // stesso endpoint, lo swap del modello avviene dietro le quinte.
        $response = $this->http
            ->withOptions([
                'verify'  => '/etc/ssl/mtls/ca.crt',        // CA per validare il server
                'cert'    => '/etc/ssl/mtls/client.crt',    // certificato client mTLS
                'ssl_key' => '/etc/ssl/mtls/client.key',    // chiave privata client
            ])
            ->timeout(120)
            ->post("{$this->endpoint}/v1/chat/completions", [
                'model'       => $this->model,
                'messages'    => [['role' => 'user', 'content' => $prompt]],
                'stream'      => false,
                'temperature' => $options['temperature'] ?? 0.2,
            ])
            ->throw()
            ->json();

        return new LlmResponse(
            content: $response['choices'][0]['message']['content'],
            latencyMs: (hrtime(true) - $start) / 1_000_000,
            tokenCount: $response['usage']['completion_tokens'] ?? 0,
            model: $this->model,
            provider: 'llama_cpp_self_hosted'
        );
    }
}

Il fatto che llama-swap esponga lo stesso schema OpenAI delle API commerciali significa che la stessa interfaccia LlmClientContract serve, senza modifiche, anche un ClaudeClient o un OpenAiClient per i fallback: è il pattern che mi permette di tenere il frontier model come rete di sicurezza senza accoppiare il codice a un singolo provider. Il pattern è MCP-compatibile: sto migrando la selezione del modello a un MCP server che la espone come tool registrato, coerente con lo standard donato da Anthropic alla Linux Foundation il 9 dicembre 2025 (approfondisco la governance dell'ecosistema in MCP donato alla Linux Foundation).

Monitoraggio, quote, failover

Prometheus scrapa un exporter ogni pochi secondi: latency p50/p95/p99, VRAM usage, GPU utilization, temperatura, throughput in token/sec. Dashboard Grafana con alert Telegram: se la p95 supera una soglia per più di qualche minuto, scatta un ping; se la VRAM va in saturazione (sintomo di un modello duplicato caricato per richieste concorrenti su modelli diversi), un ping più aggressivo. Il failover verso Claude API è automatico: il router ha un health check periodico verso l'endpoint di inferenza, e se più check consecutivi falliscono ruota il traffico sul frontier model fino al ripristino. Nei mesi di esercizio questo failover è scattato un paio di volte, sempre per cause prevedibili (un aggiornamento del motore andato male in test, un reboot programmato del datacenter notificato con largo anticipo), mai per un'instabilità del modello in sé.

L'aspetto più sottovalutato della OWASP LLM Top 10 in self-hosting è LLM10 - Unbounded Consumption: senza rate limiting applicativo, un singolo workflow rotto che manda migliaia di richieste al minuto non ti fa fatturare migliaia di euro in token come su API, ma ti porta la GPU al thermal throttling e ti fa crashare il servizio. Rate limit per ogni consumer, budget giornaliero per API key: il "denial-of-wallet" diventa "denial-of-GPU", e va contenuto allo stesso modo.

Il caso PMI reale e il modello di decisione che applico

Quando una PMI italiana mi contatta per un assessment AI, il primo parametro non è il modello top: è la sensibilità dei dati e i vincoli regolatori applicabili. Se l'azienda processa dati sensibili ex art. 9 GDPR (sanitari, giudiziari, biometrici), oppure rientra nel perimetro NIS2 categoria essenziale, oppure ha clausole contrattuali B2B che vietano il cloud extra-UE, il self-hosted è la risposta di default salvo giustificazioni tecniche forti in contrario. E dentro la risposta self-hosted, la scelta del runtime è parte dell'engineering, non un dettaglio: è la differenza tra un PoC che gira e un servizio dimensionato.

Non racconto case study di clienti specifici, perché proteggere la loro privacy è prioritario. La mia competenza la verifichi dagli articoli tecnici di questo blog, dal codice open source su GitHub e dalla metodologia documentata pubblicamente: quello che descrivo qui è la mia pipeline personale, non l'infrastruttura di un cliente.

C'è un dato che inquadra l'opportunità. Secondo l'Osservatorio Artificial Intelligence del Politecnico di Milano, solo una minoranza delle grandi imprese italiane ha una governance AI strutturata: la maggior parte sperimenta via API pubbliche senza DPIA e senza valutazione del rischio, sperando che la fortuna tenga. Il gap non è la mancanza di modelli (sono ormai una commodity), è la mancanza di chi progetta l'architettura intorno al modello: la rete, l'isolamento, il runtime giusto, il failover, l'audit trail. È esattamente il terreno dove un'analisi seria fa la differenza tra chi scala senza controllo e chi lo fa in modo informato.

Hai una PMI con dati sensibili o vincoli di data sovereignty, stai valutando un deployment AI e vuoi capire se il self-hosted su GPU europea (e con quale runtime) ha più senso per il tuo caso rispetto a un percorso ibrido? Il modulo di preventivo gratuito ti dà una prima lettura in 7 domande, 2 minuti: ti dico con chiarezza se il tuo progetto rientra in quello che so fare bene, come imposterei un primo confronto, quali domande aggiuntive ha senso porsi. Se il caso richiede un profilo diverso dal mio, te lo dico e, quando posso, ti indico una direzione utile.

Ultima modifica: