authentik/vanguard-profile-scope-mapping.py (new, and the first time that mapping is version-controlled rather than pasted from the docs) emits the Discord snowflake from the account's source connection alongside the inherited groups, so the bot never has to match a name. docker-compose.yml: discord-bot moves to its own network. It trusts the identity headers Caddy injects, and it shared the default bridge with jellyfin, copyparty, syncthing, gitea, act_runner and cadvisor, any of which could reach :8000 directly and forge them. It needs no sibling container: Caddy uses the published loopback port and MariaDB is reached through host.docker.internal. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
discord-bot — for the bot dev
Start with AGENT.md in this folder: the stack, the rules, the layout, the web-UI auth contract.
How code reaches the server
Your repo's Gitea Actions workflow builds a release bundle: one tar.gz holding main.py, app/,
website/, version.json, BUILD.json and vendor/ (your dependencies as prebuilt wheels). A push to
main installs itself: the workflow's last job posts the bundle to the server's deploy service, which
checks its signature and every checksum, swaps it into /srv/bot/app (bind-mounted at /app, previous
release kept for --rollback) and restarts the container. The install log comes back in the CI job.
A branch or pull request builds the artifact and deploys nothing. The test jobs are commented out in the
workflow — run python -m pytest -q and node --test app/tests/*.test.js locally before pushing. CI still
refuses a bundle that cannot import from its own vendor/, or whose files do not match BUILD.json, and the
deploy fails if the bot does not answer on its port after the swap.
Your secrets live in Gitea
Set them on this repo under Settings → Actions. The deploy job sends them with the bundle, signed, and the server writes the ones it allows into the environment the container gets:
| where | name | what |
|---|---|---|
| Secrets | BOT_DEPLOY_SECRET |
signs the upload; must match the server (the admin has it) |
| Secrets | DISCORD_BOT_TOKEN |
the bot token |
| Variables | DISCORD_GUILD_ID |
the Discord server id |
Rotating the token is an edit here plus a re-run of the workflow; nobody needs to touch the server. The
server keeps what it generates, chiefly DB_PASSWORD, and accepts only the names its BOT_ENV_KEYS lists —
anything else is reported in the job as settings_refused. Need a new setting? Ask the admin to add its name
to that list, then add it here.
The image contains no bot dependencies: the container imports yours from /app/vendor
(PYTHONPATH=/app/vendor). Adding a dependency means editing app/requirements.txt and pushing — no image
rebuild, no package index on the server. The image only changes if you need a new system package (ffmpeg and
curl are already there); ask the admin for that.
Day to day (on the server, in ~/vanguard-stream)
./deployBotBundle.sh vanguard-bot-bundle.zip # install a release by hand (zip or tar.gz)
./deployBotBundle.sh --status # what is installed; container state
./deployBotBundle.sh --rollback # previous release
docker compose logs -f discord-bot # live log
docker compose restart discord-bot # restart without changing code
docker compose exec discord-bot python # a REPL inside the container (your vendor/ is on sys.path)
Environment the container gives you
| var | value |
|---|---|
DISCORD_BOT_TOKEN |
from .env (stack root) — the Bot added to the existing Authentik Discord application |
DISCORD_GUILD_ID |
the server |
DB_BACKEND |
mariadb |
DB_HOST / DB_PORT |
host.docker.internal / 3306 (MariaDB runs on the host network) |
DB_NAME / DB_USER / DB_PASSWORD |
your own database + user (vanguardbot / bot / generated) |
DATA_DIR |
/data — persistent, owned by you |
MEDIA_CLIPS |
/media/clips — rw mount of /srv/media/clips (folder per user, for clip submissions) |
WEB_HOST / WEB_PORT |
0.0.0.0 / 8000 — published as https://bot.<domain>/ via Caddy + Authentik |
WEB_PUBLIC_URL |
https://bot.<domain> for absolute links |
WEB_SERVE_UI |
true — serve your pages at / on the same port as /api |
WEB_ALLOWED_GROUPS |
bot-users,admins — the groups your own code must accept |
AUTH_HEADER_USER / _GROUPS / _EMAIL |
X-Authentik-Username / X-Authentik-Groups / X-Authentik-Email — identity injected by Caddy, trust these, implement no login |
PYTHONPATH |
/app/vendor |
TZ |
America/New_York |
SQLAlchemy URL: mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}
async: mysql+aiomysql://...
Database from your own machine
MariaDB is reachable directly: media.dumpstervigilantes.rip:3306, user bot, database vanguardbot,
password from the admin (./configure.py --show on the server). Root is local-only. New connections are
rate limited at the firewall; if yours get dropped, ask the admin to add your IP to db_allow.
Conventions
- Run as uid 1000 (files you write under
/media/clipsare owned like everything else there). /appholds a released bundle; treat it as read-only and write to/datainstead.- If you write clips:
/media/clips/<discord-username>/<name>.mp4plus<name>.jsonwith{"timestamp": "<UTC ISO-8601>", "source": "medal", "source_url": "...", "uploader": "...", "title": "..."}— the Jellyfin plugin reads that sidecar for the exact recording time. - Log to stdout; the container captures it. One event per line,
key=value, keeps it searchable (LOGGING.md).