Quake III Arena in the browser, fully self-hosted — no dependency on content.quakejs.com. This first release turns the bare QuakeJS server into a complete, operable product: a web admin console, a public front page, your own retail assets, and a hardened, self-deploying image built for amd64 and arm64.
Play
- Front page at
/— a lobby showing the server name, the current map with its picture, who's on, and the leaderboard, with a Join button. The game itself is at/play.html. - Name in game — type a name in Play as (or in the console) and it's the name you appear under; it's remembered in your browser.
- One published port — the page, the game's websocket traffic, the assets, and the console all share port 80, so a single port-forward or reverse-proxy host is all a public server needs. HTTPS/
wssis supported end to end.
Admin console (/admin/, or press ` in game)
A web console that drives the dedicated server live, organised in tabs:
- Chat — a messenger threading public chat and private messages; set your in-game name here.
- Stats — a leaderboard built from the engine's own game log; survives restarts, forgets names unseen for 90 days.
- Server — current map with levelshot, switch maps (filtered to the gametype), restart, broadcast, and Backup (export/import + automatic daily backups).
- Match — one-click presets (casual FFA, duel, TDM, CTF, party) plus gametype, limits, slots, mutators, server name and MOTD; values are validated and persisted across restarts.
- Players — who's connected with their address, kick, move teams, add bots (as many as fit — see below), and timed bans with a reason.
- Maps — upload a
.pk3/.zipor your own retail paks, install from ..::LvL by id, and build the map rotation from presets. - Log — the live server log, an audit trail of every admin action, and crash notes.
The console opens in a public messenger mode anyone can use; Admin login unlocks the rest. Sign-in has a lockout keyed on the player's real address.
Your own Quake 3 assets
- Nothing licensed ships in the image — it carries only the freely redistributable demo content and point-release paks.
- Supply your own
pak0.pk3by uploading it in the console or pointingEXTRA_PAKSat a URL you control. The full game — all 31 stock maps, every bot, retail textures — then appears. - Slimming tool (
tools/add-retail.py) trims music and cinematics (~62% ofpak0, which a browser never plays), taking it from 457 MB to ~175 MB so it loads reliably in the browser client.
Moderation & multiplayer
- Real client addresses — players' addresses are read from
X-Forwarded-Forthrough your reverse proxy (TRUSTED_PROXIES), so kicks and bans land on the player rather than the proxy. - Timed bans with a reason — an hour to thirty days, or permanent; a timed ban lifts itself, and every ban is re-applied if the game server restarts (the game module forgets them otherwise).
- Bots up to the measured ceiling — the Add button offers as many as actually fit: free client slots or the game module's memory limit (24 bots — its allocator fails at 26–27, measured), whichever is lower, shown live.
- Player votes, a leaderboard, and an optional Discord/Slack webhook that pings when someone starts playing on an empty server.
Operations
- No root at runtime — the entrypoint hands the volumes to an unprivileged
quakeuser and drops privileges; Apache binds port 80 through a file capability, not as root. - No baked-in password — one is generated on first start and printed once to the log (
docker logs … | grep generated); setADMIN_PASSWORDto choose your own, or empty to disable the console. - Persistent state — settings, rotation, leaderboard, bans, audit trail, crash notes and installed maps live in named volumes and survive redeploys.
- Backups — export/import the whole state as one file from the console, plus an automatic daily backup (seven kept).
- Log rotation — Apache logs to the container output (Docker rotation applies);
games.logand the console log are capped and rotated. - Healthcheck covers the page, the game port, the crash marker, and the console — a dead console no longer reads as healthy, and the entrypoint restarts it.
- Audit trail & crash notes — every admin action and sign-in is recorded with its address; a crash keeps the last 200 log lines with the map and bot count.
Security
- Request-body size cap and per-read timeout, checked before authentication.
- Sign-in lockout keyed on the real client address;
Authorization: Basiccounts against it. Secure+HttpOnly+SameSitecookies behind TLS; a strict Content-Security-Policy on the console (nothing inline, script or style);nosniffandframe-ancestorson every page; no version banners.- Private messages are scoped to their sender and to admins.
- SSRF guard on the map-download path — every fetch and every redirect must resolve to a public address, blocking loopback, private ranges, and the cloud-metadata endpoint.
- Console passwords hashed with PBKDF2-HMAC-SHA256 at 600,000 rounds; older hashes upgrade on next sign-in.
Under the hood
- The console is a small, tested Python package (
admin/qadmin/), one module per concern, with a single settings spec shared with the config builder. - Unit and smoke tests run on the host, at image build, and inside the built image — a regression can't produce a published image.
- Multi-arch, signed images — built for
linux/amd64andlinux/arm64from digest- and checksum-pinned inputs, and signed with cosign in CI. - Install and user guide in the README, with screenshots of every console tab.
Image
Published to Docker Hub for amd64 and arm64:
docker run -d --name quakejs -p 8080:80 \
-v quakejs-assets:/var/www/html/assets \
-v quakejs-state:/var/lib/quakejs \
treyyoder/quakejs:latestVerify the signature:
cosign verify treyyoder/quakejs:latest \
--certificate-identity-regexp 'https://github.com/treyyoder/quakejs-docker/' \
--certificate-oidc-issuer https://token.actions.githubusercontent.comSee the README for the full install and user guide.