Self-hostable game-server control panel: controller + agent + 26 game modules. One-line install (prebuilt release, no Go required): curl -fsSL https://git.pdxtechs.com/dbledeez/panel/raw/branch/main/install.sh | sudo bash Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
11 KiB
Installing panel
Four supported paths, in order of least effort:
- Linux one-liner —
install.sh(recommended) - Windows one-liner —
install.ps1 - docker-compose — controller + agent + Postgres as containers
- From source — full manual control
Then: Postgres provisioning, flags reference, systemd, reverse proxy + TLS, firewall.
Linux one-liner
Debian, Ubuntu, Fedora, CentOS/RHEL, Raspbian. Run as root:
curl -fsSL https://raw.githubusercontent.com/dbledeez/panel/main/install.sh | sudo bash
What it does (idempotent — safe to re-run; existing containers, units, CA, DB and passwords are kept, binaries and modules refreshed):
- Installs Docker via
get.docker.comif missing. - Starts a managed
panel-postgrescontainer (unless you point it at an existing DB withPANEL_DB_URL). - Builds (or downloads, with
PANEL_PREBUILT_URL)controller,agent, andpanelctlinto/opt/panel/bin/. - Boots the controller once to bootstrap the internal CA and the admin account, then pairs the local agent over mTLS.
- Installs and enables
panel-controller.serviceandpanel-agent.service.
Tunables (env vars, all optional — from install.sh's header):
| Env var | Meaning | Default |
|---|---|---|
PANEL_REPO_URL |
git/https base of the panel source | github repo |
PANEL_PREBUILT_URL |
tarball with prebuilt bin/{controller,agent,panelctl} + modules/ (skips the Go build) |
— |
PANEL_VERSION |
version string baked into the binaries | git describe || dev |
PANEL_DIR |
install prefix | /opt/panel |
PANEL_DATA_DIR |
data directory | $PANEL_DIR/data |
PANEL_HTTP_PORT |
dashboard port | 8080 |
PANEL_GRPC_PORT |
agent gRPC port | 8443 |
PANEL_ADMIN_PASSWORD |
stable first-boot admin password | generated by the controller |
PANEL_DB_URL |
use an EXISTING Postgres instead of the managed container | — |
PANEL_DB_PASSWORD |
password for the managed panel-postgres container |
generated |
PANEL_SKIP_AGENT=1 |
controller only (add agents on other boxes later — see PAIRING.md) | 0 |
Example — controller-only box on a custom port with a pinned admin password:
PANEL_SKIP_AGENT=1 PANEL_HTTP_PORT=9090 PANEL_ADMIN_PASSWORD='s3cret' \
bash install.sh
Windows one-liner
PowerShell 5+. Docker Desktop must be installed and running first (the script checks and refuses otherwise).
powershell -ExecutionPolicy Bypass -File install.ps1 -AdminPassword secret123
Parameters (mirror the Linux env vars): -RepoUrl, -PrebuiltUrl,
-Version, -InstallDir (default C:\Panel), -HttpPort (8080),
-GrpcPort (8443), -AdminPassword, -DbUrl, -DbPassword,
-SkipAgent. The same PANEL_* env vars are honored as defaults.
The controller and agent are registered as logon-time Scheduled
Tasks. For a hardened always-on Windows service, wrap the same command
lines with nssm:
nssm install panel-controller C:\Panel\bin\controller.exe ....
docker-compose
A production single-box stack (Postgres + controller + agent) lives at
deploy/docker-compose.yml. From the repo root:
cp deploy/.env.example deploy/.env # then edit deploy/.env (PANEL_DB_PASSWORD is required)
docker compose -f deploy/docker-compose.yml --env-file deploy/.env up -d --build
First run: watch docker compose logs controller for the
ADMIN BOOTSTRAP banner (generated admin password), then mint a pair
token in the UI and pair the agent — see PAIRING.md.
Security note (from the compose file itself): the agent container mounts the host Docker socket to manage game-server containers. Docker-socket access is root-equivalent on the host. Run this stack only on a host you fully trust and keep the dashboard behind auth/TLS.
From source
Prereqs: Go 1.26+, Docker, buf
on PATH for proto stubs.
buf generate # proto stubs (once per clone / .proto change)
go build -o bin/controller ./controller/cmd/controller
go build -o bin/agent ./agent/cmd/agent
go build -o bin/panelctl ./controller/cmd/panelctl
# 1) Postgres (dev container) — or see "Postgres provisioning" below
docker compose -f docker-compose.dev.yml up -d
# 2) Controller — first run prints the ADMIN BOOTSTRAP banner
./bin/controller
# 3) Agent (same box, dev): plaintext is fine locally
./bin/agent --insecure --modules-dir=./modules \
--data-root=./data/instances --meta-dir=./data/instance-meta \
--backup-dir=./data/backups
For production, pair the agent instead of --insecure
(PAIRING.md) and run both under systemd (below).
There is also a root Makefile (make build, make test, make vet,
make install-local, ...).
Postgres provisioning
The controller needs Postgres (17 is what the managed container and compose stack use; the schema is plain SQL with embedded migrations, run automatically at controller start).
Bring your own instance:
CREATE ROLE panel LOGIN PASSWORD 'choose-a-password';
CREATE DATABASE panel OWNER panel;
Point the controller at it via either:
--db-url 'postgres://panel:...@dbhost:5432/panel?sslmode=disable'PANEL_DB_URLenv var (honored when--db-urlis left at its default)
The default (dev) DSN is
postgres://panel:panel_dev@127.0.0.1:5432/panel?sslmode=disable.
Or let install.sh run the panel-postgres container for you.
Controller flags
From controller/cmd/controller/main.go:
| Flag | Default | Meaning |
|---|---|---|
-grpc |
:8443 |
gRPC listen address (both Agent + Panel services) |
-http |
:8080 |
HTTP dashboard + REST API listen address |
-ca-dir |
./data/ca |
directory holding internal CA + server cert (auto-generated on first run) |
-tls |
auto |
auto | on | off — auto enables mTLS if CA + server cert present |
-tls-hostnames |
— | comma-separated SAN DNS names to add to the server cert (e.g. panel.example.com) |
-log-level |
info |
debug, info, warn, error |
-db-url |
dev DSN | Postgres connection string (env PANEL_DB_URL also honored) |
-public-url |
— | public base URL of the dashboard (needed for Steam OpenID sign-in — see ADMIN.md) |
-initial-admin-steam-id |
— | 17-digit SteamID64 linked to the first admin, enabling Sign in with Steam (ADMIN.md) |
Env vars: PANEL_DB_URL, PANEL_ADMIN_PASSWORD (first-boot admin
password preseed).
Agent flags
From agent/cmd/agent/main.go:
| Flag | Default | Meaning |
|---|---|---|
--controller |
localhost:8443 |
controller gRPC address host:port |
--agent-id |
mTLS cert CN, or hostname when --insecure |
agent identifier |
--modules-dir |
./modules |
path to modules directory |
--data-root |
./data/instances |
root directory for instance data |
--meta-dir |
./data/instance-meta |
per-instance sidecar metadata (survives agent restart) |
--backup-dir |
./data/backups |
where instance backup archives are written |
--cert-dir |
./data/certs |
holds agent.crt + agent.key + ca.crt after pairing |
--heartbeat |
10s |
heartbeat interval |
--insecure |
false |
force plaintext (no TLS) — dev/local only |
--pair-token |
— | pair mode: fetch CA + signed cert from --pair-url and exit (PAIRING.md) |
--pair-url |
http://localhost:8080 |
controller HTTP URL for the pair endpoints |
--log-level |
info |
debug, info, warn, error |
systemd
Reference units live in deploy/systemd/ (panel-controller.service,
panel-agent.service) — install.sh installs them for you. Highlights:
- Both run as the
paneluser fromWorkingDirectory=/opt/panel; the agent additionally getsSupplementaryGroups=docker. /opt/panel/panel.env(optionalEnvironmentFile) overridesPANEL_HTTP_PORT,PANEL_GRPC_PORT,PANEL_DB_URL,PANEL_ADMIN_PASSWORD, ...- The controller unit is hardened (
ProtectSystem=strict,ReadWritePaths=/opt/panel); the agent's hardening is intentionally lighter because it drives the Docker socket.
sudo cp deploy/systemd/*.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now panel-controller panel-agent
journalctl -u panel-controller -f # ADMIN BOOTSTRAP banner lives here
Reverse proxy + real TLS
The dashboard speaks plain HTTP on :8080; put a reverse proxy with a
real certificate in front of it for anything internet-facing. Only
proxy the HTTP port — agents talk to gRPC :8443 directly (mTLS,
LAN/VPN is fine; see PAIRING.md).
The dashboard uses SSE (/api/events) — disable proxy buffering on
that path or the live console will lag.
Caddy
panel.example.com {
reverse_proxy 127.0.0.1:8080 {
flush_interval -1 # SSE: stream immediately
}
}
That's the whole file — Caddy handles Let's Encrypt automatically.
nginx
server {
listen 443 ssl;
server_name panel.example.com;
ssl_certificate /etc/letsencrypt/live/panel.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/panel.example.com/privkey.pem;
client_max_body_size 512m; # scenario/mod zip uploads
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /api/events { # SSE — no buffering
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 1h;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
If the proxy terminates TLS with a public hostname and agents pair
through it, add that hostname to the controller cert SANs:
-tls-hostnames panel.example.com.
Firewall basics
On the controller box:
| Port | Proto | Who needs it | Expose to internet? |
|---|---|---|---|
| 8080 (HTTP) | tcp | browsers, panelctl admin, pairing agents |
only via the reverse proxy above |
| 8443 (gRPC) | tcp | agents + panelctl |
no — LAN/VPN preferred; mTLS protects it if you must |
| 5432 (Postgres) | tcp | controller only | never |
On each agent box: no inbound panel ports at all — the agent dials out to the controller. You only open the game ports of the instances it hosts; see NETWORKING.md for the per-game table.
Example (ufw, controller on a LAN):
sudo ufw allow from 192.168.1.0/24 to any port 8080 proto tcp
sudo ufw allow from 192.168.1.0/24 to any port 8443 proto tcp