This guide explains how to:
- Run LiteLLM using Docker
- Configure multiple API keys
- Add / update / rotate keys safely
This setup is ideal for centralizing multiple AI API keys behind a single endpoint.
Create a simple folder:
litellm/
βββ docker-compose.yml
βββ config.yaml
βββ .env β your actual secrets (never commit this)
βββ .env.example β template to share with others
βββ README.md
All secrets are stored in a .env file. Docker Compose automatically reads this file.
Copy the example file and fill in your values:
cp .env.example .envThen edit .env:
LITELLM_MASTER_KEY=your-master-key-here
POSTGRES_USER=litellm
POSTGRES_PASSWORD=your-postgres-password
POSTGRES_DB=litellm
DATABASE_URL=postgresql://litellm:your-postgres-password@db:5432/litellm
UI_USERNAME=your-ui-username
UI_PASSWORD=your-ui-passwordGenerate a secure master key with:
openssl rand -base64 32
Important: .env is listed in .gitignore β never commit it. Only commit .env.example.
version: "3.9"
services:
litellm:
image: ghcr.io/berriai/litellm:v1.40.14
container_name: litellm
ports:
- "4000:4000"
volumes:
- ./config.yaml:/app/config.yaml
command: ["--config", "/app/config.yaml"]
restart: always
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:4000/health"]
interval: 30s
timeout: 10s
retries: 3model_list:
- model_name: gpt-4o
litellm_params:
model: openai/gpt-4o
api_key: sk-key-1
- model_name: gpt-4o-backup
litellm_params:
model: openai/gpt-4o
api_key: sk-key-2
router_settings:
routing_strategy: simple-shufflemodel_name: alias used by your appsapi_key: your actual provider keyrouting_strategy: distributes load across keys
docker compose up -dCheck logs:
docker logs -f litellmCheck health:
curl http://localhost:4000/healthLiteLLM uses OpenAI-compatible format:
curl http://localhost:4000/v1/chat/completions \
-H "Authorization: Bearer anything" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o",
"messages": [
{"role": "user", "content": "Hello"}
]
}'Edit config.yaml:
- model_name: gpt-4o-key3
litellm_params:
model: openai/gpt-4o
api_key: sk-new-key-3Then reload:
docker compose restart litellmInstead of replacing directly:
- Add new key
- Keep old key temporarily
- Restart service
- Remove old key later
Delete the entry from config.yaml, then:
docker compose restart litellmrouter_settings:
routing_strategy: simple-shuffleOptions:
simple-shuffleβ random distributionleast-busyβ better for production
router_settings:
fallbacks:
- gpt-4o-backupIf primary fails β fallback used automatically.
model_list:
- model_name: client-a
litellm_params:
model: openai/gpt-4o
api_key: sk-client-a
- model_name: client-b
litellm_params:
model: openai/gpt-4o
api_key: sk-client-bThen call:
"model": "client-a"- Store all secrets in
.env(never hardcode indocker-compose.ymlorconfig.yaml) - Add
.envto.gitignoreand only commit.env.example - Restrict access via reverse proxy (Traefik/Nginx)
- Use internal network only (no public exposure)
- Commit
.envorconfig.yamlwith real keys to Git - Use
:mainDocker tag in production - Expose port 4000 publicly without auth
docker compose pull
docker compose up -dYour Apps (n8n / OpenClaw / Backend)
β
LiteLLM
β
Multiple API Keys
- Use Docker for easy deployment
- Store all secrets in
.env(copy from.env.example) - Manage all API keys in
config.yaml - Restart service after changes
- Use routing + fallback for reliability
This stack includes CLIProxyAPI as a sidecar service that wraps your Claude Code subscription (Max/Pro plan) and exposes it as an Anthropic-compatible API. LiteLLM routes claude-sonnet, claude-opus, and claude-haiku model names to it automatically.
Your Apps β LiteLLM:4000 β CLIProxyAPI:8317 (internal) β Claude Code OAuth session
OAuth requires a browser callback. Run this before starting the stack. It stores tokens in a named Docker volume that persists across restarts.
Volume naming is critical. The
docker-compose.ymlpins the project name tolitellm(via the top-levelname:field), so the auth volume is alwayslitellm_cliproxy_authβ regardless of your host folder name. If you removed thatname:field, the volume would instead be<folder>_cliproxy_auth(e.g.ai-proxy_cliproxy_auth), and the login command below would write tokens into the wrong volume. Symptom:docker logs cli-proxy-apishows0 auth filesand/v1/modelsreturns an empty list.
# Run the login container β exposes the OAuth callback port temporarily
docker run -it --rm \
-v litellm_cliproxy_auth:/root/.cli-proxy-api \
-p 54545:54545 \
--workdir /CLIProxyAPI \
--entrypoint sh \
eceasy/cli-proxy-api:latest \
-c "cp config.example.yaml config.yaml && ./CLIProxyAPI -claude-login"Verify the volume matches what Compose uses:
docker volume ls | grep cliproxy_auth # should list litellm_cliproxy_authNote: The binary is located at
/CLIProxyAPI/CLIProxyAPIinside the image (not in$PATH). The--workdirand--entrypoint shflags are required. Thecp config.example.yaml config.yamlstep is needed because noconfig.yamlexists by default.
The command prints an auth URL. Open it in your browser and complete the Claude Code sign-in. The container exits automatically once done.
VPS tip: If port 54545 is firewalled, use SSH port forwarding from your local machine:
ssh -L 54545:localhost:54545 user@your-vpsThen open
http://localhost:54545/...in your local browser.
docker compose up -dCLIProxyAPI picks up the saved tokens from the volume automatically.
Call Claude models through LiteLLM exactly like any other model:
curl http://localhost:4000/v1/chat/completions \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet",
"messages": [{"role": "user", "content": "Hello"}]
}'Available model names: claude-sonnet, claude-opus, claude-haiku
docker compose stop cli-proxy-api
docker run -it --rm \
-v litellm_cliproxy_auth:/root/.cli-proxy-api \
-p 54545:54545 \
--workdir /CLIProxyAPI \
--entrypoint sh \
eceasy/cli-proxy-api:latest \
-c "cp config.example.yaml config.yaml && ./CLIProxyAPI -claude-login"
docker compose start cli-proxy-apiRun the login command again for each additional account. CLIProxyAPI automatically round-robins requests across all authenticated sessions.
# Check CLIProxyAPI logs
docker logs cli-proxy-api
# Verify CLIProxyAPI is reachable from LiteLLM container
docker exec litellm curl -s http://cli-proxy-api:8317/v1/models
# List all models LiteLLM knows about
curl http://localhost:4000/v1/models \
-H "Authorization: Bearer $LITELLM_MASTER_KEY"The running service can't see any OAuth tokens. Usually a volume mismatch:
# 1. Confirm which volume the running service is mounting
docker inspect cli-proxy-api --format '{{range .Mounts}}{{.Name}} {{.Destination}}{{"\n"}}{{end}}'
# 2. List cliproxy volumes on the host
docker volume ls | grep cliproxy
# 3. Peek inside each to find where the tokens actually landed
docker run --rm -v litellm_cliproxy_auth:/a alpine ls -la /aIf tokens are in a different volume (e.g. ai-proxy_cliproxy_auth) than the one mounted by the service, either re-run the login against the correct volume name, or copy them over:
docker run --rm \
-v <source_volume>:/src \
-v litellm_cliproxy_auth:/dst \
alpine sh -c "cp -a /src/. /dst/"
docker compose restart cli-proxy-apiThen expect docker logs cli-proxy-api to show N clients (N auth files + ...) with N >= 1.
When adding models through the LiteLLM web UI, OpenRouter has two known limitations:
The openrouter provider option in the UI does not work reliably. Instead, use Custom OpenAI-Compatible as the provider type and set the base URL manually:
Base URL: https://openrouter.ai/api/v1
Example config.yaml equivalent:
model_list:
- model_name: mistral-7b
litellm_params:
model: openrouter/mistralai/mistral-7b-instruct
api_base: https://openrouter.ai/api/v1
api_key: sk-or-your-openrouter-keyThe web UI does not support a global provider credential that child models inherit. You must enter the API key individually on each model you add. There is no "set once, use everywhere" option in the UI.
Workaround β define credentials directly in config.yaml so they are managed in one place:
model_list:
- model_name: openrouter-model-a
litellm_params:
model: openrouter/mistralai/mistral-7b-instruct
api_base: https://openrouter.ai/api/v1
api_key: os.environ/OPENROUTER_API_KEY # reference .env variable
- model_name: openrouter-model-b
litellm_params:
model: openrouter/meta-llama/llama-3-8b-instruct
api_base: https://openrouter.ai/api/v1
api_key: os.environ/OPENROUTER_API_KEY # same key, set per modelAnd in .env:
OPENROUTER_API_KEY=sk-or-your-openrouter-keyThis avoids duplicating the raw key in config.yaml while still satisfying the per-model requirement.
If you expand this later, next step is:
- add logging (Postgres / Redis)
- add rate limiting per client
- integrate with Traefik auth
LiteLLM handles two types of migrations differently:
| Type | Handled by |
|---|---|
| Schema changes (new columns, tables) | LiteLLM auto-runs on startup via Prisma |
| Host migration (moving to new server) | Manual β use standard Postgres tools |
There is no built-in LiteLLM host migration tool. Use the steps below.
docker exec litellm_db pg_dump -U litellm litellm > litellm_backup.sqlscp docker-compose.yml config.yaml litellm_backup.sql user@new-host:~/litellm/cd ~/litellm
docker compose up -d dbdocker exec -i litellm_db psql -U litellm litellm < litellm_backup.sqldocker compose up -d litellmLiteLLM will run Prisma schema migrations automatically on startup.
- Keep the same
LITELLM_MASTER_KEYβ virtual keys stored in the DB are tied to this key. Changing it invalidates them. Copy your.envto the new host to preserve it. store_model_in_db: trueβ if enabled, models are stored in the DB and will be restored automatically with the dump. No need to re-add them.- Update DNS/reverse proxy β point your clients to the new host after migration.
- Do not commit
.envto Git β copy.env.exampleto the new host and fill in the values, or securely transfer your.envdirectly.