Deployment
Open Model Arena is a small FastAPI + vanilla-JS app in a single Docker container. Data lives in SQLite (bind-mounted volume). It expects to run inside a trusted network with an HTTPS reverse proxy in front.
Docker (recommended)
Section titled “Docker (recommended)”# docker-compose.yml (ships with the repo)services: model-arena: image: ghcr.io/pete-builds/open-model-arena:latest container_name: model-arena restart: unless-stopped ports: - "3694:3694" volumes: - arena-data:/app/data - ./models.yaml:/app/models.yaml:ro environment: - ARENA_PASSPHRASE=${ARENA_PASSPHRASE} - AUTH_TOKEN_SECRET=${AUTH_TOKEN_SECRET} - GATEWAY_API_KEY=${GATEWAY_API_KEY} - ARENA_API_TOKENS=${ARENA_API_TOKENS:-} - TZ=${TZ:-America/New_York}
volumes: arena-data:docker compose up -d and you’re live at http://localhost:3694.
Auth cookies use Secure=True, which requires HTTPS for anything other
than localhost. Terminate TLS at any proxy and route to the container.
arena.example.com { reverse_proxy localhost:3694}That’s the whole config — Caddy handles Let’s Encrypt automatically.
server { listen 443 ssl http2; server_name arena.example.com;
ssl_certificate /etc/letsencrypt/live/arena.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/arena.example.com/privkey.pem;
# SSE streams need buffering off + long timeouts. proxy_buffering off; proxy_read_timeout 300s;
location / { proxy_pass http://127.0.0.1:3694; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; }}The X-Forwarded-For header feeds the rate limiter — set it once, from
the immediate proxy only. Don’t append.
Cloudflare Tunnel
Section titled “Cloudflare Tunnel”tunnel: <your-tunnel-id>credentials-file: /etc/cloudflared/<tunnel-id>.jsoningress: - hostname: arena.example.com service: http://localhost:3694 - service: http_status:404Cloudflare terminates TLS at the edge and speaks HTTPS to Arena over the Tunnel; you don’t need to expose a port at all.
Tailscale Funnel
Section titled “Tailscale Funnel”tailscale serve --https=443 --set-path=/ http://localhost:3694tailscale funnel 443 onSame idea, over a Tailnet — great for “friends and coworkers” style sharing without exposing anything to the public internet.
Environment variables
Section titled “Environment variables”| Variable | Required | Purpose |
|---|---|---|
ARENA_PASSPHRASE |
Yes | Shared secret for the browser login |
AUTH_TOKEN_SECRET |
Yes | Signs the auth cookie (openssl rand -hex 32) |
ARENA_API_TOKENS |
No | Comma-separated bearer tokens for /api/* |
GATEWAY_API_KEY |
No | Any provider API key referenced via api_key_env |
TZ |
No | Timezone for the “battles today” stat |
Refuses to start if ARENA_PASSPHRASE or AUTH_TOKEN_SECRET are missing.
- SQLite database in the bind-mounted
/app/datavolume. WAL mode is on, so reads (SSE streams) don’t block writes (votes). - Back it up like any other SQLite database:
sqlite3 arena.db ".backup 'arena.$(date +%F).db'". - Multi-server deployments: not supported yet. If you need horizontal scale, run one arena per team and aggregate at the leaderboard level yourself. See SHOWCASE-PLAN.md.
Container image
Section titled “Container image”Multi-arch images published to GHCR on every v*.*.* tag:
ghcr.io/pete-builds/open-model-arena:latestghcr.io/pete-builds/open-model-arena:vX.Y.Zghcr.io/pete-builds/open-model-arena:X.Y.ZBoth linux/amd64 and linux/arm64 build on every release.