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.
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
- Create the ownerOpen
https://<your-app>.fly.dev/adminwith the setup code from the logs. Add model keys in Integrations (or as Fly secrets). - Upload your arena file
fly sftp shell→put arena.config.json /data/arena.config.json, thenfly ssh console --app <your-app> -C "chown node:node /data/arena.config.json". It hot-reloads;/api/healthshows"configSource": "arena.config.json". - Domain
fly certs add <your-domain> --app <your-app>plus a CNAME to<your-app>.fly.dev. Then setPUBLIC_URLtohttps://<your-domain>withfly 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.
- Set your domainIn
deploy/render.yaml, replace the placeholderarena.example.comunderdomainswith your own domain, or delete the twodomainslines. - Create the BlueprintRender → New → Blueprint → your repository, Blueprint Path
deploy/render.yaml. - Fill the secrets
OPERATOR_TOKEN,ADMIN_SECRET_KEY,PIT_SECRETand the model keys you use. AddPUBLIC_URLyourself under Environment; it is not in the Blueprint. - Create the ownerRead the setup code in the service Logs and open
/admin. - Arena filePaste your config into
/data/arena.config.jsonfrom 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.dboradmin.sqlitealone. deploy/backup.mjsmakes consistent copies (VACUUM INTO, safe while running) ofpit.dband, 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.dband restart; the entrypoint swaps it in and keeps the old files. Restoreadmin.sqliteby stopping the machine and replacing the file (remove its-waland-shmfiles first). - Keep
ADMIN_SECRET_KEYwith 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.