# Installing panel Four supported paths, in order of least effort: 1. [Linux one-liner](#linux-one-liner) — `install.sh` (recommended) 2. [Windows one-liner](#windows-one-liner) — `install.ps1` 3. [docker-compose](#docker-compose) — controller + agent + Postgres as containers 4. [From source](#from-source) — full manual control Then: [Postgres provisioning](#postgres-provisioning), [flags reference](#controller-flags), [systemd](#systemd), [reverse proxy + TLS](#reverse-proxy--real-tls), [firewall](#firewall-basics). --- ## Linux one-liner Debian, Ubuntu, Fedora, CentOS/RHEL, Raspbian. Run as root: ```bash 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](PAIRING.md)) | `0` | Example — controller-only box on a custom port with a pinned admin password: ```bash 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 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](https://nssm.cc): `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: ```bash 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](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`](https://buf.build/docs/installation/) on PATH for proto stubs. ```bash 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](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: ```sql 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` \| `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](ADMIN.md)) | | `-initial-admin-steam-id` | — | 17-digit SteamID64 linked to the first admin, enabling Sign in with Steam ([ADMIN.md](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](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. ```bash 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](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 ```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](NETWORKING.md) for the per-game table. Example (ufw, controller on a LAN): ```bash 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 ```