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>
15 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
Availability & self-hosting (mirroring the source)
By default the installer fetches from a single upstream, git.pdxtechs.com:
PANEL_REPO_URL supplies the Git source, and PANEL_PREBUILT_URL defaults to
the published prebuilt release tarball on that same host. If that host is down,
rate-limited, or you simply don't want a single external dependency in your
install path, override either one to point at a mirror or self-hosted copy you
control:
# (a) Build from your own Git mirror of the source (empty PANEL_PREBUILT_URL
# forces a from-source build; needs Go >= 1.26):
PANEL_REPO_URL=https://github.com/you/panel PANEL_PREBUILT_URL= sudo bash install.sh
# (b) Install a prebuilt tarball you host yourself (no Go toolchain needed).
# The tarball must contain bin/{controller,agent,panelctl} + modules/:
PANEL_PREBUILT_URL=https://mirror.example.com/panel-linux-amd64.tar.gz sudo bash install.sh
# (c) Air-gapped: run install.sh from a local checkout of the repo. A local
# checkout (go.work + controller/ present next to install.sh) always wins
# over the git.pdxtechs.com default and builds the tree in place.
sudo bash /path/to/panel/install.sh
To produce your own prebuilt tarball for (b), cross-build the three binaries
and tar czf them alongside modules/ — see Build below and
the release layout bin/{controller,agent,panelctl} + modules/. On Windows
the same overrides apply to install.ps1.
Windows one-liner
PowerShell 5+, run as Administrator:
irm https://git.pdxtechs.com/dbledeez/panel/raw/branch/main/install.ps1 | iex
Docker Desktop is required (it runs Postgres + your game servers). If it
isn't installed, the installer offers to install it for you via winget.
Because Docker Desktop needs WSL2/virtualization and usually a reboot, the
Windows install is effectively two steps the first time:
- Run the command above. If Docker is missing it installs Docker Desktop and then asks you to reboot + start Docker Desktop (wait for the whale icon to settle).
- Re-run the exact same command — it picks up from there and finishes.
To skip the prompt: -InstallDocker (or PANEL_INSTALL_DOCKER=1) auto-installs
Docker without asking; -SkipDockerInstall keeps the old fail-if-missing
behavior. On a box that already has Docker Desktop running, it's a genuine
one-liner.
Windows note: the controller runs great on Windows. Running game servers on Windows uses Docker Desktop, where the agent automatically switches modules from host networking to published bridge ports so they're reachable — but the smoothest, fully-proven game-hosting path is a Linux agent. See NETWORKING.md.
Parameters (mirror the Linux env vars): -RepoUrl, -PrebuiltUrl,
-Version, -InstallDir (default C:\Panel), -HttpPort (8080),
-GrpcPort (8443), -AdminEmail, -AdminPassword, -DbUrl, -DbPassword,
-DbContainer, -DbPort, -DbVolume, -SkipAgent, -InstallDocker,
-SkipDockerInstall. The same PANEL_* env vars are honored as defaults.
When run in a real console it prompts you to choose the admin email + password
up front (no log-reading); when piped (irm | iex) it uses env/param values or
generates a password.
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
Do you even need a reverse proxy?
Not necessarily. The panel serves its own dashboard on :8080 and works
fine with no proxy at all:
- Home / LAN use — just browse to
http://<your-box-ip>:8080. Steam sign-in works too (Steam returns you to that same address). Nothing else to set up. - You just want HTTPS and a domain — a reverse proxy (Caddy is the one-file option below) gives you a real cert. Optional, but nice.
You do want a proxy the moment the panel is internet-facing (real TLS, not plain HTTP over the internet). If you add one, read the Steam note below — a misconfigured proxy is the one thing that quietly breaks Steam login.
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).
Steam login behind a proxy — read this. When a proxy terminates TLS, the controller must know its real public HTTPS URL or Steam's return trip lands on the wrong origin and login fails. Do one of:
- start the controller with
--public-url https://panel.example.com(most reliable — set it and forget it), or- make sure your proxy forwards the
X-Forwarded-Protoheader (the Caddy and nginx examples below already do the right thing).If you skip both, the controller logs a warning at first Steam-login attempt telling you exactly this. With no proxy (direct IP/LAN), none of this applies — Steam login just works.
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