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 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-09-08 23:16:04 +03:00
co-authored by Claude Opus 5
parent a9b7ccdd16
commit 62d5291cd9
6 changed files with 84 additions and 72 deletions
+3 -3
View File
@@ -1,8 +1,8 @@
# ---------------------------------------------------------------- Stoat ---- # ---------------------------------------------------------------- Stoat ----
# Публичный адрес API вашего инстанса (тот же, что в клиенте). # Публичный адрес API вашего инстанса (тот же, что в клиенте).
# Внутри docker-сети Stoat можно использовать http://api:14702 — но тогда # Бот ходит по нему как обычный клиент; домен резолвится в хост через
# убедитесь, что LiveKit URL из join_call резолвится изнутри контейнера. # extra_hosts в compose.yml, поэтому TLS валидный и NAT loopback не нужен.
STOAT_API_URL=https://stoat.example.com/api STOAT_API_URL=https://chat.example.com/api
# Токен бота: Settings → My Bots → создать бота → скопировать токен. # Токен бота: Settings → My Bots → создать бота → скопировать токен.
STOAT_BOT_TOKEN= STOAT_BOT_TOKEN=
+31 -28
View File
@@ -67,51 +67,39 @@ PUBLIC_URL=https://music.example.com
JWT_SECRET=<случайные 32 байта> JWT_SECRET=<случайные 32 байта>
``` ```
`STOAT_API_URL` лучше указывать публичный (`https://домен/api`): бот тогда ведёт себя как обычный `STOAT_API_URL` указывается публичный (`https://домен/api`): бот ведёт себя как обычный клиент —
клиент и получает от `join_call` тот же LiveKit-URL, что и все. Внутренний `http://api:14702` тоже ходит в API, gateway и LiveKit через тот же внешний прокси с валидным TLS, что и браузеры.
работает, но убедитесь, что LiveKit-URL из ответа резолвится изнутри контейнера.
### 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 ```yaml
services: extra_hosts:
caddy: - "chat.example.com:host-gateway"
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
``` ```
Сервис попадает в тот же compose-проект и ту же сеть, поэтому Caddy видит его как `http://mbot:3005`, Эта запись заставляет контейнер резолвить домен инстанса в сам хост, где внешний Caddy держит
а бот ходит в Stoat API по имени `api`. 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 ```caddyfile
http://music.example.com { music.example.com {
reverse_proxy http://mbot:3005 reverse_proxy 127.0.0.1:3005
} }
``` ```
Схема `http://` — потому что у вас TLS терминирует внешний прокси (Caddy слушает `127.0.0.1:8880`). Затем `sudo systemctl reload caddy`. WebSocket проксируется автоматически.
WebSocket проксируется автоматически.
### 6. Запустить ### 6. Запустить
```bash ```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`. В логах должно появиться `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). Что стоит знать: Все параметры — в [.env.example](.env.example). Что стоит знать:
+19
View File
@@ -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
+6 -13
View File
@@ -1,15 +1,8 @@
# Добавьте отдельным блоком в /opt/stoat/Caddyfile, рядом с {$HOSTNAME} { ... }. # Внешний Caddy на хосте: /etc/caddy/Caddyfile
# # Добавьте блок рядом с остальными вашими сайтами.
# У вас Caddy слушает только 80 (127.0.0.1:8880 → 80), а TLS терминирует music.example.com {
# внешний прокси на хосте, поэтому используем схему http:// — reverse_proxy 127.0.0.1:3005
# так Caddy не будет пытаться сам выпускать сертификат.
http://music.example.com {
reverse_proxy http://mbot:3005
} }
# Если внешнего прокси нет и Caddy сам держит 80/443 — просто: # Внутренний Caddy инстанса трогать не нужно: панель к нему не относится.
# music.example.com { # WebSocket (/ws) Caddy апгрейдит сам, отдельных директив не требуется.
# reverse_proxy http://mbot:3005
# }
#
# WebSocket (/ws) проксируется автоматически, отдельных директив не нужно.
-28
View File
@@ -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"
+25
View File
@@ -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