Як розгорнути LiteLLM як LLM gateway на VPS
Запустіть LiteLLM на VPS як один OpenAI-compatible endpoint: virtual keys, бюджети для ключів, fallbacks і pinned images для стабільних оновлень.
Що робить self-hosted LLM gateway
LiteLLM — це open source LLM gateway, який ви розгортаєте самостійно: один HTTP endpoint, до якого звертаються всі ваші застосунки. Gateway передає кожен запит тому провайдеру, який має на нього відповісти. LLM означає large language model. Gateway використовує OpenAI chat completions API (application programming interface), тому будь-яка клієнтська бібліотека, яка вже працює з OpenAI, працюватиме і з ним після двох змін: base URL і key.
У цьому й полягає призначення такого шару абстракції. Застосунки більше не зберігають облікові дані провайдерів. Щоб змінити модель, достатньо змінити один рядок у конфігураційному файлі на сервері, а не вносити зміни в код п’яти сервісів. Оскільки кожен виклик проходить через один процес, у вас є централізоване місце для встановлення бюджету та обліку витрат.
Після запуску ви отримуєте:
- Один endpoint. Застосунки звертаються до
https://gateway.example.com/v1і запитують модель із заданою вами назвою, наприкладbulkабоstrong. - Віртуальні ключі. Кожен застосунок отримує власний key із власним allowlist моделей і власним лімітом витрат. Один ключ можна відкликати, не змінюючи інші.
- Fallbacks. Якщо запит завершується помилкою або prompt перевищує допустимий розмір, його автоматично повторно надсилають до іншої моделі.
- Запис у журналі. Для кожного запиту записується рядок із його вартістю, тому на запитання «який застосунок це витратив?» є відповідь.
Навіщо запускати gateway самостійно
Керований router має таку саму схему, але між кожним запитом і сервісом працює процес стороннього постачальника. Якщо запускати його самостійно, ключі провайдера та текст prompt зберігаються на сервері під вашим контролем. Ціна цього рішення реальна: тепер ви обслуговуєте компонент, від якого залежить кожен застосунок. Останній розділ цього посібника присвячено цій ціні, оскільки в більшості матеріалів про неї не згадують.
Що потрібно
- VPS (віртуальний приватний сервер) під керуванням Ubuntu 24.04, на якому встановлено Docker і плагін Compose.
- Доменне ім’я, що вказує на цей сервер, якщо зовнішні клієнти підключатимуться до шлюзу через TLS (захист транспортного рівня).
- Щонайменше один API-ключ провайдера.
Шлюз не виконує інференс. Він пересилає запити та передає відповіді потоком, тому навантаження на CPU залежить від кількості запитів, а не від розміру моделі. Сервер із 1 vCPU без проблем обслуговує кілька внутрішніх застосунків. Зростає база даних, оскільки шлюз записує один рядок витрат для кожного запиту.
Спочатку створіть config.yaml
Файл конфігурації визначає, які моделі може запитувати клієнт. Важливі чотири секції верхнього рівня: model_list, litellm_settings, router_settings і 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: 300model_name — це ім’я, яке надсилають клієнти. litellm_params.model — фактична модель, указана як provider/model. Називайте моделі за призначенням, а не за постачальником. Застосунок, який запитує bulk, продовжить працювати, навіть якщо наступного місяця ви вирішите, що bulk має бути іншою моделлю.
api_key: os.environ/ANTHROPIC_API_KEY вказує LiteLLM зчитувати цю змінну під час виконання. Фактичний ключ не з’являється у файлі. Це важливо, оскільки config.yaml — файл, який ви додаєте до репозиторію.
Два записи навмисно мають ім’я strong. Якщо кілька розгортань мають однакове model_name, маршрутизатор вважає їх взаємозамінними та пробує інше, коли перше завершується помилкою. Завдяки цьому strong продовжує працювати, навіть якщо в одного з постачальників тимчасово виникли проблеми.
num_retries: 2 повторює спробу для того самого розгортання, якщо сталася помилка, після якої можна повторити запит. Резервний варіант використовується лише після завершення цих повторних спроб. allowed_fails: 3 разом із cooldown_time: 30 вилучає розгортання з ротації на 30 секунд після 3 невдалих спроб. Тому постачальника, який повертає помилки 500, не перевірятимуть під час кожного запиту.
fallbacks і context_window_fallbacks мають різні умови спрацювання. Другу умову часто пропускають, хоча вона корисна.
fallbacksспрацьовує, коли основний виклик завершується помилкою.context_window_fallbacksспрацьовує, коли постачальник відхиляє запит, оскільки він довший за контекстне вікно цієї моделі. Тоді надто великий промпт передається моделі, у контексті якої є достатньо місця, замість повернення помилки клієнту.
Також є content_policy_fallbacks. Цей параметр призначений для випадків, коли постачальник відхиляє запит через політику щодо вмісту. Установлюйте його лише тоді, коли маєте доцільний резервний напрямок для таких запитів.
Розгортання LiteLLM на VPS за допомогою Docker Compose
Створіть каталог із трьома файлами: config.yaml, docker-compose.yml і .env. У quickstart від upstream використовується тег latest. Замість нього зафіксуйте тег релізу. Тоді docker compose up -d наступного місяця надасть вам той самий gateway, що й сьогодні, а відкат можна буде виконати одним рядком.
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 двічі читає .env. Спочатку він підставляє ${POSTGRES_PASSWORD} безпосередньо у файл compose. Потім через env_file передає всі змінні в контейнер.
v1.95.0 був поточним релізом у серпні 2026 року. Перевірте сторінку релізів проєкту та зафіксуйте версію, актуальну на момент розгортання. Для кожного релізу публікується підпис, тому перед довірою до образу його можна перевірити:
cosign verify --key https://raw.githubusercontent.com/BerriAI/litellm/v1.95.0/cosign.pub ghcr.io/berriai/litellm:v1.95.0Рядок із портом має вигляд 127.0.0.1:4000:4000. Він публікує порт лише на loopback-інтерфейсі. Якщо написати 4000:4000, gateway стане доступним з усього інтернету. Docker додає власні правила до ланцюжка FORWARD в iptables, і вони обробляються раніше за правила ufw. Тому ufw deny 4000 не заблокує такий доступ. Це найпоширеніший спосіб, у який self-hosted gateway випадково опиняється відкритим: див. як Docker публікує порт контейнера безпосередньо в обхід ufw. Зовнішній трафік надходить через reverse proxy.
Не зберігайте ключі провайдерів в образі
Файл .env містить усі секрети. Під час запуску він передається як змінні середовища, тому секрети не вбудовуються в образ і файл ніколи не додається до репозиторію.
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-...Згенеруйте два ключі LiteLLM із використанням криптографічно стійкої випадковості, а потім обмежте права доступу до файлу:
printf 'sk-%s\n' "$(openssl rand -hex 32)"
chmod 600 .envLITELLM_MASTER_KEY — облікові дані адміністратора. Вони використовуються для автентифікації Management API і є паролем для Admin UI за адресою /ui. Жоден застосунок не повинен їх зберігати.
LITELLM_SALT_KEY шифрує облікові дані провайдерів, збережені в базі даних. Задайте це значення один раз і не змінюйте його. Якщо змінити його пізніше, уже збережені облікові дані неможливо буде розшифрувати. Шлюз запуститься без помилок, але кожен виклик цих провайдерів завершуватиметься помилкою автентифікації.
STORE_MODEL_IN_DB=True дає змогу додавати й редагувати моделі в Admin UI без змін у config.yaml. Це зручно, але джерел правди стає два. Визначте, яке з них є основним, і зафіксуйте це рішення поруч із конфігурацією.
Логіка, за якою ключі не зберігають у файлі конфігурації, так само стосується інструментів, які ви надаєте агенту. У матеріалі Не зберігайте секрети провайдерів у AI-агентів описано цей підхід, а в матеріалі Файли env і секрети в Docker Compose — практичну реалізацію.
Запустіть сервіс і стежте за першим запуском:
docker compose up -d
docker compose logs -f litellmПеревірте фактичну роботу
Є дві проби без автентифікації та одна проба з автентифікацією. Вони завершуються помилками з різних причин.
curl -s http://127.0.0.1:4000/health/liveliness
curl -s http://127.0.0.1:4000/health/readiness/health/liveliness не потребує автентифікації та повертає відповідь "I'm alive!", поки процес працює. /health/readiness також не потребує автентифікації. Він повертає JSON-об’єкт із полем "status": "healthy" і полем db або код 503, якщо база даних недоступна. Налаштовуйте моніторинг на readiness, оскільки liveliness залишатиметься успішною на gateway, який не може знайти жодного virtual key.
Перевірка з автентифікацією — це та, що звертається до providers:
curl -s http://127.0.0.1:4000/health \
-H "Authorization: Bearer $LITELLM_MASTER_KEY"Вона повертає масиви healthy_endpoints і unhealthy_endpoints. Якщо модель у unhealthy_endpoints має помилку автентифікації, це означає, що ключ provider у .env неправильний або відсутній. Саме цю помилку потрібно знайти зараз. Оскільки background_health_checks: true встановлено, proxy самостійно виконує ці проби кожні health_check_interval секунд, а /health повертає останній результат. Тому опитування цього endpoint не надсилає тестовий запит до providers щоразу.
Віртуальні ключі та бюджети для окремих ключів
Кожен застосунок отримує власний ключ, створений на основі master key.
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
}'Відповідь містить поле key, значення якого починається з sk-. Цей рядок отримує застосунок, і це єдине, що він коли-небудь отримує.
models— це allowlist того, що може запитувати цей ключ. Наведений вище ключ може запитувати лишеbulk.max_budget: 5зі значеннямbudget_duration: "30d"— це п’ять доларів США за кожні ковзні 30 днів. Після вичерпання бюджету ключ припиняє працювати.rpm_limitіtpm_limitобмежують кількість запитів за хвилину та кількість токенів за хвилину лише для цього ключа.key_alias— це те, що ви побачите в журналі витрат через шість тижнів. Завжди встановлюйте це значення.
Коли бюджет вичерпано, виклик завершується помилкою HTTP 401, а тіло відповіді має такий вигляд:
ExceededBudget: Current spend for token: 7.2e-05; Max Budget for Token: 2e-07Саме код стану робить цю помилку заплутаною. Бібліотека клієнта повідомляє про 401 як про проблему автентифікації, тому розробник, який читає stack trace, починає перевіряти, чи дійсний ключ. Записуйте тіло відповіді разом із кодом стану. Інакше вичерпання бюджету щоразу виглядатиме як пошкоджені облікові дані.
Переглядайте та змінюйте ключі через той самий management API:
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}'Бюджет, обмежений на gateway, продовжує діяти навіть тоді, коли проблема виникла в самому агенті. Саме тому він є основою контролю витрат для AI-агентів на VPS.
Передавайте пакетні завдання дешевій моделі
Налаштуйте клієнт на роботу через шлюз. Базова URL-адреса, ключ і назва моделі:
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."}]
}'Будь-яка клієнтська бібліотека OpenAI працює так само: задайте base_url як https://gateway.example.com/v1, а api_key — як віртуальний ключ.
Політика маршрутизації з config.yaml тепер застосовується без відома клієнта. Запит до bulk надсилається дешевій моделі. Якщо цей виклик не вдається після всіх повторних спроб, запит повторно надсилається до strong. Якщо запит завеликий для bulk, context_window_fallbacks надсилає його до strong замість повернення помилки. Пакетні завдання, як-от класифікація або підсумовування черги, за замовчуванням виконуються дешево. Лише складні запити коштують дорожче.
Саме тут шлюз особливо корисний для агентів, які використовують інструменти. MCP (протокол контексту моделі) сервер на тому самому VPS і агент, який ним керує, можуть використовувати одну кінцеву точку. Тому модель за ними можна змінити без повторного розгортання жодного з них.
Як дізнатися, що відбулося перемикання на резервний варіант?
Це сценарій збою, який коштує грошей, оскільки зовні нічого не виглядає зламаним. Успішне перемикання на резервний варіант повертає HTTP 200 зі звичайним тілом відповіді. Ваша дешева модель може бути недоступною протягом дня, усі запити непомітно оброблятиме дорожча модель, а першим доказом стане рахунок.
Докази є в заголовках відповіді. Запитайте їх:
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— це те, що запросив клієнт.x-litellm-model-id— deployment, який надав відповідь. Якщо ці два значення відрізняються, відбулося перемикання на резервний варіант.x-litellm-attempted-fallbacksіx-litellm-attempted-retriesпідраховують такі перемикання. Для справного запиту обидва значення дорівнюють 0.x-litellm-response-cost— вартість цього запиту в доларах США.x-litellm-call-id— ідентифікатор, за яким можна знайти цей самий запит у журналах.
Записуйте x-litellm-attempted-fallbacks для кожного запиту та налаштуйте сповіщення, коли його значення перестає бути 0. Це число показує різницю між політикою маршрутизації, яка працює, і політикою, що непомітно перетворилася на «завжди використовувати дорожчу модель».
Повна реалізація цього підходу — трасування, і для неї потрібне окреме налаштування: self-hosted Langfuse для трасування викликів агентів. LiteLLM постачається з callback, тому його підключення потребує двох рядків і облікових даних.
litellm_settings:
success_callback: ["langfuse"]
failure_callback: ["langfuse"]LANGFUSE_PUBLIC_KEY=pk-lf-...
LANGFUSE_SECRET_KEY=sk-lf-...
LANGFUSE_HOST=https://langfuse.example.comНалаштуйте failure_callback, а також success_callback. Якщо пропустити цей параметр, ви зберігатимете лише трасування запитів, у яких нічого не пішло не так. Окремо від цього LiteLLM записує рядок про витрати для кожного запиту в Postgres, а Admin UI за адресою /ui читає цю таблицю. Таблиця зростає разом із трафіком, тому контролюйте вільне місце на невеликому диску.
Розмістіть gateway за reverse proxy
Жоден зовнішній вузол не повинен мати доступу до порту 4000. Завершуйте TLS у nginx або Caddy та переспрямовуйте запити на 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;
}Саме ці два рядки часто пропускають. proxy_buffering off важливий, тому що потокова відповідь складається з послідовності server-sent events. Якщо в nginx увімкнено буферизацію, він утримує фрагменти до завершення відповіді. Клієнт мовчить, а потім отримує все одразу. proxy_read_timeout 600s важливий, тому що тривала генерація перевищує стандартний тайм-аут nginx у 60 секунд. У такому разі клієнт отримує 504, а журнал помилок містить upstream timed out (110: Connection timed out) while reading response header from upstream.
Для сертифіката скористайтеся коротким шляхом: Certbot з Let’s Encrypt у nginx. Якщо на сервері вже працює кілька контейнерів, Traefik перед кількома Compose-застосунками централізовано обробляє маршрутизацію та сертифікати.
Шлюз тепер є єдиною точкою відмови
Відверто оцініть те, що ви побудували. Тепер кожен ваш застосунок залежить від одного контейнера на одному VPS. Поки він недоступний, жоден компонент не може викликати жодну модель, зокрема провайдерів, які працюють без проблем. З цього випливають чотири наслідки.
- Неправильна конфігурація одночасно зупиняє все.
restart: unless-stoppedперезапускає процес після збою, а також знову й знову перезапускає контейнер, який не може розібрати config.yaml. Після кожної зміни конфігурації перевіряйтеdocker compose logs litellmі вносьте зміни тоді, коли маєте час спостерігати за результатом. - Postgres перебуває на шляху обробки запитів. І пошук virtual key, і запис витрат використовують його. Відповідь
/health/readinessзі статусом 503 попереджає, що шлюз працює, але не може виконувати ні одну, ні іншу операцію. - Масштабуйте додаванням екземплярів, а не збільшенням одного екземпляра. Власні рекомендації проєкту передбачають один worker на екземпляр (
--num_workers 1) і кілька екземплярів, що використовують одну базу даних. Два невеликі шлюзи за load balancer усувають залежність від одного контейнера. Але залежність від бази даних вони не усувають. - Створюйте резервні копії даних, які неможливо відновити повторною генерацією. Це
config.yamlі.envразом ізpg_dumpбази даних. ВтратаLITELLM_SALT_KEYзробить зашифровані облікові дані провайдерів у цьому дампі непридатними, тому env-файл і дамп мають копіюватися одним завданням: резервні копії restic у зовнішнє сховище.
Оновлення виконується редагуванням тега образу та запуском docker compose up -d. За замовчуванням LiteLLM запускає prisma migrate deploy під час старту, тому новий контейнер переносить схему бази даних під час першого запуску. Створіть дамп до зміни тега, оскільки повернення до старого образу не скасує міграцію, яка вже відбулася.
FAQ
Чи додає LiteLLM помітну затримку до кожного виклику?
Проєкт заявляє затримку 8 ms на 95-му перцентилі за 1000 запитів на секунду, як зазначено в його README у серпні 2026 року. Вважайте це даними від постачальника. Фактично на вашу затримку впливає мережева відстань між застосунками та шлюзом, оскільки кожен виклик тепер містить додатковий круговий обмін даними. Розмістіть шлюз у тому самому регіоні, що й застосунки, які його викликають, а потім виміряйте власні накладні витрати за допомогою заголовка x-litellm-overhead-duration-ms у реальній відповіді.
Чому потокова передача перестала працювати після встановлення nginx перед шлюзом?
Тому що nginx за замовчуванням буферизує відповіді від upstream, а потокова генерація складається з послідовності server-sent events. Коли proxy_buffering увімкнено, nginx накопичує фрагменти й передає їх лише після завершення відповіді. Клієнт мовчки очікує, а потім отримує всю відповідь одночасно. У блоці location встановіть proxy_buffering off;. У тому самому блоці збільште proxy_read_timeout, оскільки тривала генерація інакше перевищить стандартне значення nginx — 60 секунд — і клієнт отримає 504.
Що відбувається, коли у virtual key закінчується бюджет?
Виклик завершується помилкою HTTP 401, а тіло відповіді має форму ExceededBudget: Current spend for token: 7.2e-05; Max Budget for Token: 2e-07. Код 401 вводить в оману: бібліотека клієнта повідомляє про помилку автентифікації, тому замість читання повідомлення користувачі починають перевіряти, чи дійсний ключ. Записуйте тіло відповіді разом із кодом стану. Перевірте фактичне положення ключа за допомогою /key/info?key=sk-..., використовуючи master key, і збільште ліміт за допомогою /key/update, якщо бюджет установлено надто низьким.
Чи може шлюз маршрутизувати запити до локальної моделі та до hosted-моделей?
Так, це ще один запис у model_list. Використовуйте префікс ollama_chat/ з api_base, наприклад model: ollama_chat/llama3.1 разом із api_base: http://ollama:11434. Усередині контейнера localhost означає цей самий контейнер. Тому використовуйте ім’я сервісу Compose або адресу хоста в Docker network, але ніколи не 127.0.0.1. Розгортання локальної моделі є окремим завданням: див. самостійне розгортання LLM за допомогою Ollama на VPS.