SSD Nodes Learn 🎉 VPS da $4.99/mese
Guide Matt ConnorDi Matt Connor · Aggiornato 2026-08-07

Come usare LiteLLM come gateway LLM self-hosted

Configura LiteLLM su un VPS come endpoint compatibile con OpenAI, con chiavi virtuali, budget per chiave, fallback e immagini Docker bloccate alla versione.

Cosa fa un gateway LLM self-hosted

LiteLLM è un gateway LLM open source che ospiti autonomamente: un unico endpoint HTTP che tutte le applicazioni utilizzano e che inoltra ogni richiesta al provider responsabile della risposta. LLM significa large language model, ovvero modello linguistico di grandi dimensioni. Il gateway utilizza l'API OpenAI Chat Completions (application programming interface), quindi qualsiasi libreria client che comunica già con OpenAI può utilizzarlo dopo due modifiche: l'URL di base e la chiave.

Questo livello di indirezione è il punto centrale. Le applicazioni non devono più contenere le credenziali dei provider. Per cambiare modello basta modificare una riga nel file di configurazione sul server, invece di cambiare il codice in cinque servizi. Inoltre, poiché ogni chiamata passa dallo stesso processo, hai un punto in cui impostare un budget e registrare quanto è stato speso.

Dopo l'avvio, hai a disposizione:

  • Un endpoint unico. Le applicazioni utilizzano https://gateway.example.com/v1 e richiedono un nome modello definito da te, ad esempio bulk o strong.
  • Chiavi virtuali. Ogni applicazione riceve una chiave separata, con il proprio elenco di modelli consentiti e il proprio limite di spesa. Puoi revocare una chiave senza modificare le altre.
  • Fallback. Una chiamata non riuscita o un prompt troppo grande viene ritentato automaticamente con un altro modello.
  • Un record nei log. Ogni richiesta scrive una riga con il relativo costo, quindi è possibile sapere quale applicazione ha sostenuto quella spesa.

Perché eseguire autonomamente il gateway

Un router gestito ha la stessa architettura, ma ogni richiesta passa attraverso il processo di un soggetto esterno. Eseguirlo autonomamente consente di mantenere le chiavi del provider e il testo dei prompt su un sistema sotto il proprio controllo. Il costo operativo è reale: ora è necessario gestire il componente da cui dipendono tutte le applicazioni. L’ultima sezione di questa guida tratta proprio questo costo, perché è l’aspetto che la maggior parte delle guide omette.

Requisiti

  • Un VPS (virtual private server) con Ubuntu 24.04, Docker e il plugin Compose installati.
  • Un nome di dominio che punti al VPS, se macchine esterne devono raggiungere il gateway tramite TLS (transport layer security).
  • Almeno una API key del provider.

Il gateway non esegue inferenza. Inoltra le richieste e restituisce le risposte in streaming, quindi il carico della CPU dipende dal volume delle richieste e non dalle dimensioni del modello. Un server con 1 vCPU gestisce senza problemi alcune applicazioni interne. A crescere è il database, perché il gateway registra una riga di spesa per ogni richiesta.

Scrivere prima config.yaml

Il file di configurazione stabilisce quali modelli un client può richiedere. Sono importanti quattro sezioni di primo livello: model_list, litellm_settings, router_settings e general_settings.

model_list:
  - model_name: bulk
    litellm_params:
      model: anthropic/claude-haiku-4-5
      api_key: os.environ/ANTHROPIC_API_KEY
  - model_name: strong
    litellm_params:
      model: anthropic/claude-sonnet-5
      api_key: os.environ/ANTHROPIC_API_KEY
  - model_name: strong
    litellm_params:
      model: openai/gpt-5.5
      api_key: os.environ/OPENAI_API_KEY

litellm_settings:
  num_retries: 2
  request_timeout: 120
  allowed_fails: 3
  cooldown_time: 30
  json_logs: true
  set_verbose: false

router_settings:
  fallbacks: [{"bulk": ["strong"]}]
  context_window_fallbacks: [{"bulk": ["strong"]}]

general_settings:
  background_health_checks: true
  health_check_interval: 300

model_name è il nome inviato dai client. litellm_params.model è il modello reale, scritto come provider/model. Assegnare ai modelli nomi basati sul compito, non sul fornitore. Un'applicazione che richiede bulk continua a funzionare anche se il mese successivo si decide che bulk debba corrispondere a un modello diverso.

api_key: os.environ/ANTHROPIC_API_KEY indica a LiteLLM di leggere quella variabile durante l'esecuzione. La chiave letterale non compare mai nel file. Questo è importante perché config.yaml è il file di cui si esegue il commit.

Due voci hanno intenzionalmente lo stesso nome, strong. Quando più deployment hanno lo stesso model_name, il router li tratta come intercambiabili e prova l'altro se il primo fallisce. In questo modo strong continua a funzionare se uno dei fornitori è temporaneamente indisponibile.

num_retries: 2 ripete la richiesta allo stesso deployment in caso di errore ritentabile. Il fallback viene attivato solo dopo aver esaurito questi tentativi. allowed_fails: 3 insieme a cooldown_time: 30 rimuove un deployment dalla rotazione per 30 secondi dopo 3 errori consecutivi. In questo modo un provider che restituisce errori 500 non viene contattato a ogni richiesta.

fallbacks e context_window_fallbacks hanno trigger diversi. Il secondo è quello utile che spesso viene omesso.

  • fallbacks viene attivato quando la chiamata primaria fallisce.
  • context_window_fallbacks viene attivato quando il provider rifiuta la richiesta perché supera la finestra di contesto del modello. In questo modo un prompt troppo lungo viene inviato a un modello con spazio sufficiente, invece di restituire un errore al chiamante.

È disponibile anche content_policy_fallbacks, per i casi in cui un provider rifiuta la richiesta per motivi di policy sui contenuti. Impostarlo solo se esiste una destinazione appropriata per queste richieste.

Implementare LiteLLM su un VPS con Docker Compose

Creare una directory contenente tre file: config.yaml, docker-compose.yml e .env. La guida rapida upstream usa il tag latest. Impostare invece un tag di release, così docker compose up -d il mese prossimo fornirà lo stesso gateway disponibile oggi e un rollback richiederà una sola riga.

services:
  litellm:
    image: ghcr.io/berriai/litellm:v1.95.0
    restart: unless-stopped
    command: ["--config", "/app/config.yaml", "--num_workers", "1"]
    ports:
      - "127.0.0.1:4000:4000"
    volumes:
      - ./config.yaml:/app/config.yaml:ro
    env_file: .env
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16
    restart: unless-stopped
    environment:
      POSTGRES_USER: litellm
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
      POSTGRES_DB: litellm
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U litellm"]
      interval: 5s
      timeout: 5s
      retries: 10
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

Compose legge .env due volte in questo caso. La prima volta per sostituire ${POSTGRES_PASSWORD} direttamente nel file Compose, la seconda tramite env_file per passare ogni variabile al container.

v1.95.0 era la release corrente nell'agosto 2026. Controllare la pagina delle release del progetto e impostare il tag della versione corrente al momento del deployment. Ogni release pubblica una firma, quindi è possibile verificare l'immagine prima di considerarla attendibile:

cosign verify --key https://raw.githubusercontent.com/BerriAI/litellm/v1.95.0/cosign.pub ghcr.io/berriai/litellm:v1.95.0

La riga della porta è 127.0.0.1:4000:4000 e pubblica la porta soltanto sull'interfaccia di loopback. Scrivere invece 4000:4000 rende il gateway raggiungibile da tutta Internet, perché Docker aggiunge regole proprie nella catena FORWARD di iptables, che vengono valutate prima di quelle di ufw; di conseguenza, ufw deny 4000 non lo blocca. Questo è il modo più comune in cui un gateway self-hosted finisce per essere esposto: vedere come Docker pubblica direttamente una porta del container oltre ufw. Il traffico dall'esterno arriva invece tramite il reverse proxy.

Mantieni le chiavi dei provider fuori dall’immagine

Il file .env contiene tutti i secret. Viene passato come variabile d’ambiente in fase di esecuzione, quindi non viene mai incluso nell’immagine e non viene mai sottoposto a commit.

LITELLM_MASTER_KEY=sk-REPLACE_ME
LITELLM_SALT_KEY=sk-REPLACE_ME_TOO
POSTGRES_PASSWORD=REPLACE_ME_AS_WELL
DATABASE_URL=postgresql://litellm:REPLACE_ME_AS_WELL@db:5432/litellm
STORE_MODEL_IN_DB=True
LITELLM_MODE=PRODUCTION
LITELLM_LOG=ERROR
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-proj-...

Genera le due chiavi LiteLLM usando casualità reale, quindi limita i permessi del file:

printf 'sk-%s\n' "$(openssl rand -hex 32)"
chmod 600 .env

LITELLM_MASTER_KEY è la credenziale amministrativa. Autentica l’API di gestione ed è la password per la Admin UI all’indirizzo /ui. Nessuna applicazione deve mai conservarla.

LITELLM_SALT_KEY cifra le credenziali dei provider archiviate nel database. Impostala una volta e non modificarla. Se la cambi in seguito, le credenziali già archiviate non possono essere decifrate. Il gateway si avvia normalmente, ma ogni chiamata a quei provider fallisce per un errore di autenticazione.

STORE_MODEL_IN_DB=True consente di aggiungere e modificare i modelli dalla Admin UI senza modificare config.yaml. È comodo, ma divide la fonte autorevole in due. Decidi quale deve essere la fonte autorevole e annota la decisione accanto alla configurazione.

Il motivo per cui le chiavi devono restare fuori dal file di configurazione è lo stesso per cui devono restare fuori dagli strumenti che fornisci a un agente. Mantieni i secret dei provider fuori dagli agenti AI descrive questo approccio, mentre file env e secret in Docker Compose ne illustra gli aspetti operativi.

Avvia il servizio e monitora il primo avvio:

docker compose up -d
docker compose logs -f litellm

Verificare che il servizio funzioni effettivamente

Sono disponibili due probe senza autenticazione e uno autenticato. Possono fallire per motivi diversi.

curl -s http://127.0.0.1:4000/health/liveliness
curl -s http://127.0.0.1:4000/health/readiness

/health/liveliness non richiede autenticazione e risponde "I'm alive!" quando il processo è in esecuzione. Anche /health/readiness non richiede autenticazione. Restituisce un oggetto JSON con "status": "healthy" e un campo db, oppure 503 quando il database non è raggiungibile. Configura il monitoraggio sulla readiness, perché la liveliness può rimanere verde su un gateway che non riesce a cercare nemmeno una chiave virtuale.

Il controllo autenticato è quello che comunica con i provider:

curl -s http://127.0.0.1:4000/health \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY"

Risponde con gli array healthy_endpoints e unhealthy_endpoints. Un modello presente in unhealthy_endpoints con un errore di autenticazione indica che la chiave del provider in .env è errata o mancante. È esattamente il problema da individuare in questa fase. Poiché è impostato background_health_checks: true, il proxy esegue automaticamente queste probe ogni health_check_interval secondi e /health restituisce l'ultimo risultato. Il polling non invia quindi ogni volta una richiesta di test ai provider.

Chiavi virtuali e budget per chiave

Ogni applicazione riceve una chiave propria, generata utilizzando la chiave master.

curl -s http://127.0.0.1:4000/key/generate \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "key_alias": "nightly-summariser",
    "models": ["bulk"],
    "max_budget": 5,
    "budget_duration": "30d",
    "rpm_limit": 60,
    "tpm_limit": 200000
  }'

La risposta contiene un campo key che inizia con sk-. Questa stringa viene fornita all'applicazione ed è l'unico dato che l'applicazione riceve.

  • models è un'allowlist delle richieste che questa chiave può effettuare. La chiave precedente può richiedere bulk e nient'altro.
  • max_budget: 5 con budget_duration: "30d" è pari a cinque dollari statunitensi ogni 30 giorni consecutivi; dopodiché la chiave smette di funzionare.
  • rpm_limit e tpm_limit limitano rispettivamente le richieste al minuto e i token al minuto per questa sola chiave.
  • key_alias è il valore che riconoscerai nel log della spesa sei settimane dopo. Impostalo sempre.

Quando il budget è esaurito, la chiamata restituisce HTTP 401 con un corpo di questo formato:

ExceededBudget: Current spend for token: 7.2e-05; Max Budget for Token: 2e-07

Il codice di stato è ciò che crea confusione. Una libreria client interpreta 401 come un problema di autenticazione, quindi lo sviluppatore che legge lo stack trace inizia a verificare se la chiave è valida. Registra il corpo della risposta insieme al codice di stato; altrimenti l'esaurimento del budget sembrerà ogni volta una credenziale non valida.

Ispeziona e modifica le chiavi tramite la stessa API di gestione:

curl -s "http://127.0.0.1:4000/key/info?key=sk-..." \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY"

curl -s -X POST http://127.0.0.1:4000/key/update \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"key": "sk-...", "max_budget": 25}'

Un budget applicato al gateway resta efficace anche quando il problema è causato dall'agent stesso. Per questo costituisce la base del controllo dei costi per agent AI su un VPS.

Inviare elaborazioni in batch a un modello economico

Indicare al client il gateway. URL di base, chiave e nome del modello:

curl -s http://127.0.0.1:4000/v1/chat/completions \
  -H "Authorization: Bearer sk-<the virtual key>" \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "bulk",
    "messages": [{"role": "user", "content": "Say hello in five words."}]
  }'

Qualsiasi libreria client OpenAI funziona allo stesso modo: impostare base_url su https://gateway.example.com/v1 e api_key sulla chiave virtuale.

La policy di routing definita in config.yaml viene ora applicata senza che il chiamante debba conoscerla. Una richiesta per bulk viene inviata al modello economico. Se la chiamata non riesce dopo i tentativi configurati, la richiesta viene ripetuta usando strong. Se il prompt è troppo lungo per bulk, context_window_fallbacks lo invia a strong invece di restituire un errore. Le elaborazioni in batch, come una classificazione completa o il riepilogo di un arretrato, usano per impostazione predefinita il modello economico; solo le richieste più complesse hanno un costo maggiore.

È anche in questo caso che un gateway dimostra la propria utilità con gli agenti che usano strumenti. Un server MCP (model context protocol) sullo stesso VPS e l’agente che lo utilizza possono puntare entrambi a un unico endpoint. In questo modo è possibile cambiare il modello sottostante senza ridistribuire nessuno dei due.

Come sapere se si è verificato un fallback?

Questa è la modalità di errore più costosa, perché nulla sembra non funzionare. Un fallback completato correttamente restituisce HTTP 200 con un normale corpo della risposta. Il modello economico può essere indisponibile per un giorno, mentre ogni chiamata viene gestita in silenzio da quello più costoso. La prima evidenza può essere la fattura.

L'evidenza è presente negli header della risposta. Richiedili:

curl -s -D - -o /dev/null http://127.0.0.1:4000/v1/chat/completions \
  -H "Authorization: Bearer sk-<the virtual key>" \
  -H 'Content-Type: application/json' \
  -d '{"model":"bulk","messages":[{"role":"user","content":"ping"}]}' \
  | grep -i '^x-litellm'
  • x-litellm-model-group indica ciò che il client ha richiesto. x-litellm-model-id indica il deployment che ha risposto. Se questi due valori non coincidono, si è verificato un fallback.
  • x-litellm-attempted-fallbacks e x-litellm-attempted-retries contano i fallback. In una chiamata corretta, entrambi valgono 0.
  • x-litellm-response-cost indica il costo di quella singola chiamata in dollari statunitensi.
  • x-litellm-call-id è l'identificatore da usare per trovare la stessa chiamata nei log.

Registra x-litellm-attempted-fallbacks per ogni richiesta e genera un alert quando smette di essere 0. Questo singolo valore distingue una policy di routing funzionante da una policy che è diventata silenziosamente "usa sempre il modello più costoso".

La versione completa di questo approccio è il tracing, che richiede una configurazione dedicata: Langfuse self-hosted per il tracing delle chiamate degli agenti. LiteLLM include il callback, quindi per collegarlo servono due righe e le credenziali.

litellm_settings:
  success_callback: ["langfuse"]
  failure_callback: ["langfuse"]
LANGFUSE_PUBLIC_KEY=pk-lf-...
LANGFUSE_SECRET_KEY=sk-lf-...
LANGFUSE_HOST=https://langfuse.example.com

Imposta sia failure_callback sia success_callback. Se li ometti, conserverai soltanto le trace delle richieste in cui non si è verificato alcun problema. Indipendentemente da tutto questo, LiteLLM scrive una riga di spesa per ogni richiesta in Postgres e l'Admin UI all'indirizzo /ui legge quella tabella. La tabella cresce con il traffico, quindi controllala se il disco è piccolo.

Metti il gateway dietro un reverse proxy

Niente al di fuori del server deve poter raggiungere la porta 4000. Termina TLS in nginx o Caddy e inoltra le richieste all'indirizzo di loopback.

location / {
    proxy_pass http://127.0.0.1:4000;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_buffering off;
    proxy_read_timeout 600s;
}

Queste sono due delle direttive che spesso vengono omesse. proxy_buffering off è importante perché una risposta in streaming è una sequenza di eventi inviati dal server e, con il buffering attivo, nginx conserva i blocchi finché la risposta non termina. Il client resta quindi senza dati e poi riceve tutto in una volta. proxy_read_timeout 600s è importante perché una generazione lunga supera il timeout predefinito di nginx, pari a 60 secondi. In questo caso il client riceve un errore 504 e il log degli errori registra upstream timed out (110: Connection timed out) while reading response header from upstream.

Per il certificato, Certbot con Let's Encrypt su nginx è la procedura più rapida. Se il server pubblica già diversi container, Traefik davanti a più applicazioni Compose gestisce routing e certificati in un unico punto.

Il gateway è ora un singolo punto di errore

Siate consapevoli di ciò che avete configurato. Ora ogni applicazione che gestite dipende da un container su un solo VPS. Quando il container è inattivo, nessun componente può chiamare alcun modello, inclusi i provider che funzionano correttamente. Ne derivano quattro conseguenze.

  • Una configurazione errata blocca tutto contemporaneamente. restart: unless-stopped riavvia un processo in caso di crash e continua a riavviare un container che non riesce a fare il parsing di config.yaml. Dopo ogni modifica alla configurazione, leggete docker compose logs litellm e apportate queste modifiche solo quando avete tempo di monitorarne l'effetto.
  • Postgres si trova nel percorso delle richieste. La ricerca delle chiavi virtuali e la registrazione della spesa utilizzano entrambe il database. Se /health/readiness restituisce 503, il gateway è in esecuzione ma non può svolgere nessuna delle due operazioni.
  • Aumentate il numero di istanze, non le dimensioni di una singola istanza. La documentazione del progetto raccomanda un worker per istanza (--num_workers 1) e più istanze che condividono un unico database. Due gateway di piccole dimensioni dietro un load balancer eliminano il container singolo dalla configurazione. Non eliminano il database.
  • Eseguite il backup di ciò che non potete rigenerare. Si tratta di config.yaml e .env, insieme a un pg_dump del database. Se perdete LITELLM_SALT_KEY, le credenziali dei provider cifrate contenute in quel dump diventano inutilizzabili. Per questo il file env e il dump devono appartenere allo stesso job di backup: backup restic su storage esterno.

Per eseguire un aggiornamento, modificate il tag dell'immagine ed eseguite docker compose up -d. Per impostazione predefinita, LiteLLM esegue prisma migrate deploy all'avvio. Il nuovo container aggiorna quindi lo schema del database al primo avvio. Eseguite il dump prima di modificare il tag, perché ripristinare l'immagine precedente non annulla una migrazione già eseguita.

FAQ

LiteLLM aggiunge una latenza rilevabile a ogni chiamata?

Il progetto dichiara 8 ms al 95° percentile con 1000 richieste al secondo, come indicato nel README ad agosto 2026. Consideralo un dato del fornitore. Il fattore che incide realmente sulla latenza è la distanza di rete tra le applicazioni e il gateway, perché ogni chiamata richiede un round trip aggiuntivo. Esegui il gateway nella stessa regione delle applicazioni che lo utilizzano, quindi misura il tuo overhead con l'header x-litellm-overhead-duration-ms in una risposta reale.

Perché lo streaming ha smesso di funzionare dopo aver messo nginx davanti al servizio?

Perché nginx memorizza nel buffer le risposte upstream per impostazione predefinita, mentre una risposta in streaming è una sequenza di server-sent events. Con proxy_buffering attivo, nginx raccoglie i blocchi e li invia soltanto al termine della risposta. Il client resta quindi in attesa senza ricevere dati, poi riceve l'intera risposta in una volta sola. Imposta proxy_buffering off; nel blocco location. Aumenta proxy_read_timeout nello stesso blocco, perché una generazione lunga altrimenti supera il valore predefinito di 60 secondi di nginx e il client riceve un 504.

Cosa succede quando una virtual key esaurisce il budget?

La chiamata fallisce con HTTP 401 e un body del tipo ExceededBudget: Current spend for token: 7.2e-05; Max Budget for Token: 2e-07. Il codice 401 è fuorviante: una libreria client lo segnala come errore di autenticazione, quindi si inizia a verificare se la key è valida invece di leggere il messaggio. Registra il body della risposta insieme al codice di stato. Verifica la posizione effettiva della key con /key/info?key=sk-... usando la master key, quindi aumenta il limite con /key/update se il budget era troppo basso.

Il gateway può instradare le richieste anche verso un modello locale, oltre che verso modelli hosted?

Sì. Il modello locale è un'ulteriore voce in model_list. Usa il prefisso ollama_chat/ con un api_base, ad esempio model: ollama_chat/llama3.1 insieme a api_base: http://ollama:11434. All'interno di un container, localhost indica quel container. Usa quindi il nome del servizio Compose o l'indirizzo dell'host sulla rete Docker, mai 127.0.0.1. Avviare il modello locale è un'attività separata: consulta self-hosting di un LLM con Ollama su un VPS.