← Все гайды AI Release
EN Подписаться на @ai_release1

Как открыть доступ к локальной LLM из интернета без проброса портов: туннели и reverse proxy

Обновлено: 21.10.2026 · AI Release · @ai_release1
Как открыть доступ к локальной LLM из интернета без проброса портов: туннели и reverse proxy

TL;DR

Требования и подготовка

Иллюстрация к разделу «Требования и подготовка»
Иллюстрация к разделу «Требования и подготовка»

Пример: минимальная проверка, что модель жива локально.

curl -s http://127.0.0.1:11434/api/tags

Ожидаемый вывод — JSON вида {"models":[{"name":"<имя-модели>",...}]}. Если получили список моделей, имя одной из них берите для запросов дальше.

Схема потока данных, которую вы соберёте:

клиент (Интернет)
   │  HTTPS + Authorization: Bearer <токен>
   ▼
Cloudflare edge  (TLS-терминация, DNS llm.<ваш-домен>)
   │  исходящее соединение cloudflared → edge, TCP 7844
   ▼
cloudflared (ваш хост)
   │  http://127.0.0.1:8080
   ▼
Caddy (проверка Bearer-токена, отключён буферинг ответа)
   │  http://127.0.0.1:11434
   ▼
локальный сервер модели (Ollama / llama.cpp / vLLM)

Зачем: обратный прокси выполняет две роли, которые нельзя вешать на сам инференс-сервер: проверку токена и отключение буферизации для стриминга. Туннель не публикует порт — он держит исходящее соединение, поэтому NAT и белый IP не нужны.

Как проверить: запустите ss -ltnp | grep -E '11434|8080' — должны быть видны слушающие сокеты только на 127.0.0.1.

Шаг 1. Привяжите инференс к localhost и проверьте API

Что сделать: убедитесь, что сервер модели слушает loopback и отвечает на OpenAI-совместимый эндпоинт.

Зачем: если сервис уже слушает 0.0.0.0, вы случайно опубликовали его в локальную сеть. Прокси и туннель должны быть единственной точкой входа.

Как проверить: два запроса — список моделей и короткая генерация.

curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:11434/v1/models
curl -s http://127.0.0.1:11434/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model":"<имя-модели>","messages":[{"role":"user","content":"ping"}],"max_tokens":16}'

Ожидаемо: 200 и JSON с полем choices. Для Ollama привязка к адресу задаётся переменной окружения OLLAMA_HOST, например OLLAMA_HOST=127.0.0.1:11434 в unit-файле systemd — синтаксис и поддерживаемые значения сверьте с документацией Ollama.

Пример: если сервис в Docker и вы хотите, чтобы прокси на хосте его видел, публикуйте порт только на loopback:

docker run -d --name llm -p 127.0.0.1:11434:11434 <образ>

Почему это работает: -p 127.0.0.1:... создаёт правило DNAT только для loopback-интерфейса, снаружи хоста порт недоступен.

Шаг 2. Поднимите туннель и reverse proxy с токеном

Иллюстрация к разделу «Шаг 2. Поднимите туннель и reverse proxy с токеном»
Иллюстрация к разделу «Шаг 2. Поднимите туннель и reverse proxy с токеном»

Что сделать: создайте именованный туннель Cloudflare, направьте его на Caddy, а Caddy — на модель.

Зачем: туннель даёт стабильный HTTPS-URL без открытых портов; Caddy добавляет авторизацию, которой у локального инференса обычно нет.

Как проверить: по шагам ниже.

1. Авторизация и создание туннеля (интерактивно, один раз):

cloudflared tunnel login
cloudflared tunnel create llm-gw
cloudflared tunnel route dns llm-gw llm.<ваш-домен>

login откроет браузер и положит сертификат в ~/.cloudflared/. Команда create выдаст UUID и файл ~/.cloudflared/<UUID>.json — это credentials, их не коммитят в git.

2. Конфиг ~/.cloudflared/config.yml:

tunnel: llm-gw
credentials-file: /home/<пользователь>/.cloudflared/<UUID>.json
ingress:
  - hostname: llm.<ваш-домен>
    service: http://127.0.0.1:8080
  - service: http_status:404

Правило http_status:404 обязательно последним — Cloudflare Tunnel требует catch-all.

3. Caddyfile рядом:

{
    http_port 8080
}

http://llm.<ваш-домен> {
    @badAuth not header Authorization "Bearer <ДЛИННЫЙ_СЛУЧАЙНЫЙ_ТОКЕН>"
    respond @badAuth "Unauthorized" 401

    reverse_proxy 127.0.0.1:11434 {
        flush_interval -1
    }
}

Схема http:// в адресе сайта — потому что TLS уже терминируется на edge Cloudflare; http_port 8080 переносит слушатель Caddy с 80 на 8080, чтобы не занимать стандартный порт. flush_interval -1 отключает буферизацию — без него стриминг токенов придёт одним куском в конце.

4. Запуск и валидация:

caddy validate --config ./Caddyfile --adapter caddyfile
caddy run --config ./Caddyfile --adapter caddyfile
cloudflared tunnel run llm-gw

Пример: токен сгенерируйте локально, не придумывайте руками:

openssl rand -hex 32

Почему это работает: cloudflared сам инициирует соединение к edge и держит его; edge маршрутизирует запросы на llm.<ваш-домен> в этот туннель, дальше запрос идёт по loopback до Caddy.

Для быстрой проверки без домена есть временный туннель cloudflared tunnel --url http://127.0.0.1:11434, но он публикует порт модели без авторизации и выдаёт нестабильный URL — только для теста на 5 минут.

Альтернатива на nginx вместо Caddy:

location / {
    if ($http_authorization != "Bearer <ТОКЕН>") { return 401; }
    proxy_pass http://127.0.0.1:11434;
    proxy_buffering off;
    proxy_read_timeout 300s;
}

Шаг 3. Проверьте доступ из интернета и закройте лишнее

Иллюстрация к разделу «Шаг 3. Проверьте доступ из интернета и закройте лишнее»
Иллюстрация к разделу «Шаг 3. Проверьте доступ из интернета и закройте лишнее»

Что сделать: прогоните три проверки — отказ без токена, успех с токеном, стриминг.

Зачем: первые две показывают, что авторизация включена, третья — что длинные ответы не буферизуются.

Как проверить:

curl -s -o /dev/null -w "%{http_code}\n" https://llm.<ваш-домен>/v1/models
curl -s https://llm.<ваш-домен>/v1/models -H "Authorization: Bearer <ТОКЕН>"
curl -N https://llm.<ваш-домен>/v1/chat/completions \
  -H "Authorization: Bearer <ТОКЕН>" -H "Content-Type: application/json" \
  -d '{"model":"<имя-модели>","stream":true,"messages":[{"role":"user","content":"Считай до 5"}]}'

Ожидаемо: 401 в первой команде; JSON со списком моделей во второй; в третьей — строки data: {...}, приходящие по одной, а не всё сразу в конце. Флаг curl -N отключает буферизацию на стороне клиента, иначе вы не отличите клиентскую задержку от серверной.

Итоговая таблица проверок:

Что проверяемКомандаОжидаемый результат
Модель жива локально`curl -s http://127.0.0.1:11434/api/tags`JSON со списком моделей
Порт не торчит наружу`ss -ltnp \grep 11434`только `127.0.0.1:11434`
Авторизация включена`curl -o /dev/null -w "%{http_code}" https://llm.<ваш-домен>/v1/models``401`
Доступ с токеномтот же запрос с `-H "Authorization: Bearer <ТОКЕН>"``200` + JSON
Стриминг работает`curl -N ... "stream":true`поток строк `data:`

Чек-лист перед тем, как считать работу законченной:

Возможные проблемы и решения

Проблема. Туннель отвечает 502 Bad Gateway, локально всё работает

ss -ltnp | grep caddy

Если в выводе :80, а не :8080 — добавьте глобальный блок с http_port 8080 в начало Caddyfile, перезапустите systemctl restart caddy и повторите curl.

Проблема. Локальный curl не подключается к модели

systemctl status <имя-юнита-модели>
ss -ltnp | grep 11434

Если сокета нет вообще — сервис упал, смотрите journalctl -u <имя-юнита> -n 100. Если сокет есть, но на другом адресе — приведите OLLAMA_HOST (или аналог) к 127.0.0.1:11434.

Проблема. OpenAI-совместимый клиент получает 401, хотя токен передан

curl -s -o /dev/null -w "%{http_code}\n" https://llm.<ваш-домен>/v1/models \
  -H "Authorization: Bearer <ТОКЕН>"

200 означает, что формат заголовка совпал. Если получаете 401 и здесь — сверьте токен посимвольно (частая причина: лишний пробел или перевод строки при копировании).

Резюме и следующие шаги

Вы подняли цепочку «локальная модель → Caddy с Bearer-токеном → Cloudflare Tunnel», не открывая входящих портов, и проверили её тремя запросами. Дальше имеет смысл подключить к этому эндпоинту прикладной сценарий — например, ИИ-помощника для интернет-магазина на локальной модели, и вынести токен в секрет-хранилище вместо Caddyfile.

FAQ

Нужен ли белый IP и проброс портов на роутере?

Нет. cloudflared устанавливает исходящее соединение к edge Cloudflare по TCP 7844, поэтому NAT и динамический IP не мешают.

Можно ли обойтись без домена?

Для теста — да, временный туннель cloudflared tunnel --url http://127.0.0.1:11434 выдаст URL на trycloudflare.com. Для постоянной работы — нет: URL меняется, а авторизации по умолчанию нет.

Заменяет ли туннель авторизацию?

Нет. Туннель скрывает порт, но не проверяет, кто пришёл. Проверку токена делает reverse proxy (Caddy/nginx) — без неё ваш эндпоинт открыт всем, кто узнал URL.

Что произойдёт при обрыве связи с интернетом?

cloudflared переподключится автоматически, но только если запущен как сервис. Запуск из терминала умрёт вместе с сессией — поэтому systemctl enable --now cloudflared в чек-листе обязателен.

🤖 Кратко для ИИ