Salta al contenuto principale

Servire Ollama in produzione VPS EU 2026: setup completo Hetzner

Quanta memoria serve, che velocità aspettarsi e come proteggere l'endpoint quando servi Ollama su un VPS europeo: calcoli eseguiti e prezzi datati.

Cosa ne pensi di questo articolo?

Servire Ollama in produzione VPS EU 2026: setup completo Hetzner
Sovereign AI PMI EUFoto:Tara WinsteadPexels

In Breve

Quanta memoria serve, che velocità aspettarsi e come proteggere l'endpoint quando servi Ollama su un VPS europeo: calcoli eseguiti e prezzi datati.

Che cosa decidi davvero quando servi un modello su un VPS europeo

Servire un modello di linguaggio su un tuo server in Europa significa prendere tre decisioni, non una: quanta memoria serve (e quindi quale macchina), come impedire che l'endpoint sia raggiungibile da chiunque, e come accettare che su una CPU la generazione sia lenta. Ollama, il programma che usiamo come esempio, rende facile l'installazione; non rende facili queste tre scelte, e sono quelle in cui una PMI sbaglia.

La risposta breve, prima del dettaglio: un modello da 7-8 miliardi di parametri, quantizzato, entra in 8-16 GB di RAM, ma su una VPS senza GPU produce poche decine di token al secondo nel caso migliore e pochi token al secondo nel caso tipico. Funziona per un reparto che interroga i propri documenti, non per un servizio con decine di utenti contemporanei. L'articolo ti dà i calcoli per verificarlo sul tuo caso, e dichiara dove i numeri sono ipotesi da misurare.

Un paragone per fissare l'idea: scegliere la macchina è come scegliere il furgone per un trasloco. Il volume del carico (i pesi del modello) decide se entra; la lunghezza dell'ultimo scatolone che apri (il contesto) decide se resta posto per altro; la velocità del furgone (la banda di memoria) decide quanto ci metti. Un furgone più piccolo non è più lento perché è peggiore, ma perché porta meno roba per viaggio.

Quando ha senso farlo in proprio

Le ragioni che reggono, per una PMI, sono poche e vanno dette con i loro costi. Le altre, come «costa zero in API», valgono solo dopo aver contato il resto (lo faremo nell'articolo sui costi).

  • Dati che non vuoi mandare a un terzo: contratti, cartelle cliniche, verbali. Se i testi restano sul tuo server, il fornitore del modello non li vede mai.
  • Volume stabile e prevedibile: un uso regolare ammortizza un canone fisso meglio di un uso a picchi.
  • Indipendenza da un singolo fornitore: i pesi aperti si possono spostare su un'altra macchina o un altro fornitore europeo.
  • Esperimenti senza costo marginale: provare dieci prompt in più non genera una fattura.

Nessuna di queste ragioni dice «è più veloce» o «è più capace»: un modello piccolo in proprio è quasi sempre più lento e meno capace di un modello grande in cloud. Se la qualità della risposta conta più della riservatezza, il confronto va fatto con dati tuoi: l'articolo su come scegliere fra modello piccolo e grande mostra come, e quello su quando il cloud europeo vince elenca i casi in cui conviene non farlo.

Quanta memoria serve: pesi, cache e margine

La memoria occupata da un modello in esecuzione ha tre parti. I pesi sono il file del modello: dipendono dal numero di parametri e dalla quantizzazione. La cache K/V è la memoria che l'attenzione usa per non rileggere l'intero testo a ogni token: cresce linearmente col contesto e con il numero di richieste elaborate in parallelo. Il margine copre buffer di calcolo, sistema operativo e tutto il resto.

Per i pesi non conviene calcolare a mente: la libreria di Ollama pubblica la dimensione del file di ciascun modello. Dividendo quella dimensione per il numero di parametri (dalla scheda del modello) si ottengono i bit per peso effettivi, che risultano vicini a 4,9: la quantizzazione predefinita è a circa 4-5 bit, non a 4 esatti. La cache K/V si calcola invece dall'architettura: due matrici (K e V) per strato, per ciascuna delle teste KV, per la dimensione della testa, per due byte in virgola mobile a 16 bit. Lo fa il programma che segue.

"""Dimensionamento della memoria per servire un modello in locale.

Tutti i dati di ingresso vengono da fonti pubbliche (vedi l'articolo):
- parametri e architettura: config.json e API di Hugging Face;
- dimensione del file quantizzato: pagina del modello nella libreria Ollama.
Le ipotesi sono marcate IPOTESI.
"""
GIB = 1024 ** 3

MODELLI = {
    # nome: (parametri, strati, teste KV, dim. testa, GB del tag predefinito in libreria)
    "llama3.1:8b": (8_030_261_248, 32, 8, 128, 4.9),
    "mistral:7b": (7_248_023_552, 32, 8, 128, 4.4),
    "qwen2.5:7b": (7_615_616_512, 28, 4, 128, 4.7),
}
BYTE_KV = 2  # IPOTESI: cache K/V in f16, il valore predefinito di Ollama
OVERHEAD_GIB = 1.0  # IPOTESI: buffer di calcolo e runtime, da misurare sul proprio carico
RISERVA_OS_GIB = 1.5  # IPOTESI: sistema operativo, nginx, il resto
RAM_GB = {"CCX13": 8, "CCX23": 16, "CCX33": 32}  # pagina Hetzner General Purpose


def kv_per_token(strati, teste_kv, dim_testa):
    # 2 = una matrice K e una V per ogni strato
    return 2 * strati * teste_kv * dim_testa * BYTE_KV


print("bit per peso impliciti nel file scaricato (GB decimali, ipotesi)")
for nome, (par, st, kv, hd, gb) in MODELLI.items():
    print(f"  {nome:12s} {gb * 1e9 * 8 / par:5.2f} bit/peso")

print("\nKV cache per token e per 8192 token di contesto")
for nome, (par, st, kv, hd, gb) in MODELLI.items():
    b = kv_per_token(st, kv, hd)
    print(f"  {nome:12s} {b // 1024:4d} KiB/token  {b * 8192 / GIB:5.2f} GiB a 8192")

print("\nMemoria totale (pesi + KV + overhead) e verdetto per taglia di RAM")
for contesto, parallele in [(4096, 1), (8192, 1), (8192, 4)]:
    print(f"contesto {contesto}, richieste parallele {parallele}")
    for nome, (par, st, kv, hd, gb) in MODELLI.items():
        pesi = gb * 1e9 / GIB
        cache = kv_per_token(st, kv, hd) * contesto * parallele / GIB
        tot = pesi + cache + OVERHEAD_GIB
        esito = []
        for srv, ram in RAM_GB.items():
            ok = tot + RISERVA_OS_GIB <= ram * 1e9 / GIB
            esito.append(f"{srv}:{'si' if ok else 'NO'}")
        print(f"  {nome:12s} {tot:5.2f} GiB  " + " ".join(esito))

# Output:
# bit per peso impliciti nel file scaricato (GB decimali, ipotesi)
#   llama3.1:8b   4.88 bit/peso
#   mistral:7b    4.86 bit/peso
#   qwen2.5:7b    4.94 bit/peso
#
# KV cache per token e per 8192 token di contesto
#   llama3.1:8b   128 KiB/token   1.00 GiB a 8192
#   mistral:7b    128 KiB/token   1.00 GiB a 8192
#   qwen2.5:7b     56 KiB/token   0.44 GiB a 8192
#
# Memoria totale (pesi + KV + overhead) e verdetto per taglia di RAM
# contesto 4096, richieste parallele 1
#   llama3.1:8b   6.06 GiB  CCX13:NO CCX23:si CCX33:si
#   mistral:7b    5.60 GiB  CCX13:si CCX23:si CCX33:si
#   qwen2.5:7b    5.60 GiB  CCX13:si CCX23:si CCX33:si
# contesto 8192, richieste parallele 1
#   llama3.1:8b   6.56 GiB  CCX13:NO CCX23:si CCX33:si
#   mistral:7b    6.10 GiB  CCX13:NO CCX23:si CCX33:si
#   qwen2.5:7b    5.81 GiB  CCX13:si CCX23:si CCX33:si
# contesto 8192, richieste parallele 4
#   llama3.1:8b   9.56 GiB  CCX13:NO CCX23:si CCX33:si
#   mistral:7b    9.10 GiB  CCX13:NO CCX23:si CCX33:si
#   qwen2.5:7b    7.13 GiB  CCX13:NO CCX23:si CCX33:si

Tre cose si leggono nell'uscita. La prima: Llama 3.1 8B e Mistral 7B pagano 128 KiB di cache per token, Qwen 2.5 7B solo 56 KiB, perché ha 4 teste KV invece di 8 e meno strati; a 8192 token la differenza è mezzo gibibyte, trascurabile in un caso e decisiva nell'altro. La seconda: con 4 richieste in parallelo la memoria sale di circa 3 GiB per Llama, perché la cache si moltiplica, e non il file dei pesi. La terza: i 7,5 GiB utili di una macchina da 8 GB sono giusti per Mistral a 4096 di contesto e non bastano per Llama con la stessa configurazione, secondo le ipotesi dichiarate in testa al programma (1 GiB di overhead, 1,5 GiB per il sistema). Sono ipotesi tue da misurare: guarda la memoria del processo con il tuo carico e correggi le due costanti.

Il programma non conosce due fatti. Se il contesto richiesto dall'applicazione supera quello che il modello supporta, la richiesta viene troncata o rifiutata; e se la RAM non basta, il sistema operativo comincia a usare lo swap su disco, con un rallentamento che si nota subito. In entrambi i casi la prima cosa da cambiare è la dimensione del modello o il contesto, non la macchina.

Quanto è veloce: il tetto imposto dalla banda di memoria

Per produrre ogni token un modello denso rilegge, in sostanza, tutti i suoi pesi. Su una CPU il collo di bottiglia è quindi la banda di memoria, non il numero di core: se la banda è B gigabyte al secondo e i pesi pesano P gigabyte, la velocità non supera B/P token al secondo. È un tetto, non una previsione, perché il calcolo vero costa altro tempo, ma ti dice subito che cosa è impossibile.

La banda di una macchina virtuale condivisa non è un dato di listino: dipende dal processore e da chi altro usa l'host. Per questo il programma la lascia come ipotesi e mostra tre valori; la misura vera la ottieni con una richiesta all'API di Ollama, i cui campi di risposta eval_count e eval_duration (nanosecondi) sono documentati e danno direttamente i token al secondo.

"""Limite teorico di velocita' e lettura delle metriche di Ollama.

Per generare un token un modello denso legge (circa) tutti i suoi pesi dalla
memoria. Se la banda di memoria e' B e i pesi pesano P byte, la velocita'
non puo' superare B / P token al secondo. E' un tetto, non una previsione.
"""

PESI_GB = {"llama3.1:8b": 4.9, "mistral:7b": 4.4, "qwen2.5:3b": 1.9}
BANDE_GB_S = [20, 40, 80]  # IPOTESI: la banda reale di una VM va misurata


def tetto(banda_gb_s, pesi_gb):
    return banda_gb_s / pesi_gb


print("tetto teorico in token/s (banda ipotizzata -> modello)")
for nome, p in PESI_GB.items():
    celle = "  ".join(f"{b} GB/s: {tetto(b, p):5.1f}" for b in BANDE_GB_S)
    print(f"  {nome:12s} {celle}")

RISPOSTA = 300  # token di una risposta di media lunghezza (IPOTESI)
print(f"\ntempo minimo per {RISPOSTA} token, llama3.1:8b")
for b in BANDE_GB_S:
    print(f"  {b} GB/s -> {RISPOSTA / tetto(b, PESI_GB['llama3.1:8b']):5.1f} s")


def token_al_secondo(risposta):
    """Campi documentati nell'API di Ollama: eval_count (token generati) e
    eval_duration (nanosecondi spesi a generarli)."""
    return risposta["eval_count"] / (risposta["eval_duration"] / 1e9)


# Risposta di ESEMPIO costruita a mano, non misurata: serve a mostrare la formula.
esempio = {"eval_count": 120, "eval_duration": 15_000_000_000}
print(f"\nesempio illustrativo: {token_al_secondo(esempio):.1f} token/s")

# Output:
# tetto teorico in token/s (banda ipotizzata -> modello)
#   llama3.1:8b  20 GB/s:   4.1  40 GB/s:   8.2  80 GB/s:  16.3
#   mistral:7b   20 GB/s:   4.5  40 GB/s:   9.1  80 GB/s:  18.2
#   qwen2.5:3b   20 GB/s:  10.5  40 GB/s:  21.1  80 GB/s:  42.1
#
# tempo minimo per 300 token, llama3.1:8b
#   20 GB/s ->  73.5 s
#   40 GB/s ->  36.8 s
#   80 GB/s ->  18.4 s
#
# esempio illustrativo: 8.0 token/s

Con una banda di 40 GB/s, Llama 3.1 8B non può superare circa 8 token al secondo, e una risposta di 300 token richiede almeno 37 secondi. Un modello da 3 miliardi di parametri (qui Qwen 2.5 3B, 1,9 GB) raddoppia o triplica il tetto, al prezzo di una minore capacità. L'esempio finale usa una risposta costruita a mano per mostrare la formula: i numeri veri li produce la tua macchina. Su CPU, la lentezza non è un difetto di configurazione: è la fisica del problema, e l'unico modo di aggirarla è un modello più piccolo, una GPU o la coda di lavori.

Quale server: i prezzi che contano, con la data

I prezzi dei server cambiano, e quelli che circolano nei vecchi articoli sono spesso superati. La pagina «General Purpose» di Hetzner, consultata il 2026-11-13, riporta per la sede di Norimberga (NBG1) i valori della tabella, tutti al mese e IVA esclusa. La stessa pagina indica prezzi «da» più bassi per altre sedi e, accanto ad alcune configurazioni, l'avviso «currently unavailable»: la disponibilità si controlla nella console, non nell'articolo.

IstanzavCPU dedicateRAMDiscoNBG1, €/mese IVA esclusa
CCX1328 GB80 GB NVMe43,49
CCX23416 GB160 GB NVMe86,49
CCX33832 GB240 GB NVMe138,99
CCX431664 GB360 GB NVMe276,49

Il confronto col programma di memoria è immediato: per un modello da 7-8 miliardi di parametri la macchina da 8 GB è al limite, quella da 16 GB è la prima comoda, quella da 32 GB lascia spazio a due modelli o a più richieste in parallelo. Le istanze CCX hanno vCPU dedicate, quindi la velocità è più regolare di una VM condivisa, ma il prezzo mensile è di un ordine di grandezza superiore a quello di un piccolo VPS da sito web: ricontrolla sempre la pagina prima di fare conti, e usa l'articolo sui costi del self-hosting per decidere se la spesa si ripaga.

Installare Ollama e configurarlo per restare in memoria

L'installazione su Linux, secondo la documentazione ufficiale, è un comando: curl -fsSL https://ollama.com/install.sh | sh. Eseguire uno script scaricato direttamente in una shell è una scelta di fiducia verso chi lo pubblica; se la tua policy lo vieta, la stessa pagina descrive l'installazione manuale con il pacchetto compresso e un'unità systemd. In ogni caso il servizio resta in ascolto su 127.0.0.1:11434, e l'indirizzo cambia solo se imposti OLLAMA_HOST.

Le impostazioni che contano si danno con un file di override di systemd. Il file seguente le mostra una per una; i valori predefiniti sono quelli della FAQ ufficiale di Ollama, consultata il 2026-11-13.

# /etc/systemd/system/ollama.service.d/override.conf
# Si crea con: sudo systemctl edit ollama.service
[Service]
# Lascia OLLAMA_HOST al valore predefinito (127.0.0.1:11434): niente porta aperta.
# Quanto resta in memoria un modello dopo l'ultima richiesta. Il valore predefinito
# e' 5 minuti; -1 lo tiene caricato per sempre.
Environment="OLLAMA_KEEP_ALIVE=30m"
# Richieste elaborate insieme da ciascun modello. Predefinito: 1.
# La RAM cresce di OLLAMA_NUM_PARALLEL x OLLAMA_CONTEXT_LENGTH (vedi memoria.py).
Environment="OLLAMA_NUM_PARALLEL=2"
# Modelli caricati insieme, se entrano in RAM. Predefinito su CPU: 3.
Environment="OLLAMA_MAX_LOADED_MODELS=1"
# Richieste in coda prima del rifiuto con 503. Predefinito: 512.
Environment="OLLAMA_MAX_QUEUE=32"

Ogni riga ha un motivo. OLLAMA_KEEP_ALIVE decide se il modello resta in RAM fra due richieste: a 5 minuti (il predefinito) la prima domanda dopo una pausa paga il ricaricamento dei pesi dal disco, a -1 la memoria resta occupata anche se nessuno la usa. OLLAMA_NUM_PARALLEL moltiplica la cache come mostra il programma sopra: raddoppiarlo senza rifare i conti è il modo più rapido di finire in swap. OLLAMA_MAX_QUEUE limita la coda: oltre quel limite il server risponde 503, che per l'applicazione è un messaggio chiaro invece di un'attesa infinita. Questo file è uno schema da adattare, non eseguito: i nomi delle variabili vengono dalla FAQ, i valori sono scelte tue.

Proteggere l'endpoint: la porta aperta è il rischio vero

Nella FAQ e nella pagina dell'API locale di Ollama che ho letto non compare nessun meccanismo di autenticazione: chi raggiunge la porta può chiedere al modello qualsiasi cosa e, con le funzioni di gestione, scaricare o cancellare modelli. Non è una dichiarazione sull'intero prodotto, è ciò che dice quella documentazione, ed è sufficiente per una regola pratica: l'endpoint non va mai esposto a Internet senza qualcosa davanti.

La soluzione più semplice è non esporlo affatto: se l'applicazione gira sullo stesso server, chiama 127.0.0.1. Se serve raggiungerlo da un'altra macchina, la documentazione stessa propone un reverse proxy come nginx. Lo schema che segue aggiunge tre cose: una chiave in intestazione verificata da nginx, un limite di richieste e il buffering spento per le risposte a pezzi.

# /etc/nginx/conf.d/ollama.conf  (schema da adattare, non eseguito)
# Serve solo se l'applicazione che chiama il modello sta su un'altra macchina.
# Se sta sullo stesso server, chiama http://127.0.0.1:11434 e questo file non serve.

# Una riga per chiave, fuori dal repository, permessi 640:  "Bearer <chiave>" 1;
map $http_authorization $chiave_valida {
    default 0;
    include /etc/nginx/ollama-chiavi.map;
}

limit_req_zone $binary_remote_addr zone=ollama:10m rate=30r/m;

server {
    listen 443 ssl;
    server_name ai.example.eu;                  # il tuo dominio
    ssl_certificate     /etc/letsencrypt/live/ai.example.eu/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/ai.example.eu/privkey.pem;

    location / {
        if ($chiave_valida = 0) { return 401; }
        limit_req zone=ollama burst=5;
        proxy_pass http://127.0.0.1:11434;
        proxy_buffering off;                     # le risposte arrivano a pezzi
        proxy_read_timeout 300s;                 # una generazione lenta non e' un guasto
    }
}

La mappa legge le chiavi valide da un file separato, così il segreto non finisce nel repository né nel testo della configurazione. limit_req protegge la macchina da un client impazzito, non da un attaccante determinato. proxy_buffering off serve perché una risposta generata token per token deve arrivare al client man mano, e proxy_read_timeout a 300 secondi evita di scambiare una generazione lenta per un guasto. Lo schema non è stato eseguito (nginx non è installato sulla macchina dove ho scritto questo articolo): le direttive vanno controllate con nginx -t sul tuo server, e il certificato TLS lo ottieni con lo strumento che preferisci.

Il firewall completa il quadro: tutto chiuso tranne SSH e HTTPS, e una verifica dall'esterno che la porta 11434 non risponda.

# Schema da adattare, non eseguito. Aprire solo cio' che serve.
sudo ufw default deny incoming
sudo ufw allow 22/tcp
sudo ufw allow 443/tcp
sudo ufw enable
# Dopo: dall'esterno la porta 11434 deve risultare chiusa.
# Da un'altra macchina:  nc -vz <ip-del-server> 11434   -> connessione rifiutata o timeout

La riga finale è la più importante: una porta che «dovrebbe» essere chiusa si controlla con un tentativo di connessione da un'altra rete, non rileggendo la configurazione. Se usi Docker, la sua documentazione avverte che ufw e le regole create da Docker sono incompatibili: una porta pubblicata da un container può aggirare il firewall, quindi anche in quel caso la verifica da fuori è l'unica prova.

Che cosa va storto, in ordine di frequenza

  • Il processo viene terminato dal sistema per memoria: il modello più la cache superano la RAM. Si vede in journalctl -u ollama, il comando indicato dalla guida di diagnosi ufficiale.
  • La prima richiesta dopo una pausa è lentissima: il modello è stato scaricato dalla memoria per OLLAMA_KEEP_ALIVE troppo breve.
  • Le richieste si accumulano e poi falliscono con 503: la coda ha raggiunto OLLAMA_MAX_QUEUE; il carico supera la capacità della macchina.
  • La risposta si interrompe a metà nel browser: il proxy ha il buffering attivo o un timeout troppo corto.
  • L'endpoint risponde a chiunque: manca la chiave o la porta è aperta; è il guasto più grave e più silenzioso.

Gli ultimi due sono errori di configurazione che non producono messaggi: nessun log ti avvisa che un estraneo sta usando la tua CPU. Il primo e il secondo, invece, si vedono subito e si correggono con i numeri dei programmi sopra. L'ordine conta: prima chiudi la porta, poi misuri la memoria, poi ottimizzi la velocità.

Domande frequenti

Una VPS senza GPU basta per un assistente aziendale?

Dipende dal numero di utenti contemporanei. Per poche persone che interrogano documenti interni e accettano risposte in decine di secondi sì; per un servizio con molte richieste simultanee no, perché la velocità è limitata dalla banda di memoria e le richieste si mettono in coda.

Come misuro la velocità reale della mia macchina?

Invia una richiesta a /api/generate senza streaming e dividi eval_count per eval_duration espresso in secondi. Ripeti più volte, a macchina scarica e sotto carico, e riporta un intervallo.

Tenere i dati in Europa basta per il GDPR?

No: la residenza è una parte del quadro. Restano base giuridica, minimizzazione, sicurezza e contratti con i fornitori. L'articolo su privacy by design li passa uno per uno.

Che cosa sai fare adesso

Sai stimare, con le formule dell'architettura, se un modello entra nella macchina che stai guardando e con quanti utenti in parallelo; sai quale tetto di velocità impone la memoria e come misurare la velocità vera; sai configurare Ollama perché resti in RAM senza saturarla e perché non esponga una porta senza protezione. Il passo successivo è decidere che cosa farci girare: il pezzo sul recupero dei documenti costruisce la parte che dà al modello le tue informazioni, mentre il confronto fra famiglie di modelli ti aiuta a sceglierlo. Il tema della capacità sotto carico è ripreso in scaling da 1 a 50 utenti.

Nella stessa serie trovi anche il riepilogo con la scheda di decisione.

Fonti

I due programmi sono stati eseguiti con Python 3.14.4 (python3 -I), due volte, con uscita identica; le ipotesi sono dichiarate nel codice. Configurazioni di systemd, nginx e firewall sono schemi da adattare: non eseguiti, nessun server avviato. Il numero di parametri viene dall'API dei modelli di Hugging Face.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti