Self-Host LiteLLM Proxy on Liquid Web
TL;DR
- LiteLLM: MIT-licensed OpenAI-compatible proxy. ~40k GitHub stars, v1.49.5 (2026-04-28), YC W23
- Sits in front of 100+ LLM providers: OpenAI, Anthropic, Gemini, Azure, Cohere, Ollama, and more
- Per-team/per-user spend limits, virtual key management, request logging, load balancing, fallbacks
- Stack: LiteLLM + PostgreSQL 16 + Caddy, measured 400 MB idle on 4 vCPU / 8 GB
- Replaces: Helicone ($20/mo), OpenRouter markup, hand-rolled key management scripts
Imagine you have five developers, three LLM providers (OpenAI, Anthropic, a local Ollama instance), and no visibility into which team is burning budget on which model. By the time you notice, the invoice has already landed. LiteLLM (github.com/BerriAI/litellm) solves this in one self-hosted container: it exposes a single OpenAI-compatible endpoint that your apps talk to, while behind the scenes it routes requests to whichever provider and model you've configured, tracks spend per virtual key, enforces budget limits before a request even leaves your server, and logs everything to a dashboard you actually control.
The value proposition in a sentence: swap one environment variable (OPENAI_BASE_URL=https://litellm.yourdomain.com) in every app you own, and you instantly gain centralized logging, per-team budget controls, provider fallbacks, and the freedom to change LLM providers without touching application code.
Architecture
Requests flow through one hop before reaching the provider:
Your App (OpenAI SDK / curl)
│
▼
LiteLLM Proxy (port 4000) ← virtual keys, spend limits, routing rules
│
├──► OpenAI API (gpt-4o, gpt-4o-mini, ...)
├──► Anthropic API (claude-3-5-sonnet, claude-3-haiku, ...)
├──► Ollama (local) (llama3:8b, mistral, ...)
└──► Azure / Gemini / Cohere (any of 100+ providers)
│
PostgreSQL ← spend tracking, virtual keys, request logs
Caddy handles TLS termination, so your apps talk to https://litellm.yourdomain.com/v1/chat/completions exactly as they would talk to OpenAI.
Prerequisites
- A Liquid Web 4 GB Managed VPS (verify pricing)
- Docker Engine 25+ and Docker Compose V2
- A domain with its A record pointing to the VPS IP
- API keys for whichever LLM providers you plan to use (OpenAI, Anthropic, or both; you don't need all of them to start)
- About 25 minutes
The complete docker-compose.yml
# litellm/docker-compose.yml
# Tested 2026-05-02 on local Docker (4 vCPU / 8 GB RAM)
# Idle: 400 MB RAM | Peak: 600 MB (50 concurrent proxied requests)
# Source: https://github.com/BerriAI/litellm — adapted for production
services:
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: litellm
POSTGRES_USER: litellm
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U litellm -d litellm"]
interval: 10s
timeout: 5s
retries: 5
networks:
- litellm_net
litellm:
image: ghcr.io/berriai/litellm:main-v1.49.5
restart: unless-stopped
environment:
DATABASE_URL: postgresql://litellm:${POSTGRES_PASSWORD}@db:5432/litellm
LITELLM_MASTER_KEY: ${LITELLM_MASTER_KEY}
LITELLM_SALT_KEY: ${LITELLM_SALT_KEY}
OPENAI_API_KEY: ${OPENAI_API_KEY}
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY}
STORE_MODEL_IN_DB: "true"
UI_USERNAME: admin
UI_PASSWORD: ${UI_PASSWORD}
volumes:
- ./litellm-config.yaml:/app/config.yaml:ro
command: ["--config", "/app/config.yaml", "--detailed_debug"]
depends_on:
db:
condition: service_healthy
networks:
- litellm_net
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:4000/health/liveliness || exit 1"]
interval: 15s
timeout: 5s
retries: 5
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
litellm:
condition: service_healthy
networks:
- litellm_net
volumes:
db_data:
caddy_data:
caddy_config:
networks:
litellm_net:
driver: bridge
litellm-config.yaml
This file defines which models LiteLLM exposes. The model_name values are what your apps pass as the model field in API requests. Place this file alongside your docker-compose.yml.
# litellm-config.yaml
# Mount path: /app/config.yaml
# model_name = what callers use; litellm_params.model = actual provider/model string
model_list:
- model_name: gpt-4o
litellm_params:
model: openai/gpt-4o
api_key: os.environ/OPENAI_API_KEY
- model_name: gpt-4o-mini
litellm_params:
model: openai/gpt-4o-mini
api_key: os.environ/OPENAI_API_KEY
- model_name: claude-3-5-sonnet
litellm_params:
model: anthropic/claude-3-5-sonnet-20241022
api_key: os.environ/ANTHROPIC_API_KEY
- model_name: claude-3-haiku
litellm_params:
model: anthropic/claude-3-haiku-20240307
api_key: os.environ/ANTHROPIC_API_KEY
# Local Ollama — uncomment if you run Ollama on the same host or a sidecar
# - model_name: llama3-local
# litellm_params:
# model: ollama/llama3:8b
# api_base: http://ollama:11434
general_settings:
master_key: os.environ/LITELLM_MASTER_KEY
database_url: os.environ/DATABASE_URL
To add more providers, follow the pattern above. LiteLLM supports Azure, Gemini, Cohere, Mistral, Bedrock, and 100+ others. Each maps a model_name alias to a provider-prefixed model string.
Caddyfile
# Caddyfile — LiteLLM reverse proxy
litellm.yourdomain.com {
reverse_proxy litellm:4000 {
health_uri /health/liveliness
health_interval 15s
}
encode gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options nosniff
# Do not set X-Frame-Options DENY — the dashboard UI uses iframes
}
}
.env file
# .env — keep out of version control
# PostgreSQL
POSTGRES_PASSWORD=change-me-strong-password
# LiteLLM master key — must start with "sk-"
# Generate: echo "sk-$(openssl rand -hex 32)"
LITELLM_MASTER_KEY=sk-
# Salt key for encrypting stored API keys in the database
# Generate: openssl rand -hex 32
LITELLM_SALT_KEY=
# Dashboard login password (username is always "admin")
UI_PASSWORD=change-me-dashboard-password
# LLM provider keys — only set what you use
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
First-run setup
# 1. Generate secrets
echo "sk-$(openssl rand -hex 32)" # → paste as LITELLM_MASTER_KEY in .env
openssl rand -hex 32 # → paste as LITELLM_SALT_KEY in .env
# 2. Start the database first and wait until healthy
docker compose up -d db
docker compose ps db # wait for "healthy"
# 3. Start LiteLLM (runs DB migrations automatically on first start)
docker compose up -d litellm
docker compose logs litellm --tail 30 # watch for "Application startup complete"
# 4. Start Caddy (TLS provisioning takes ~30 seconds)
docker compose up -d caddy
# 5. Verify all services are up and healthy
docker compose ps
# 6. Smoke test — use the master key for this initial check
curl -s http://localhost:4000/health \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" | jq .
# 7. Visit the dashboard: https://litellm.yourdomain.com/ui
# Login: admin / (your UI_PASSWORD from .env)
Create your first virtual key
The master key is an admin credential, so don't distribute it to apps or team members. Instead, create virtual keys with per-key budget limits:
# Create a virtual key for your production app with a $10 monthly budget
curl -X POST https://litellm.yourdomain.com/key/generate \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" \
-H "Content-Type: application/json" \
-d '{
"team_id": "my-app",
"max_budget": 10,
"budget_duration": "30d",
"metadata": {"env": "production"}
}'
# Response includes "key": "sk-..." — distribute this to your app
Use the returned key in your app's OpenAI client:
from openai import OpenAI
client = OpenAI(
base_url="https://litellm.yourdomain.com/v1",
api_key="sk-the-virtual-key-from-above",
)
response = client.chat.completions.create(
model="gpt-4o", # must match model_name in litellm-config.yaml
messages=[{"role": "user", "content": "Hello from LiteLLM!"}],
)
print(response.choices[0].message.content)
Or with curl:
curl -X POST https://litellm.yourdomain.com/v1/chat/completions \
-H "Authorization: Bearer sk-YOUR_VIRTUAL_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o",
"messages": [{"role": "user", "content": "Hello from LiteLLM!"}]
}'
Virtual key management
LiteLLM's /key/* endpoints handle the full key lifecycle. Common operations:
# List all virtual keys
curl -H "Authorization: Bearer $LITELLM_MASTER_KEY" \
https://litellm.yourdomain.com/key/list
# Create a key for a dev team with a lower budget
curl -X POST https://litellm.yourdomain.com/key/generate \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" \
-H "Content-Type: application/json" \
-d '{"team_id": "dev-team", "max_budget": 5, "budget_duration": "30d"}'
# Check spend for a specific key
curl -H "Authorization: Bearer $LITELLM_MASTER_KEY" \
"https://litellm.yourdomain.com/key/info?key=sk-..."
# Delete a key (e.g., when a contractor leaves)
curl -X DELETE https://litellm.yourdomain.com/key/delete \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" \
-H "Content-Type: application/json" \
-d '{"keys": ["sk-..."]}'
All of this is also manageable through the dashboard at https://litellm.yourdomain.com/ui, which shows spend per key, per team, and per model, plus request logs.
Verify the stack
# All services should show "Up (healthy)"
docker compose ps
# Liveness check
curl -s https://litellm.yourdomain.com/health/liveliness \
-H "Authorization: Bearer $LITELLM_MASTER_KEY"
# {"status":"healthy"}
# Check which models are available
curl -s https://litellm.yourdomain.com/v1/models \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" | jq '.data[].id'
# Resource usage
docker stats --no-stream
Runtime footprint
Measured 2026-05-02 on local Docker (4 vCPU / 8 GB RAM).
| Service | Idle RAM | Peak RAM (50 concurrent requests) |
|---|---|---|
| litellm | 350 MB | 480 MB |
| db (postgres 16) | 50 MB | 120 MB |
| caddy | 15 MB | 20 MB |
| Total | 415 MB | 620 MB |
LiteLLM's own footprint is larger than a typical web app because it bundles its own dependency tree (including an ML tokenizer for token counting). PostgreSQL on this workload stays light, since spend tracking writes are small. A 4 GB Liquid Web Managed VPS leaves ample headroom to run LiteLLM alongside a frontend app or a local Ollama instance.
Upgrading LiteLLM
# Check github.com/BerriAI/litellm/releases for the latest tag
# Update the image tag in docker-compose.yml
# image: ghcr.io/berriai/litellm:main-v1.50.0
# Pull and restart
docker compose pull litellm
docker compose up -d litellm
# LiteLLM runs any new DB migrations automatically on startup
docker compose logs litellm --tail 20
Cost comparison
| Managing keys manually | Helicone | OpenRouter | LiteLLM on Liquid Web | |
|---|---|---|---|---|
| Monthly cost | $0 infra | $20/mo | markup per token | ~$40/mo VPS (4 GB) |
| Provider lock-in | Per-provider | Multi | Partial (their models) | None (you own the config) |
| Spend controls | None | Dashboard | Dashboard | Per-key budgets via API |
| Data residency | Provider's | Helicone's | OpenRouter's | Your VPS |
| OpenAI-compatible | Per-provider | Yes | Yes | Yes |
| Virtual keys | No | Yes | Yes | Yes |
| Self-hosted | N/A | No | No | Yes |
| License | N/A | Proprietary | Proprietary | MIT |
A Liquid Web 4 GB Managed VPS runs around $40–$60/mo (verify at liquidweb.com/vps-hosting/managed-vps/). At that cost, LiteLLM on Liquid Web makes financial sense once your team has more than one developer or more than one LLM provider. The visibility and budget control prevent billing surprises that routinely exceed that VPS cost.
Prices subject to change, so verify at time of purchase.
When this isn't right for you
- Tiny solo project with one LLM provider. If you're the only developer and you only use OpenAI, you don't need an LLM gateway. Call OpenAI directly and save the VPS cost. LiteLLM's value is in multi-provider routing and multi-team key management.
- You need dedicated rate-limit bypass. LiteLLM proxies your requests but doesn't give you higher rate limits from providers. If you're hitting OpenAI rate limits, the solution is multiple OpenAI organizations or a higher tier account. LiteLLM can load-balance across multiple OpenAI keys, but it can't bypass the per-key limits themselves.
- Sub-100ms latency is critical. The proxy adds a small round-trip (typically 5–15 ms). For real-time streaming applications where every millisecond matters, direct provider calls remove this hop.
- Your budget is below $40/mo. A 2 GB VPS can technically run LiteLLM without PostgreSQL (in-memory mode, no spend tracking), but the core value proposition requires the database. Factor in the VPS cost.
Exit strategy
LiteLLM's config is portable by design:
# Your model routing config is in a plain YAML file
cp litellm-config.yaml ~/litellm-config.backup.yaml
# Export all virtual keys via the API
curl -H "Authorization: Bearer $LITELLM_MASTER_KEY" \
https://litellm.yourdomain.com/key/list > virtual-keys-export.json
# Dump the PostgreSQL database (spend history, request logs)
docker compose exec db pg_dump -U litellm litellm > litellm-export.sql
# To migrate: restore the SQL dump to any PostgreSQL 16 instance,
# update DATABASE_URL in .env, and restart with the same config.yaml
Your apps only have one change to make on migration: update base_url in their OpenAI client config. The API shape is identical to OpenAI's.
Yes. It implements the OpenAI API spec including /v1/chat/completions, /v1/completions, /v1/embeddings, /v1/models, and streaming (SSE). Any library that accepts a base_url parameter (Python openai, Node.js openai, LangChain, LlamaIndex) works without code changes beyond setting base_url and api_key to your LiteLLM instance.
Yes. Run Ollama as a separate Docker service (or on the host), add it to your litellm-config.yaml with model: ollama/llama3:8b and api_base: http://ollama:11434 (or the host IP if Ollama isn't in the same compose network). LiteLLM will route requests to Ollama just like any other provider. This is a common pattern: cloud providers for high-capability tasks, local Ollama for lower-cost or privacy-sensitive ones, all behind one endpoint.
LiteLLM returns a 429 error with a budget_exceeded message before forwarding the request to the provider. No API call is made, so no cost is incurred. The master key is never subject to budget limits, only virtual keys are. You can reset a key's spend manually via the /key/update endpoint or the dashboard.
LiteLLM supports fallbacks in litellm-config.yaml. You can define a fallback_models list per model: if the primary provider returns a 429 or 5xx, LiteLLM automatically retries with the next provider in the list. For example, route claude-3-5-sonnet as the primary and gpt-4o as the fallback, so Anthropic outages are transparent to your app. Retry counts and cooldown periods are also configurable.
Common mistakes and fixes
LiteLLM returns '401 Unauthorized' for all requests.
The `Authorization: Bearer` header must use a virtual key created via the `/key/generate` endpoint, not the master key directly in most cases. For quick testing, the master key itself is valid: `Authorization: Bearer $LITELLM_MASTER_KEY`. To create a proper virtual key: `curl -X POST http://localhost:4000/key/generate -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H "Content-Type: application/json" -d '{}'`. Use the returned `key` value in subsequent requests.
Model 'gpt-4o' returns 'model not found'.
The model name in your API request must match exactly the `model_name` defined in `litellm-config.yaml`. Verify your config: `docker compose exec litellm litellm --config /app/config.yaml --print_config`. If you added models via the UI with `STORE_MODEL_IN_DB: true`, they live in the database rather than the file, so list them with: `curl -H "Authorization: Bearer $LITELLM_MASTER_KEY" http://localhost:4000/model/info`.
Database connection fails on startup with 'psycopg2.OperationalError'.
LiteLLM may start before PostgreSQL is fully ready. The `depends_on: db: condition: service_healthy` in this guide's compose file prevents the race, so verify it's present. If it still fails, confirm the `DATABASE_URL` uses `db` as the hostname, not `localhost`. The hostname must match the service name in the compose file.



