Self-hostable game server control panel: Go controller + agent, 26 game modules, embedded web UI. One-line install via install.sh / install.ps1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
6.8 KiB
Architecture
How panel actually fits together. (An earlier revision of this document described the original design — SvelteKit, SQLite, Casbin, SFTP; none of that shipped. This is the real stack.)
Topology
┌───────────────────────────────┐ ┌────────────────────────────┐
│ Controller │ gRPC/mTLS │ Agent (per host) │
│ - Embedded web dashboard │◄──────────►│ - one persistent bidi │
│ - REST /api/* + SSE events │ :8443 │ stream (Agent.Connect) │
│ - Postgres (all state) │ │ - Docker runtime │
│ - Internal CA + pairing │ │ - module loader │
│ - Scheduler (cron + CEL) │ │ - RCON adapters │
│ - Admin HTTP /admin/v1/* │ │ - state tracker │
│ - Prometheus /metrics │ │ - SteamCMD / tar sidecars │
└───────────────────────────────┘ └────────────────────────────┘
▲ HTTP :8080 │ docker.sock
browser / panelctl / curl game containers (one per instance)
One controller orchestrates N agents. Agents connect outbound only — an agent box needs no inbound panel ports, just its game ports.
Components
Controller (controller/cmd/controller) — a single Go binary:
- Embedded vanilla-JS dashboard. No framework, no build step:
static/new.html(shell) +static/rcc.js+static/rcc.css, compiled in via//go:embedand served hash-stamped with immutable caching. The legacy single-filestatic/index.htmlis a frozen "classic" UI, opt-in via thepanel_ui=classiccookie. Live data reaches the browser over SSE (GET /api/events, withLast-Event-IDresume), not WebSockets. - Postgres for everything durable: users/sessions, agents, instances + assigned ports, schedules, backup index, pair-token hashes, Steam credential ciphertext. Migrations are embedded and run at startup. (No SQLite anywhere.)
- Internal CA (
controller/internal/ca, material indata/ca/) — issues the gRPC server cert and, via the pairing flow, each agent's client cert.-tls autoturns mTLS on whenever the CA exists. - Scheduler — robfig/cron 6-field triggers and CEL expressions evaluated against the live event bus with sustained-for windows ("empty for 30 minutes → stop"). Actions: rcon, instance start/stop/restart, backup.
- Auth — argon2id + session cookies; roles are just
admin/user; optional Sign-in-with-Steam (OpenID). No OIDC/WebAuthn/Casbin. - Admin HTTP (
/admin/v1/*) — out-of-band repair surface, gated by loopback or theX-Panel-Admin-Tokenheader, usable when the dashboard or a session is broken.
Agent (agent/cmd/agent) — a single Go binary per game host:
- Dials the controller's gRPC port and holds one bidi stream; all
control traffic (create/start/stop/update/backup/RCON/file ops,
events, heartbeats) is multiplexed over
oneofenvelope messages (proto/panel/v1). Reconnect + rehydrate: instance metadata survives agent restarts via--meta-dirsidecar files rebuilt from the controller's DB. - Docker-per-game runtime (
agent/internal/runtime): one container per instance, named volumes and/or bind mounts under--data-root, optional host networking.panel-*images are auto-built from the module's in-tree Dockerfile on first use; third-party images are pulled. Windows-only servers run under Wine + Xvfb in those images. - Sidecars:
steamcmd/steamcmdfor installs/updates (Windows-depot and beta-branch capable), alpine tar containers for backup/restore. - State tracker (
agent/internal/state): container state + RCON polling + log-line pattern matching → typed PlayerEvent/AppState events streamed upstream and fanned out to the browser (SSE), the scheduler (CEL), andpanelctl watch. - RCON adapters (
agent/internal/rcon):source_rcon,telnet,websocket_rcon(Rust),stdio,be_rcon(DayZ),docker_exec_rcon— all redial on EOF.
panelctl (controller/cmd/panelctl) — operator CLI. gRPC surface
for day-to-day lifecycle (agents/instances/create/start/stop/rcon/
backup/watch), plus a panelctl admin HTTP surface for the repair
endpoints. Uses the same mTLS certs as agents.
Modules (modules/<id>/module.yaml) — declarative manifests: image
or Dockerfile, ports, env, volumes, config files + templated values,
RCON adapter + commands, state-source polls, log event patterns, update
providers, appearance. The loader (pkg/module) is strict
(KnownFields) — unknown keys are rejected, not ignored. The README's
"Module manifest" section documents the schema by example.
Security model
- Control plane: gRPC with mTLS; agent certs are issued via
one-time pairing tokens (PAIRING.md) and verified at the
TLS handshake (
tls.RequireAndVerifyClientCert). Plaintext exists only as an explicit dev opt-in (--insecure/-tls off). - Browser: session cookies over your reverse proxy's TLS (INSTALL.md).
- Secrets: per-instance generated secrets in instance metadata; Steam credentials AES-GCM-encrypted with a key HKDF-derived from the CA key (ADMIN.md).
- Blast radius: the agent needs the Docker socket — root-equivalent on its host by design. Run agents on hosts you trust and don't expose the controller's gRPC port to the internet without cause.
Data flow, end to end
- Operator clicks Create →
POST /api/instances→ controller allocates host ports (per-agent window, DB-reserved — see NETWORKING.md) →InstanceCreateenvelope to the agent. - Agent resolves the manifest (env filtering,
$INSTANCE_ID/$DATA_PATHsubstitution), auto-builds/pulls the image, creates the container, persists sidecar metadata. - Update → SteamCMD sidecar into the game volume. Start → container start; state tracker begins polling/tailing; readiness regex flips the card to Running.
- Events stream to the controller → Postgres aggregates + SSE fanout →
dashboard, scheduler CEL evaluation,
panelctl watch.
Not implemented (on purpose or yet)
- OIDC / WebAuthn / Casbin ACLs — local auth + Steam OpenID only.
- SFTP — the web file manager (docker exec/cp with bind-mount fast path) covers the common case.
- Host-mode (non-Docker) runtime — manifests declare it, the agent fails loudly if asked.
- Multi-tenancy/billing — this is for someone running their own boxes.