Summary
Я развернул у себя Excalidraw, Caddy Proxy Manager и LLM-роутер. В каждом случае README обещал «одну команду», а на деле я упирался в решения, которые авторы приняли давно и не афишируют: облако, зашитое в сборку, веб-морда, не умеющая жить за прокси, и окружение, которого нет у systemd. Вот три истории и чему они учат.
История 1. Excalidraw, который звонит домой
Я люблю Excalidraw - в нём рисуются схемы к моим статьям. Хотелось поставить свою копию в контейнер на домашнем Proxmox, чтобы работать вдвоём в одной доске.
Агент клонировал репозиторий, собрал образ и сразу нашёл неприятное: продакшен-сборка выглядит как обычная статика за nginx, но в .env.production прописаны адреса сервисов автора. Хранилище сцен, библиотеки, сервер совместной работы и Firebase - всё ведёт на облако проекта.
Я расстроился и попросил: «Сделай без Firebase, чтобы не платить Google». Честный ответ агента был такой: эмулятор Firebase для продакшена не годится, так пишет сам Google. Остаются три пути:
- совместная работа без сохранения файлов вообще;
- локальный эмулятор (не рекомендуется);
- форк четырёх функций (
saveToFirebase,loadFromFirebaseи их файловые близнецы) с подменой на свой backend: Postgres или SQLite плюс MinIO.
Для живого редактирования хватило тонкого сервера комнат на socket.io - он не требует ни Redis, ни базы. В итоге появился рабочий docker-compose.collab.yml с опциональным собственным Firebase-проектом.
Отдельная находка про безопасность: правила Firestore у Excalidraw разрешают чтение и запись документа, но не листинг. То есть вся защита держится на том, что ID комнаты невозможно угадать. Для доски со схемами нормально. Для чего-то секретного - нет.
Tip
Если проект выглядит как «просто статика», откройте
.env.production. Часто именно там спрятано всё, что держит функцию на чужом облаке.
История 2. Keenetic, который не хочет жить за прокси
Я переезжал с Nginx Proxy Manager на Caddy Proxy Manager. Причины: слой L4 из коробки, WAF на основе Coraza, гео-блокировки, forward-auth и mTLS. Но экосистема «панелей для Caddy» оказалась куда менее зрелой, чем у NPM, и выбирать пришлось вдумчиво.
Быстро появились первые странности.
Веб-интерфейс роутера. По IP-адресу вход работает. Через мой домен - нет. Я долго разбирал заголовки Host, Origin, X-Forwarded-*, пробовал разные варианты reverse_proxy со слиянием JSON. В итоге вывод был неприятный: роутер просто не поддерживает вход через произвольный обратный прокси. Штатные пути - собственное облако производителя или VPN. Не каждый баг про ваш прокси, иногда апстрим для такого не проектировали.
WebSocket у видеорегистратора. В NPM есть галочка «Websockets support». В Caddy её нет, и это нормально: reverse_proxy сам всё умеет. Но соединение всё равно падало. Виноваты оказались две мелочи:
- Caddy сопоставляет пути точно, а не по префиксу. Чтобы поймать всё вложенное, нужна звёздочка в конце;
- в настройках остался
Path Prefix Rewrite, доставшийся от другого сценария.
Рукопожатие WebSocket при этом проходит (ответ 101) и тут же обрывается. Это значит, что закрывает его сам сервер за прокси, скорее всего, придираясь к Host или Origin. Не прокси.
Невнятная ошибка при загрузке конфига. Caddy отказывался применять правила блокировщика с сообщением invalid CIDR "0.0.0.0". Оказалось, нужна маска: 0.0.0.0/0 и ::/0.
И напоследок важное правило: административный API Caddy на порту 2019 нельзя выставлять наружу. Это консоль управления всем прокси.
История 3. LLM-роутер и «работает из консоли, падает как сервис»
Третий проект - OmniRoute, свой OpenAI-совместимый роутер к языковым моделям. Я поставил его в контейнер Proxmox, причём через npm, а не через Docker: запускать Docker внутри LXC не рекомендуют.
Из консоли работало сразу. Как systemd-сервис - нет.
status=203/EXEC- systemd не нашёл бинарник. Он лежал не в/usr/bin, а внутри каталогаnvm.- Поправил путь - теперь
EADDRINUSE. Дело в том, что сервис уже работал, запущенный вручную ранее, и держал порт. - Рабочий unit получился, когда я явно прописал
PATHвнутри нужной версии Node.
[Service]
Environment=PATH=/root/.nvm/versions/node/<версия>/bin:/usr/local/bin:/usr/bin
ExecStart=/root/.nvm/versions/node/<версия>/bin/omnirouteУрок старый как мир: окружение интерактивной сессии не равно окружению сервиса. То, что видит ваша консоль, systemd не видит.
Два домена для одного сервиса
Самая полезная мысль этой истории пришла уже при публикации. У сервиса два типа клиентов.
- Человек открывает панель в браузере. Для него подходят Basic Auth или forward-auth.
- Программа (редактор кода, консольный клиент) ходит в API. Basic Auth её только сломает.
Поэтому я развёл их на два разных домена: «человеческий» с аутентификацией и «машинный» с проверкой ключа или списком разрешённых адресов. Так не приходится выбирать между удобством и защитой.
Что объединяет три истории
| Проект | Что казалось | Что оказалось |
|---|---|---|
| Excalidraw | Статика за nginx | Сборка зашита на облако автора |
| Keenetic за прокси | Проблема в заголовках | Апстрим не рассчитан на домен |
| OmniRoute | Просто запустить сервис | У systemd другой PATH |
Каждый раз ошибка была не в моём конфиге, а в скрытом допущении. Поэтому я теперь делаю три вещи, прежде чем ставить что-то «одной командой»:
- Читаю не только README, но и
.env.production, Dockerfile и список внешних адресов. - Проверяю, что не будет работать при моей схеме доступа (за прокси, за VPN, без интернета).
- Разделяю доступ для людей и программ.
Честная документация self-host-проекта должна говорить не только «вот docker-compose», но и «вот что в этой конфигурации не заработает».