Files
panel/docs/INSTALL.md
T
dbledeez 4ccccc6fe2 panel v0.9.2 — public release
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>
2026-07-15 00:43:35 -07:00

15 KiB

Installing panel

Four supported paths, in order of least effort:

  1. Linux one-linerinstall.sh (recommended)
  2. Windows one-linerinstall.ps1
  3. docker-compose — controller + agent + Postgres as containers
  4. 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):

  1. Installs Docker via get.docker.com if missing.
  2. Starts a managed panel-postgres container (unless you point it at an existing DB with PANEL_DB_URL).
  3. Builds (or downloads, with PANEL_PREBUILT_URL) controller, agent, and panelctl into /opt/panel/bin/.
  4. Boots the controller once to bootstrap the internal CA and the admin account, then pairs the local agent over mTLS.
  5. Installs and enables panel-controller.service and panel-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:

  1. 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).
  2. 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_URL env var (honored when --db-url is 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 | offauto 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 panel user from WorkingDirectory=/opt/panel; the agent additionally gets SupplementaryGroups=docker.
  • /opt/panel/panel.env (optional EnvironmentFile) overrides PANEL_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-Proto header (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