cat ~/projects/haggan-cs2/README.md
haggan-cs2
Public CS2 servers (1v1 arenas and deathmatch) with persistent rankings and a web app in Rust, run on my own hardware behind a VPS edge.
- stack
- Rust, axum, MariaDB, Docker, Traefik, Cloudflare
- live
- haggan.net
What it is
Counter-Strike 2 servers open to anyone: 1v1 arenas and a deathmatch server. Players get an ELO-style ranking, weapon preferences that persist between sessions and a leaderboard on the web.
How it’s built
- The game server runs in a container on a machine at home. Players connect through a VPS, so no ports are opened on the home connection.
- The web app is a small Rust (axum) binary in a distroless container. It reads rankings from MariaDB and signs players in with Steam.
- The VPS terminates TLS behind Cloudflare and only accepts traffic from Cloudflare. Rate limits are set per route type, and endpoints meant only for the game server are blocked publicly.
Things I had to solve
- Locking down egress. The game container can reach the internet and the few internal services it needs, and nothing else. A verification script checks 50 allowed and denied flows, so the rules can’t drift without me noticing.
- Updates that silently fail. Valve ships updates without notice, and the updater could report “already up to date” on a stale build. That got the server delisted. An update check now compares the running build with Steam’s current one and updates only when nobody is playing.
- Patching an upstream image safely. The base image does a few things I don’t want, like echoing secrets to logs. Instead of forking it, a patch script applies guarded edits that fail the build if upstream changes underneath them.
What I’d do differently
Start with the egress rules and monitoring, not finish with them. Everything I added late was harder to verify than what I designed in from the start.