Запустить веб-приложение с ИИ на своём ноутбуке — дело получаса. Команда 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-токен.
Отсюда два действия, которые стоит сделать сразу, а не потом:
- Убедиться, что продление реально сработало хотя бы раз. Не «команда есть в cron», а «дата истечения сдвинулась». Сухой прогон и проверка даты — в гайде про HTTPS и автопродление сертификатов.
- Поставить проверку срока в мониторинг. Пятьдесят раз проще, чем ловить по факту.
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 — и не делает ничего. Порт остаётся занят, старый процесс продолжает отдавать старый код, скрипт деплоя докладывает «перезапущено».
Итог: деплой «прошёл», версия не поменялась, а узнать об этом неоткуда — порт живой, страница открывается.
Механизм, который это закрыл, стоит описать, потому что он переносится на любую систему с разделением прав:
- Неповышенный процесс не убивает сервер, а кладёт файл-маркер запроса в каталог данных.
- Повышенный watchdog на очередном тике маркер валидирует (точный путь, владелец файла, возраст,
nonceпротив повтора), потребляет и перезапускает сервер сам. - Маркер удаляется до рестарта и при любом исходе, принятый
nonceпишется в журнал — иначе битый файл пережёвывался бы каждую минуту вечно. - Успех считается доказанным, только когда версия сошлась по двум независимым HTTP-проверкам:
/healthz— версия процесса, который реально слушает порт, и?v=в HTML — то, что уедет в браузер.
Ни новой службы, ни хранимого пароля, ни открытого порта. И главное свойство: тихий успех стал невозможен — провал уходит в лог отдельной строкой и в файл вердикта со статусом failed.
Отсюда общее правило мониторинга: открытый порт не доказывает ничего. Он не доказывает даже того, что там ваше приложение, не говоря о его версии. Минимальный набор проверок для сервиса без дежурной смены — в статье про мониторинг личного сервиса.
#Мифы и заблуждения
«Docker решает проблему деплоя». Он решает проблему сборки окружения и создаёт две новых для ИИ-приложений: нет доступа к экрану и сложный доступ к видеокарте. Если ваш сервис — веб плюс приём данных, контейнер удобен. Если ему нужно снимать экран машины — контейнер отменяет саму функцию.
«Есть аптайм — значит работает». Аптайм процесса и корректность ответа — разные вещи. Осиротевший процесс на унаследованном сокете имеет прекрасный аптайм.
«Автозапуск настроен — значит переживёт перезагрузку». Проверяется только фактической перезагрузкой без логина. Всё остальное — предположение.
«Свой сервер = приватность». Свой сервер даёт вам контроль над хранением. Он не отменяет того, что запросы уходят туда, куда вы их отправляете: подключили облачного провайдера по ключу — текст запросов уходит именно ему. Данные остаются на вашей машине, если приложение развёрнуто у вас и работает с локальной моделью.
«Сначала масштабирование». Личный сервис на одного-двух человек не упирается в производительность. Он упирается в вашу способность заметить сбой и в ваше время на обслуживание.
#С чего начать по шагам
- Сделайте приложение независимым от того, кто его запустил: абсолютные пути, каталог данных вне репозитория, секреты в
.envвне git. - Добавьте дешёвую ручку живости, которая отдаёт версию сборки. Без версии ручка бесполезна на шестом этапе.
- Настройте автозапуск и проверьте перезагрузкой без входа в систему, а не отчётом установщика.
- Поднимите доступ извне — туннелем, если домена ещё нет, но считайте это временным.
- Заведите домен и HTTPS. В тот же день поставьте проверку срока сертификата.
- Поставьте прокси, отключите буферизацию, поднимите таймаут чтения.
- Сделайте деплой, который не имеет права отчитаться об успехе без подтверждения версии по HTTP.
- Прочитайте честный список рисков самостоятельного хостинга и решите, готовы ли вы быть единственным дежурным.
Пункт 8 стоит последним намеренно. Он единственный, который может отменить все предыдущие, и лучше отменить их до, а не после переезда.
#Частые вопросы
#Нужен ли сервер с видеокартой, чтобы поднять личный ИИ-сервис?
Обычно нет. Само веб-приложение — это Python и SQLite, оно спокойно живёт на скромном арендованном сервере. Видеокарта нужна там, где крутится модель, а модель чаще всего либо стоит у вас дома, либо это ключ облачного провайдера. Смешивать эти две роли на одной машине — самый дорогой способ решить задачу.
#Сколько времени в месяц уходит на обслуживание такого сервиса?
Честного среднего числа нет — оно зависит от того, насколько рано вы поставили мониторинг. Опыт этого проекта: пока проверок нет, обслуживание стоит ноль часов в спокойный месяц и целый день в тот, когда что-то сломалось молча. Каждая автоматическая проверка переводит часть этого дня в минуты.
#Можно ли обойтись без домена и жить на туннеле?
Для себя одного — можно, годами. Как только сервисом пользуется кто-то ещё, меняющийся адрес и самопроизвольные обрывы становятся вашей проблемой поддержки. Плюс без своего домена вы не управляете почтой, сертификатом и записями DNS.
#Что делать, если после деплоя сайт отдаёт старую версию?
Не перезапускать вслепую. Сначала выяснить, кто держит порт: найти слушателя по номеру порта, посмотреть его идентификатор процесса и сессию. Типовая причина — осиротевший процесс, поднятый в другой сессии или с другими правами, которого ваш терминал не может убить. На Windows это ещё и молчаливый отказ taskkill, который возвращает успех.
#Обязательно ли настраивать HTTPS, если сервисом пользуюсь только я?
Если вход только с локальной машины — нет. Как только адрес открывается из интернета, пароль и сессионная кука ходят по сети открытым текстом, а часть браузерных возможностей (сервис-воркер, установка как приложение) без HTTPS просто не включится.
#Как понять, что пора уходить с домашней машины на аренду?
По двум признакам: сервис нужен не только вам, и вас начинает раздражать зависимость от домашнего электричества и канала. Если ни один не наступил — переезжать не нужно, аренда добавит счёт и не добавит ничего, кроме нового набора отказов.
#Вывод
Если вам нужен доступ к своему ассистенту с телефона и вы согласны, чтобы иногда он был недоступен, — оставайтесь на локальном запуске плюс туннель, это честный компромисс и он ничего не стоит. Если сервисом пользуется кто-то кроме вас — проходите все шесть этапов и считайте шестой главным: неприятность самохостинга не в том, что что-то ломается, а в том, что вы можете об этом не узнать.
Persona развёртывается по этому же маршруту: локальный запуск, автозапуск, домен, прокси, мониторинг. Сегодня она бесплатна и работает со своей моделью или вашим ключом провайдера — посмотреть, из чего она состоит, можно до того, как поднимать что-либо на сервере.