Skip to content

Requirements and first run

The community that uses a judgebot runs it, with its own Discord application, model key and spend cap. A complete instance is three containers from one compose file: db (Postgres with pgvector), api (the web page and HTTP API) and bot (Discord). A fourth, refresh, runs nightly from cron to keep cards and rules current. An optional cloudflared publishes the API without an open port. You can stop after api and never touch Discord. The bot is the last thing to add.

  • A host that stays on, with Docker and the compose plugin. Running takes about 200 MB of RAM across the three containers. Building the image takes ~4 GB and a lot of CPU. A low-powered host pulls the CI-built image instead (docker compose pull).
  • No Rust. The image carries judge-ingest as its refresh service, so the data loads run in a container. (Working from source is Development setup. A binary run on the host verifies HTTPS against the system’s CA certificates, ca-certificates on Debian and Ubuntu, and without them it refuses to start.)
  • A model provider. With .env alone that is Anthropic’s API (ANTHROPIC_API_KEY). A judge.toml chooses anything else.
  • Optionally a Voyage AI key for the semantic-search leg, or an embedding model on an OpenAI-compatible server. Without one the bot runs on the category map and full-text legs and logs a warning at startup.
  • For Discord, an application of your own and its bot token. Create the Discord app walks through the developer portal with links into Discord’s documentation.
  1. cp .env.example .env and fill in the keys. Every knob is documented in the file and in the configuration reference. Two are contacts, not keys: the bot requires JUDGE_OPERATOR_DISCORD (your Discord username) and judge-api requires JUDGE_OPERATOR_EMAIL (a support address).
  2. docker compose up -d db. Postgres publishes on localhost:5432 (loopback only). If something on the host already has that port, set DB_PORT in .env and change the port in DATABASE_URL to match.
  3. docker compose run --rm refresh init loads everything: the schema, the cards, the current rules, the alias and note lists, embeddings if you have an embedder, and the emoji if DISCORD_TOKEN is already set. The first embed pays the embedder once for every rule and glossary entry, a few cents on Voyage. Afterwards only changed rules are re-embedded. init is safe to run again.
  4. docker compose up -d api. Open http://localhost:8787 and ask a question. This runs the full pipeline, so it is a good place to check the model setup before Discord is involved. api and bot both apply pending migrations at startup unless JUDGE_AUTO_MIGRATE=false.
  5. Create the Discord app, then docker compose up -d bot. Then run docker compose run --rm refresh emoji once, so answers show mana symbols as pictures instead of {W}.
  6. Schedule scripts/refresh-data.sh nightly and scripts/backup-db.sh weekly. The deployment runbook has the cron lines.

init is these, in this order, and each can be run by itself:

Terminal window
docker compose run --rm refresh migrate
docker compose run --rm refresh cards
docker compose run --rm refresh rules latest
docker compose run --rm refresh aliases # the list built into the binary
docker compose run --rm refresh notes
docker compose run --rm refresh embed # with an embedder configured
docker compose run --rm refresh emoji # once the Discord app exists

aliases and notes take a file instead when you keep your own list: mount it and name it, from the repository root. Each replaces its table, and so does init with the built-in lists, so load your own after any init.

Terminal window
docker compose run --rm -v ./data:/data:ro refresh aliases /data/aliases.yaml

The schema is created with vector(1024) columns, Voyage’s width. If your embedder produces another width (OpenAI’s text-embedding-3-small is 1536), init handles it: on a database that holds no vectors yet it takes the configured embedder’s width. Loading step by step, run judge-ingest reembed --yes instead of embed the first time. It retypes the columns and records the embedding space before filling them. The bot refuses to mix two spaces. On a mismatch it logs an error naming both and runs with the vector leg dark.

The deployment runbook publishes api through a Cloudflare Tunnel with edge rate limiting in front of the anonymous page. Any reverse proxy works. Keep API_CLIENT_IP=peer unless Cloudflare is the only route to the origin. The security page gives the reason.