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
+31 -28
View File
@@ -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). Что стоит знать: