Технологии

Как поднять свой ИИ-сервис: от ноутбука до домена

Шесть этапов от локального запуска до публичного домена. На каждом — что именно ломается первым, сколько это стоит в деньгах и во времени и какой новый класс отказов вы покупаете вместе с удобством.

Запустить веб-приложение с ИИ на своём ноутбуке — дело получаса. Команда uvicorn app:app --reload, браузер на 127.0.0.1:8000, всё работает. Ровно на этом месте заканчивается девяносто процентов инструкций в интернете.

Дальше начинается то, о чём в них не пишут. Приложение должно работать, когда вы вышли из системы. Должно вернуться после перезагрузки, на которую вас никто не спрашивал. Должно продолжать отдавать свежий код после деплоя, а не молча кормить браузер вчерашней версией. Должно как-то сообщать вам, что оно упало, — потому что дежурной смены у вас нет, дежурный это вы, и узнать о падении вы имеете шанс от единственного пользователя, который в этот момент оказался на сайте.

Эта статья — маршрут из шести этапов. Он написан не как список идеальных практик, а как хроника: на каждом этапе названо, что сломалось первым, во что это обошлось и по какому признаку понять, что вы уже готовы к следующему шагу. Часть примеров — из эксплуатации Persona на Windows Server, и они специально приведены с деталями, потому что именно детали обычно и не публикуют.

#Рамка: «поднять сервис» — это шесть разных задач

Люди говорят «задеплоить» так, будто это одно действие. На самом деле их шесть, и они решаются в строгом порядке — каждый следующий бессмысленен без предыдущего.

Этап Что вы получаете Что ломается первым
1. Локальный запуск приложение работает у вас ничего; иллюзия готовности
2. Живучесть процесса переживает краш и логаут автозапуск «только при входе пользователя»
3. Доступ извне адрес, который открывается не только у вас канал/туннель отваливается по своим причинам
4. Домен и HTTPS нормальный адрес и замок в браузере автопродление сертификата на третий месяц
5. Обратный прокси один вход, буферизация, таймауты потоковый ответ ИИ перестаёт быть потоковым
6. Эксплуатация вы узнаёте о сбое раньше пользователя деплой рапортует успех, отдавая старый код

Порядок не переставляется. Домен на приложении, которое не переживает перезагрузку, — это красивая ссылка на страницу с ошибкой соединения.

#Этап 1. Локальный запуск: чем он врёт

Локальный запуск честно проверяет ровно одно: код собирается и отвечает. Он не проверяет ничего из того, что вас потом сломает.

На локальном запуске у процесса есть ваш интерактивный сеанс, ваши переменные окружения, ваш профиль пользователя, ваши права. Всё это исчезает на этапе 2, и исчезает молча. Приложение, которое читает конфигурацию из .env рядом с текущим каталогом, на локальном запуске работает; та же программа, поднятая планировщиком с другим рабочим каталогом, стартует с пустой базой и создаёт её заново — и вы полдня будете искать, куда делись данные.

Поэтому единственная полезная работа на этапе 1 — сделать приложение независимым от того, кто и откуда его запустил:

  • пути к данным задаются абсолютной переменной окружения, а не относительным путём;
  • каталог данных лежит вне репозитория (у Persona это PERSONA_DATA_DIR, по умолчанию ~/.persona), иначе git clean однажды сотрёт вам базу;
  • секреты живут в .env вне git или в каталоге данных, и нигде больше;
  • есть дешёвая ручка проверки живости, которая не ходит в базу и не будит воркеров. У Persona это /healthz, отдающая {"status": "ok", "version": ...}. Версия в ответе понадобится на этапе 6 и там окажется главным, что есть в этой ручке.

Про хранение ключей стоит сказать прямо, раз уж речь о секретах: в Persona ключ участника вида byo_api_key_<провайдер> лежит в таблице пользовательских настроек в открытом виде. Шифруется только ключ владельца, и то при доступном хранилище. Это не «мы так решили — это удобно», это состояние, о котором нужно знать, если вы разворачиваете инстанс на кого-то кроме себя.

#Этап 2. Живучесть: тот самый баг про «только при входе пользователя»

Симптом выглядел так: сайт работает, пока владелец сервера залогинен по RDP. Отключился — через несколько минут сайт лёг. Первая версия диагноза была «падает приложение», и она была неверной.

Настоящая причина в настройках задачи планировщика. Windows Task Scheduler по умолчанию создаёт задачу с типом входа Interactive — «выполнять только для вошедшего в систему пользователя». Пока RDP-сессия жива, задача тикает и держит watchdog, watchdog держит uvicorn. Логаут — Windows останавливает задачу, watchdog умирает, вместе с ним умирает порождённый им сервер. Ни в одном логе приложения этого не видно: приложение не падало, его прибили снаружи.

Исправление — два параметра:

Principal : LogonType = S4U     RunLevel = Highest
Triggers  : BootTrigger + TimeTrigger (repeat PT1M, без Duration)
Settings  : StartWhenAvailable = true
            MultipleInstancesPolicy = IgnoreNew
            DisallowStartIfOnBatteries = false

S4U (Service-for-User) даёт токен пользователя без хранения его пароля — это и есть «выполнять независимо от того, вошёл ли пользователь». BootTrigger возвращает сервис после перезагрузки, когда в систему вообще никто не входил: без него всё честно ждёт логина владельца.

Отдельная деталь, которая стоила больше всего времени: этот баг возвращался после каждой переустановки. Установочный скрипт правил настройки, но не трогал принципал, а неповышенный шелл получает Access is denied (0x80070005) ровно на LogonType=S4U, RunLevel=Highest и BootTrigger — и не получает на всём остальном. То есть установка «проходила успешно», а задача оставалась интерактивной. Вывод регламента: после установки автозапуска проверяйте не код возврата установщика, а фактический принципал задачи.

На Linux ту же роль играет unit systemd, и ловушки там другие: пользовательский юнит останавливается вместе с сеансом, а перезапуск без предохранителя уходит в бесконечный цикл. Windows-специфика — графическая сессия, права на принципал задачи и молчаливый отказ установщика без повышения — разобрана отдельно.

#Этап 3. Доступ извне: почему туннель — это не «временное решение»

Белый IP есть далеко не у всех, а домашний провайдер за NAT — норма. Стандартный ответ — туннель. Практика оказалась менее радужной: у каждого туннеля есть свой способ не работать, и узнаёте вы о нём не в документации, а через несколько дней эксплуатации.

Из перебранного на этом проекте:

  • Cloudflare Tunnel требует исходящий 7844, который на арендованной машине оказался закрыт на сетевом уровне. Не «медленно» — просто закрыт;
  • devtunnel от Microsoft ходит по 443 и потому проходит везде, но URL меняется при пересоздании, а сам туннель отваливается сам по себе;
  • Tailscale Funnel выглядел заменой на бумаге (443, стабильный URL, служба Windows) и в итоге тоже не стал постоянным решением в этой конфигурации.

Отсюда правило, к которому мы пришли: туннель — это способ прожить до домена, а не архитектура. Как только сервис нужен кому-то кроме вас, вы платите за нормальный вход. И проверять исходящий порт, который нужен решению, надо до того, как читать про его достоинства.

Здесь же решается вопрос, где вообще будет жить сервис. Две ветки: аренда и своё железо. Требования считаются от нагрузки конкретного приложения, а не от красивых тарифов — это разобрано в подборе VPS под личный ИИ-сервис. Если тянет поставить коробку дома, честный счёт с электричеством, шумом и вашим временем — в статье про домашний сервер под ИИ. Заранее один факт против домашнего варианта и против контейнера на VPS: захват экрана в контейнере не работает — там нет графической сессии, снимать нечего. Контейнерный режим — это веб и приём данных извне, и ничего больше. Развёрнуто — в разборе Docker для ИИ-приложений.

#Этап 4. Домен и HTTPS: ломается не выдача, а продление

Первичная выдача сертификата сегодня автоматизирована до неприличия — её делает любой инструмент за одну команду. Поэтому проблема не там.

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

Отсюда два действия, которые стоит сделать сразу, а не потом:

  1. Убедиться, что продление реально сработало хотя бы раз. Не «команда есть в cron», а «дата истечения сдвинулась». Сухой прогон и проверка даты — в гайде про HTTPS и автопродление сертификатов.
  2. Поставить проверку срока в мониторинг. Пятьдесят раз проще, чем ловить по факту.

DNS-часть отдельно коварна тем, что почти любая ошибка в ней выглядит одинаково — «домен не работает», хотя работает: записи ещё не разошлись, или у резолвера кэш, или запись создана в зоне, которую никто не спрашивает. Проверять надо у авторитетного сервера, а не браузером.

#Этап 5. Обратный прокси: где ИИ-приложение отличается от блога

Здесь общие инструкции по nginx перестают работать, и это единственное место, где у нас есть право писать своё.

Обычному сайту всё равно, буферизует ли прокси ответ. ИИ-приложению — нет, потому что его главный сценарий это потоковый ответ: модель отдаёт токены по одному, интерфейс их показывает по мере поступления. Прокси с буферизацией по умолчанию накапливает ответ и отдаёт его целиком. Внешне это выглядит как «модель думает тридцать секунд и вываливает всё сразу» — и вы будете винить модель.

Второй параметр — таймаут. Стандартные шестьдесят секунд ответа хватает блогу и не хватает генерации на локальной модели: холодный старт плюс длинный ответ спокойно уходят за минуту, прокси рвёт соединение, пользователь видит обрыв ровно посередине фразы.

Минимальный набор, который нужно осознанно выставить:

location / {
    proxy_pass http://127.0.0.1:8000;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_buffering off;          # без этого потока не будет
    proxy_read_timeout 600s;      # длинная генерация
    proxy_set_header X-Forwarded-Proto $scheme;   # иначе HSTS не встанет
}

Последняя строка — не косметика. Приложение, которое ставит Strict-Transport-Security только на HTTPS-запросах, за прокси видит обычный http:// и заголовок не поставит, если не сказать ему правду о схеме. Готовые конфигурации для обоих серверов и разбор параметров — в гайде про обратный прокси перед ИИ-сервисом.

#Этап 6. Эксплуатация: тихий отказ дороже громкого

Громкий отказ — это когда всё лежит и вы это видите. Он неприятен, но лечится. Дорогой отказ — тихий: система рапортует успех и делает не то.

Самая дорогая история этого проекта — ровно такая, и родилась она как побочный эффект правильного фикса из этапа 2. Задача теперь запускается с S4U и RunLevel=Highest, значит она живёт в сессии 0, значит порождённый ею uvicorn тоже повышенный. Обычный неповышенный терминал владельца убить такой процесс не может: taskkill /F /T печатает SUCCESS, возвращает 0 — и не делает ничего. Порт остаётся занят, старый процесс продолжает отдавать старый код, скрипт деплоя докладывает «перезапущено».

Итог: деплой «прошёл», версия не поменялась, а узнать об этом неоткуда — порт живой, страница открывается.

Механизм, который это закрыл, стоит описать, потому что он переносится на любую систему с разделением прав:

  1. Неповышенный процесс не убивает сервер, а кладёт файл-маркер запроса в каталог данных.
  2. Повышенный watchdog на очередном тике маркер валидирует (точный путь, владелец файла, возраст, nonce против повтора), потребляет и перезапускает сервер сам.
  3. Маркер удаляется до рестарта и при любом исходе, принятый nonce пишется в журнал — иначе битый файл пережёвывался бы каждую минуту вечно.
  4. Успех считается доказанным, только когда версия сошлась по двум независимым HTTP-проверкам: /healthz — версия процесса, который реально слушает порт, и ?v= в HTML — то, что уедет в браузер.

Ни новой службы, ни хранимого пароля, ни открытого порта. И главное свойство: тихий успех стал невозможен — провал уходит в лог отдельной строкой и в файл вердикта со статусом failed.

Отсюда общее правило мониторинга: открытый порт не доказывает ничего. Он не доказывает даже того, что там ваше приложение, не говоря о его версии. Минимальный набор проверок для сервиса без дежурной смены — в статье про мониторинг личного сервиса.

#Мифы и заблуждения

«Docker решает проблему деплоя». Он решает проблему сборки окружения и создаёт две новых для ИИ-приложений: нет доступа к экрану и сложный доступ к видеокарте. Если ваш сервис — веб плюс приём данных, контейнер удобен. Если ему нужно снимать экран машины — контейнер отменяет саму функцию.

«Есть аптайм — значит работает». Аптайм процесса и корректность ответа — разные вещи. Осиротевший процесс на унаследованном сокете имеет прекрасный аптайм.

«Автозапуск настроен — значит переживёт перезагрузку». Проверяется только фактической перезагрузкой без логина. Всё остальное — предположение.

«Свой сервер = приватность». Свой сервер даёт вам контроль над хранением. Он не отменяет того, что запросы уходят туда, куда вы их отправляете: подключили облачного провайдера по ключу — текст запросов уходит именно ему. Данные остаются на вашей машине, если приложение развёрнуто у вас и работает с локальной моделью.

«Сначала масштабирование». Личный сервис на одного-двух человек не упирается в производительность. Он упирается в вашу способность заметить сбой и в ваше время на обслуживание.

#С чего начать по шагам

  1. Сделайте приложение независимым от того, кто его запустил: абсолютные пути, каталог данных вне репозитория, секреты в .env вне git.
  2. Добавьте дешёвую ручку живости, которая отдаёт версию сборки. Без версии ручка бесполезна на шестом этапе.
  3. Настройте автозапуск и проверьте перезагрузкой без входа в систему, а не отчётом установщика.
  4. Поднимите доступ извне — туннелем, если домена ещё нет, но считайте это временным.
  5. Заведите домен и HTTPS. В тот же день поставьте проверку срока сертификата.
  6. Поставьте прокси, отключите буферизацию, поднимите таймаут чтения.
  7. Сделайте деплой, который не имеет права отчитаться об успехе без подтверждения версии по HTTP.
  8. Прочитайте честный список рисков самостоятельного хостинга и решите, готовы ли вы быть единственным дежурным.

Пункт 8 стоит последним намеренно. Он единственный, который может отменить все предыдущие, и лучше отменить их до, а не после переезда.

#Частые вопросы

#Нужен ли сервер с видеокартой, чтобы поднять личный ИИ-сервис?

Обычно нет. Само веб-приложение — это Python и SQLite, оно спокойно живёт на скромном арендованном сервере. Видеокарта нужна там, где крутится модель, а модель чаще всего либо стоит у вас дома, либо это ключ облачного провайдера. Смешивать эти две роли на одной машине — самый дорогой способ решить задачу.

#Сколько времени в месяц уходит на обслуживание такого сервиса?

Честного среднего числа нет — оно зависит от того, насколько рано вы поставили мониторинг. Опыт этого проекта: пока проверок нет, обслуживание стоит ноль часов в спокойный месяц и целый день в тот, когда что-то сломалось молча. Каждая автоматическая проверка переводит часть этого дня в минуты.

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

Для себя одного — можно, годами. Как только сервисом пользуется кто-то ещё, меняющийся адрес и самопроизвольные обрывы становятся вашей проблемой поддержки. Плюс без своего домена вы не управляете почтой, сертификатом и записями DNS.

#Что делать, если после деплоя сайт отдаёт старую версию?

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

#Обязательно ли настраивать HTTPS, если сервисом пользуюсь только я?

Если вход только с локальной машины — нет. Как только адрес открывается из интернета, пароль и сессионная кука ходят по сети открытым текстом, а часть браузерных возможностей (сервис-воркер, установка как приложение) без HTTPS просто не включится.

#Как понять, что пора уходить с домашней машины на аренду?

По двум признакам: сервис нужен не только вам, и вас начинает раздражать зависимость от домашнего электричества и канала. Если ни один не наступил — переезжать не нужно, аренда добавит счёт и не добавит ничего, кроме нового набора отказов.

#Вывод

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

Persona развёртывается по этому же маршруту: локальный запуск, автозапуск, домен, прокси, мониторинг. Сегодня она бесплатна и работает со своей моделью или вашим ключом провайдера — посмотреть, из чего она состоит, можно до того, как поднимать что-либо на сервере.

Хотите ИИ, который помнит вас?

Persona держит контекст между чатами и работает на вашем железе — если развернуть её у себя и подключить локальную модель.

Создать аккаунт