127.0.0.1:11434, llama.cpp server на 127.0.0.1:8080 или vLLM на 127.0.0.1:8000 — точные порты и флаги сверьте с документацией вашей сборки); cloudflared (Cloudflare Tunnel) или ngrok; Caddy или nginx как reverse proxy; curl; Linux/macOS с systemd (или Docker); домен, делегированный в Cloudflare (для именованного туннеля). Версии пакетов сверьте с документацией.
sudo для установки пакетов и регистрации systemd-юнита.llm.<ваш-домен> — для именованного туннеля.cloudflared к edge Cloudflare. Входящие порты на роутере открывать не нужно.Пример: минимальная проверка, что модель жива локально.
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.
Что сделать: убедитесь, что сервер модели слушает 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-интерфейса, снаружи хоста порт недоступен.

Что сделать: создайте именованный туннель 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;
}

Что сделать: прогоните три проверки — отказ без токена, успех с токеном, стриминг.
Зачем: первые две показывают, что авторизация включена, третья — что длинные ответы не буферизуются.
Как проверить:
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:` |
Чек-лист перед тем, как считать работу законченной:
cloudflared и Caddy запущены как systemd-сервисы (systemctl enable --now cloudflared caddy), а не в открытом терминалеCaddyfile с правами 600, файл не в gitconfig.yml есть catch-all правило http_status:404trycloudflare-туннель остановленjournalctl -u cloudflared -n 50 нет повторяющихся ошибок подключенияПроблема. Туннель отвечает 502 Bad Gateway, локально всё работает
ERR Unable to reach the origin service. The service may be down or it may not be responding to traffic from cloudflared error="dial tcp 127.0.0.1:8080: connect: connection refused"cloudflared ходит на 127.0.0.1:8080, а Caddy слушает другой порт — например, вы не добавили блок { http_port 8080 } и он занял :80.ss -ltnp | grep caddy
Если в выводе :80, а не :8080 — добавьте глобальный блок с http_port 8080 в начало Caddyfile, перезапустите systemctl restart caddy и повторите curl.
Проблема. Локальный curl не подключается к модели
curl: (7) Failed to connect to 127.0.0.1 port 11434: Connection refused0.0.0.0 внутри контейнера без публикации порта).systemctl status <имя-юнита-модели>
ss -ltnp | grep 11434
Если сокета нет вообще — сервис упал, смотрите journalctl -u <имя-юнита> -n 100. Если сокет есть, но на другом адресе — приведите OLLAMA_HOST (или аналог) к 127.0.0.1:11434.
Проблема. OpenAI-совместимый клиент получает 401, хотя токен передан
401 Unauthorized в ответе, в логах Caddy — запрос отклонён правилом @badAuthAuthorization: Bearer sk-..., а вы настроили Basic-авторизацию (basicauth), которая ждёт Authorization: Basic base64(login:password). Форматы несовместимы.Caddyfile из Шага 2, и тот же токен пропишите в клиенте.curl -s -o /dev/null -w "%{http_code}\n" https://llm.<ваш-домен>/v1/models \
-H "Authorization: Bearer <ТОКЕН>"
200 означает, что формат заголовка совпал. Если получаете 401 и здесь — сверьте токен посимвольно (частая причина: лишний пробел или перевод строки при копировании).
Вы подняли цепочку «локальная модель → Caddy с Bearer-токеном → Cloudflare Tunnel», не открывая входящих портов, и проверили её тремя запросами. Дальше имеет смысл подключить к этому эндпоинту прикладной сценарий — например, ИИ-помощника для интернет-магазина на локальной модели, и вынести токен в секрет-хранилище вместо Caddyfile.
Нужен ли белый 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 в чек-листе обязателен.