# 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+, run **as Administrator**:

```powershell
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](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](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
```
