From 62d5291cd98377539ccee5fc179655193f1ecbbf Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Tue, 8 Sep 2026 23:15:34 +0300 Subject: [PATCH] Run the panel as its own compose project behind the host Caddy The instance's internal Caddy is only for Stoat itself, so the panel no longer goes through it: the bot ships its own compose project, publishes 3005 on loopback, and the host's external Caddy gives it a domain. extra_hosts pins the instance domain to host-gateway, so the bot reaches the API, gateway and LiveKit through the external proxy with a valid certificate instead of depending on router NAT loopback. The old in-project layout stays available as a fallback example, together with a step-by-step guide for when voice fails to connect. Co-Authored-By: Claude Opus 5 --- .env.example | 6 +-- README.md | 59 +++++++++++++----------- compose.yml | 19 ++++++++ deploy/Caddyfile.snippet | 19 +++----- deploy/compose.override.yml.example | 28 ----------- deploy/compose.stoat-network.yml.example | 25 ++++++++++ 6 files changed, 84 insertions(+), 72 deletions(-) create mode 100644 compose.yml delete mode 100644 deploy/compose.override.yml.example create mode 100644 deploy/compose.stoat-network.yml.example diff --git a/.env.example b/.env.example index e72cb50..5ead378 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,8 @@ # ---------------------------------------------------------------- Stoat ---- # Публичный адрес API вашего инстанса (тот же, что в клиенте). -# Внутри docker-сети Stoat можно использовать http://api:14702 — но тогда -# убедитесь, что LiveKit URL из join_call резолвится изнутри контейнера. -STOAT_API_URL=https://stoat.example.com/api +# Бот ходит по нему как обычный клиент; домен резолвится в хост через +# extra_hosts в compose.yml, поэтому TLS валидный и NAT loopback не нужен. +STOAT_API_URL=https://chat.example.com/api # Токен бота: Settings → My Bots → создать бота → скопировать токен. STOAT_BOT_TOKEN= diff --git a/README.md b/README.md index f23ef25..b4b9b0d 100644 --- a/README.md +++ b/README.md @@ -67,51 +67,39 @@ PUBLIC_URL=https://music.example.com JWT_SECRET=<случайные 32 байта> ``` -`STOAT_API_URL` лучше указывать публичный (`https://домен/api`): бот тогда ведёт себя как обычный -клиент и получает от `join_call` тот же LiveKit-URL, что и все. Внутренний `http://api:14702` тоже -работает, но убедитесь, что LiveKit-URL из ответа резолвится изнутри контейнера. +`STOAT_API_URL` указывается публичный (`https://домен/api`): бот ведёт себя как обычный клиент — +ходит в API, gateway и LiveKit через тот же внешний прокси с валидным TLS, что и браузеры. -### 4. Добавить сервис в `compose.override.yml` +### 4. Указать домен инстанса в `compose.yml` -В `/opt/stoat/compose.override.yml` (готовый пример — [deploy/compose.override.yml.example](deploy/compose.override.yml.example)): +Бот запускается **отдельным compose-проектом** и с внутренним Caddy инстанса никак не связан. +В [compose.yml](compose.yml) поправьте одну строку — домен вашего Stoat: ```yaml -services: - caddy: - ports: !override - - "127.0.0.1:8880:80" - - mbot: - build: ../stoat-mbot - restart: always - env_file: ../stoat-mbot/.env - depends_on: - api: - condition: service_started - volumes: - - ../stoat-mbot/data:/data + extra_hosts: + - "chat.example.com:host-gateway" ``` -Сервис попадает в тот же compose-проект и ту же сеть, поэтому Caddy видит его как `http://mbot:3005`, -а бот ходит в Stoat API по имени `api`. +Эта запись заставляет контейнер резолвить домен инстанса в сам хост, где внешний Caddy держит +443 с валидным сертификатом. Так бот не зависит от NAT loopback на роутере. -### 5. Пробросить панель через Caddy +### 5. Повесить панель на внешний Caddy -В `/opt/stoat/Caddyfile` добавьте блок ([deploy/Caddyfile.snippet](deploy/Caddyfile.snippet)): +Порт панели публикуется только на локальный интерфейс (`127.0.0.1:3005`), домен выдаёт внешний +Caddy хоста — в `/etc/caddy/Caddyfile` ([deploy/Caddyfile.snippet](deploy/Caddyfile.snippet)): ```caddyfile -http://music.example.com { - reverse_proxy http://mbot:3005 +music.example.com { + reverse_proxy 127.0.0.1:3005 } ``` -Схема `http://` — потому что у вас TLS терминирует внешний прокси (Caddy слушает `127.0.0.1:8880`). -WebSocket проксируется автоматически. +Затем `sudo systemctl reload caddy`. WebSocket проксируется автоматически. ### 6. Запустить ```bash -cd /opt/stoat && docker compose up -d --build mbot && docker compose logs -f mbot +cd /opt/stoat-mbot && docker compose up -d --build && docker compose logs -f ``` В логах должно появиться `yt-dlp detected`, `bot is ready` и `panel is listening`. @@ -142,6 +130,21 @@ cd /opt/stoat && docker compose up -d --build mbot && docker compose logs -f mbo --- +## Если бот не заходит в голосовой канал + +Порядок диагностики — сверху вниз, каждый шаг отсекает свой слой: + +1. **`bot is ready` в логах.** Нет — проблема в `STOAT_API_URL`/`STOAT_BOT_TOKEN`. Проверьте, что + домен из `extra_hosts` совпадает с доменом в `STOAT_API_URL`, и что изнутри контейнера он + резолвится в хост: `docker compose exec mbot node -e "fetch(process.env.STOAT_API_URL).then(r=>console.log(r.status))"`. +2. **`joining voice channel`, но нет `voice connection established`.** Значит `join_call` отдал + токен, а WebSocket до LiveKit не поднялся — смотрите, доступен ли `/livekit` через внешний домен. +3. **Бот в канале, но звука нет.** Это уже медиа-трафик: LiveKit анонсирует клиентам свой адрес + из `rtc.node_ip` / `use_external_ip` в `/opt/stoat/livekit.yml` и ждёт UDP на 50000-50100. + Если анонсируется внешний IP, а роутер не умеет NAT loopback, пакеты от контейнера до него не + дойдут. Тогда либо включите hairpin на роутере, либо запустите бота внутри compose-проекта + Stoat — пример в [deploy/compose.stoat-network.yml.example](deploy/compose.stoat-network.yml.example). + ## Настройки Все параметры — в [.env.example](.env.example). Что стоит знать: diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..b37690a --- /dev/null +++ b/compose.yml @@ -0,0 +1,19 @@ +name: stoat-mbot + +services: + mbot: + build: . + restart: always + env_file: .env + ports: + # Наружу не торчит: домен вешается на внешний Caddy хоста. + - "127.0.0.1:3005:3005" + extra_hosts: + # Домен Stoat резолвится в хост, где внешний Caddy держит валидный TLS. + # Так бот ходит в API, gateway и LiveKit ровно как обычный клиент, + # без зависимости от NAT loopback на роутере. + # Замените на свой домен: + - "chat.example.com:host-gateway" + volumes: + # cookies.txt для yt-dlp и/или локальная медиатека + - ./data:/data diff --git a/deploy/Caddyfile.snippet b/deploy/Caddyfile.snippet index 4adff01..9cb93ea 100644 --- a/deploy/Caddyfile.snippet +++ b/deploy/Caddyfile.snippet @@ -1,15 +1,8 @@ -# Добавьте отдельным блоком в /opt/stoat/Caddyfile, рядом с {$HOSTNAME} { ... }. -# -# У вас Caddy слушает только 80 (127.0.0.1:8880 → 80), а TLS терминирует -# внешний прокси на хосте, поэтому используем схему http:// — -# так Caddy не будет пытаться сам выпускать сертификат. -http://music.example.com { - reverse_proxy http://mbot:3005 +# Внешний Caddy на хосте: /etc/caddy/Caddyfile +# Добавьте блок рядом с остальными вашими сайтами. +music.example.com { + reverse_proxy 127.0.0.1:3005 } -# Если внешнего прокси нет и Caddy сам держит 80/443 — просто: -# music.example.com { -# reverse_proxy http://mbot:3005 -# } -# -# WebSocket (/ws) проксируется автоматически, отдельных директив не нужно. +# Внутренний Caddy инстанса трогать не нужно: панель к нему не относится. +# WebSocket (/ws) Caddy апгрейдит сам, отдельных директив не требуется. diff --git a/deploy/compose.override.yml.example b/deploy/compose.override.yml.example deleted file mode 100644 index a05aafd..0000000 --- a/deploy/compose.override.yml.example +++ /dev/null @@ -1,28 +0,0 @@ -# /opt/stoat/compose.override.yml -# -# Раскладка: -# /opt/stoat — ваш self-hosted Stoat (compose.yml, Caddyfile, ...) -# /opt/stoat-mbot — этот репозиторий -# -# Сервис живёт в том же compose-проекте, поэтому попадает в сеть stoat_default: -# Caddy видит его как http://mbot:3005, а бот — Stoat API как http://api:14702. -services: - # ваш существующий override для Caddy — оставьте как есть - caddy: - ports: !override - - "127.0.0.1:8880:80" - - mbot: - build: ../stoat-mbot - restart: always - env_file: ../stoat-mbot/.env - depends_on: - api: - condition: service_started - volumes: - # cookies.txt для yt-dlp и/или локальная медиатека - - ../stoat-mbot/data:/data - # Панель отдаётся через Caddy. Порт наружу нужен, только если панель - # проксирует внешний nginx на хосте, минуя Caddy: - # ports: - # - "127.0.0.1:3005:3005" diff --git a/deploy/compose.stoat-network.yml.example b/deploy/compose.stoat-network.yml.example new file mode 100644 index 0000000..6113bbe --- /dev/null +++ b/deploy/compose.stoat-network.yml.example @@ -0,0 +1,25 @@ +# Запасной вариант: если по какой-то причине бот должен ходить в Stoat +# по внутренним именам сервисов, а не через внешний домен. +# +# Кладётся в /opt/stoat/compose.override.yml (в дополнение к вашему caddy-блоку), +# при этом в /opt/stoat-mbot/.env нужно указать STOAT_API_URL=http://api:14702. +# +# Минус: LiveKit-URL из join_call всё равно приходит публичным, и его придётся +# резолвить изнутри сети — поэтому по умолчанию рекомендуется отдельный +# compose-проект (см. compose.yml в корне репозитория). +services: + caddy: + ports: !override + - "127.0.0.1:8880:80" + + mbot: + build: ../stoat-mbot + restart: always + env_file: ../stoat-mbot/.env + depends_on: + api: + condition: service_started + ports: + - "127.0.0.1:3005:3005" + volumes: + - ../stoat-mbot/data:/data