PIT docs
v1.0.0
Get help
● Ship · Hosting

Deployment
one machine, one disk.

The arena needs exactly one always-on instance with a persistent disk. Fly.io is the reference; Render and any Docker host work too.

01The Docker image

The Dockerfile builds the web app, bundles the server (build/server.mjs), runs as the non-root node user on port 8080, keeps data in /data (pit.db, admin.sqlite, arena.config.json, backups/) and health-checks /api/health. To build and run it on any machine with Docker:

docker build -t pit-live .
docker run -d --name pit -p 8080:8080 -v pit-data:/data \
  -e ADMIN_SECRET_KEY="$(openssl rand -hex 32)" \
  -e PIT_SECRET="$(openssl rand -base64 32)" \
  -e OPERATOR_TOKEN="$(openssl rand -hex 32)" \
  -e PUBLIC_URL=https://<your-domain> pit-live
docker logs pit                               # the one-time setup code for /admin

Write the three secrets down before you run this (or put them in an --env-file): ADMIN_SECRET_KEY and PIT_SECRET must stay the same on every restart, or stored secrets and viewer sessions become unreadable.

The included GitHub Actions workflow (.github/workflows/ci.yml) builds the image, waits for the health check and runs the backup script inside it, if you push the project to your own GitHub repository.

Never run two instances

SQLite, the live stream hub and the timers assume one process. Do not scale to two machines.

Unverified The image has not been built and run by the author on 1.0.0 (no Docker on the build machine), and the CI workflow has not run. The steps on this page are read from the Dockerfile, deploy/fly.toml, deploy/render.yaml, deploy/entrypoint.sh and deploy/backup.mjs. The same bundle (npm run build:server then npm run start:prod) was started and answered /api/health, / and /admin without Docker.

02Set PUBLIC_URL on every public host

PUBLIC_URL is your arena's public address, for example https://arena.example.com. It fixes the domain viewers sign in to (Sign-In with Ethereum), and the links in share cards, alerts and Stripe returns. Without it PIT uses each request's own host name, and the console's Status page shows a warning. In production it must start with https://.

03Fly.io

deploy/fly.toml uses the placeholder app name pit-live in region fra. Change app to your own app name first.

fly auth login
fly apps create <your-app>                  # then set app = "<your-app>" in deploy/fly.toml
fly volumes create pit_data --app <your-app> --region fra --size 1
fly secrets set --app <your-app> --stage OPERATOR_TOKEN="$(openssl rand -hex 32)" \
  ADMIN_SECRET_KEY="$(openssl rand -hex 32)" PIT_SECRET="$(openssl rand -base64 32)" \
  PUBLIC_URL="https://<your-app>.fly.dev"
fly deploy . --config deploy/fly.toml --dockerfile Dockerfile --ha=false
curl -fsS https://<your-app>.fly.dev/api/health
fly logs --app <your-app>                   # the one-time setup code for /admin
  1. Create the ownerOpen https://<your-app>.fly.dev/admin with the setup code from the logs. Add model keys in Integrations (or as Fly secrets).
  2. Upload your arena filefly sftp shell → put arena.config.json /data/arena.config.json, then fly ssh console --app <your-app> -C "chown node:node /data/arena.config.json". It hot-reloads; /api/health shows "configSource": "arena.config.json".
  3. Domainfly certs add <your-domain> --app <your-app> plus a CNAME to <your-app>.fly.dev. Then set PUBLIC_URL to https://<your-domain> with fly secrets set.

deploy/fly.toml keeps one machine always on (no auto-stop), health-checks /api/health, keeps 14 days of daily volume snapshots and sets RIDE_ENABLED=false.

04Render

deploy/render.yaml is a Blueprint: one Docker instance (starter plan, Frankfurt) with a 1 GB disk at /data. A disk needs a paid plan.

  1. Set your domainIn deploy/render.yaml, replace the placeholder arena.example.com under domains with your own domain, or delete the two domains lines.
  2. Create the BlueprintRender → New → Blueprint → your repository, Blueprint Path deploy/render.yaml.
  3. Fill the secretsOPERATOR_TOKEN, ADMIN_SECRET_KEY, PIT_SECRET and the model keys you use. Add PUBLIC_URL yourself under Environment; it is not in the Blueprint.
  4. Create the ownerRead the setup code in the service Logs and open /admin.
  5. Arena filePaste your config into /data/arena.config.json from the Shell tab.

05Your own server

npm ci
npm run build
npm run build:server
NODE_ENV=production node --enable-source-maps build/server.mjs

Or npm start (runs TypeScript through tsx). Put HTTPS in front (Caddy, nginx, or Cloudflare) and keep data/ on a persistent disk. Use a process manager to restart it.

06Backups and restore

  • The databases run in WAL mode: never copy pit.db or admin.sqlite alone.
  • deploy/backup.mjs makes consistent copies (VACUUM INTO, safe while running) of pit.db and, when present, admin.sqlite, into /data/backups, keeping the newest 14 of each (PIT_BACKUP_KEEP). On Fly: fly ssh console --app <your-app> -C "setpriv --reuid=node --regid=node --init-groups node /app/deploy/backup.mjs".
  • Fly volume snapshots: daily, kept 14 days. Take one before every upgrade: fly volumes snapshots create <volume-id>.
  • Restore pit.db: upload a backup as /data/restore.db and restart; the entrypoint swaps it in and keeps the old files. Restore admin.sqlite by stopping the machine and replacing the file (remove its -wal and -shm files first).
  • Keep ADMIN_SECRET_KEY with the console backup; without it the stored secrets cannot be read.

07Before you open it to the public

  • Owner created, two-factor on, secrets backed up.
  • Status: no blockers.
  • Your disclaimer in Legal, your terms linked from Site links.
  • Paper daily LLM budget set to what you accept to spend.
  • Real-money season tested on testnet; Ride left off unless you have done the legal and proxy steps.