1 Commits

Author SHA1 Message Date
dbledeez a00bd620a1 Panel — public source drop (v0.9.0)
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>
2026-07-14 21:17:39 -07:00
5 changed files with 189 additions and 28 deletions
+4
View File
@@ -1,3 +1,7 @@
# REFERENCE ONLY — install.sh GENERATES the real unit at install time,
# substituting the actual PANEL_DIR/ports/user (see write_unit in install.sh).
# This static copy assumes the /opt/panel defaults; edit it by hand if you wire
# the units up yourself (e.g. via compose) instead of running install.sh.
[Unit] [Unit]
Description=Panel agent (Docker runtime for game-server instances) Description=Panel agent (Docker runtime for game-server instances)
Documentation=https://github.com/dbledeez/panel Documentation=https://github.com/dbledeez/panel
+4
View File
@@ -1,3 +1,7 @@
# REFERENCE ONLY — install.sh GENERATES the real unit at install time,
# substituting the actual PANEL_DIR/ports/user (see write_unit in install.sh).
# This static copy assumes the /opt/panel defaults; edit it by hand if you wire
# the units up yourself (e.g. via compose) instead of running install.sh.
[Unit] [Unit]
Description=Panel controller (HTTP dashboard :8080 + agent gRPC :8443) Description=Panel controller (HTTP dashboard :8080 + agent gRPC :8443)
Documentation=https://github.com/dbledeez/panel Documentation=https://github.com/dbledeez/panel
+2 -3
View File
@@ -11,8 +11,6 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
@@ -21,7 +19,8 @@ golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeId
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
+23 -7
View File
@@ -18,7 +18,10 @@
[CmdletBinding()] [CmdletBinding()]
param( param(
[string]$RepoUrl = $(if ($env:PANEL_REPO_URL) { $env:PANEL_REPO_URL } else { 'https://git.pdxtechs.com/dbledeez/panel' }), [string]$RepoUrl = $(if ($env:PANEL_REPO_URL) { $env:PANEL_REPO_URL } else { 'https://git.pdxtechs.com/dbledeez/panel' }),
[string]$PrebuiltUrl = $env:PANEL_PREBUILT_URL, # Defaults to the published v0.9.0 windows/amd64 prebuilt zip so the installer
# needs no Go toolchain. Pass -PrebuiltUrl '' (empty) to force a source build.
# A local checkout (running install.ps1 from inside the repo) wins over this.
[string]$PrebuiltUrl = $(if ($null -ne $env:PANEL_PREBUILT_URL) { $env:PANEL_PREBUILT_URL } else { 'https://git.pdxtechs.com/dbledeez/panel/releases/download/v0.9.0/panel-v0.9.0-windows-amd64.zip' }),
[string]$Version = $env:PANEL_VERSION, [string]$Version = $env:PANEL_VERSION,
[string]$InstallDir = $(if ($env:PANEL_DIR) { $env:PANEL_DIR } else { 'C:\Panel' }), [string]$InstallDir = $(if ($env:PANEL_DIR) { $env:PANEL_DIR } else { 'C:\Panel' }),
[int] $HttpPort = $(if ($env:PANEL_HTTP_PORT) { [int]$env:PANEL_HTTP_PORT } else { 8080 }), [int] $HttpPort = $(if ($env:PANEL_HTTP_PORT) { [int]$env:PANEL_HTTP_PORT } else { 8080 }),
@@ -26,10 +29,15 @@ param(
[string]$AdminPassword = $env:PANEL_ADMIN_PASSWORD, [string]$AdminPassword = $env:PANEL_ADMIN_PASSWORD,
[string]$DbUrl = $env:PANEL_DB_URL, [string]$DbUrl = $env:PANEL_DB_URL,
[string]$DbPassword = $env:PANEL_DB_PASSWORD, [string]$DbPassword = $env:PANEL_DB_PASSWORD,
[string]$DbContainer = $(if ($env:PANEL_DB_CONTAINER) { $env:PANEL_DB_CONTAINER } else { 'panel-postgres' }),
[int] $DbPort = $(if ($env:PANEL_DB_PORT) { [int]$env:PANEL_DB_PORT } else { 5432 }),
[string]$DbVolume = $(if ($env:PANEL_DB_VOLUME) { $env:PANEL_DB_VOLUME } else { 'panel_pgdata' }),
[switch]$SkipAgent [switch]$SkipAgent
) )
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$PgContainer = 'panel-postgres' # Run two independent panels on one host by giving each install distinct
# -InstallDir, -HttpPort, -GrpcPort, -DbContainer, -DbPort and -DbVolume.
$PgContainer = $DbContainer
function Log($msg) { Write-Host "[panel] $msg" -ForegroundColor Green } function Log($msg) { Write-Host "[panel] $msg" -ForegroundColor Green }
function Warn($msg) { Write-Host "[panel] $msg" -ForegroundColor Yellow } function Warn($msg) { Write-Host "[panel] $msg" -ForegroundColor Yellow }
@@ -65,15 +73,15 @@ if ($DbUrl) {
if (-not $DbPassword) { if (-not $DbPassword) {
$DbPassword = -join ((1..32) | ForEach-Object { '{0:x}' -f (Get-Random -Maximum 16) }) $DbPassword = -join ((1..32) | ForEach-Object { '{0:x}' -f (Get-Random -Maximum 16) })
} }
Log "starting postgres container '$PgContainer'" Log "starting postgres container '$PgContainer' on 127.0.0.1:$DbPort (volume $DbVolume)"
docker run -d --name $PgContainer --restart unless-stopped ` docker run -d --name $PgContainer --restart unless-stopped `
-e POSTGRES_USER=panel -e "POSTGRES_PASSWORD=$DbPassword" -e POSTGRES_DB=panel ` -e POSTGRES_USER=panel -e "POSTGRES_PASSWORD=$DbPassword" -e POSTGRES_DB=panel `
-p 127.0.0.1:5432:5432 ` -p "127.0.0.1:${DbPort}:5432" `
-v panel_pgdata:/var/lib/postgresql/data ` -v "${DbVolume}:/var/lib/postgresql/data" `
postgres:17-alpine | Out-Null postgres:17-alpine | Out-Null
if ($LASTEXITCODE -ne 0) { Fail "docker run for postgres failed" } if ($LASTEXITCODE -ne 0) { Fail "docker run for postgres failed" }
} }
$DbUrl = "postgres://panel:$DbPassword@127.0.0.1:5432/panel?sslmode=disable" $DbUrl = "postgres://panel:$DbPassword@127.0.0.1:$DbPort/panel?sslmode=disable"
Log "waiting for postgres" Log "waiting for postgres"
$ready = $false $ready = $false
foreach ($i in 1..30) { foreach ($i in 1..30) {
@@ -90,6 +98,13 @@ $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
if ($ScriptDir -and (Test-Path (Join-Path $ScriptDir 'go.work')) -and (Test-Path (Join-Path $ScriptDir 'controller'))) { if ($ScriptDir -and (Test-Path (Join-Path $ScriptDir 'go.work')) -and (Test-Path (Join-Path $ScriptDir 'controller'))) {
$Src = $ScriptDir $Src = $ScriptDir
Log "using local checkout at $Src" Log "using local checkout at $Src"
# Running from inside the repo means build THIS tree. Don't let the prebuilt
# default override it. An explicit -PrebuiltUrl or $env:PANEL_PREBUILT_URL still
# forces prebuilt (it won't equal the untouched default / the env is non-null).
$prebuiltDefault = 'https://git.pdxtechs.com/dbledeez/panel/releases/download/v0.9.0/panel-v0.9.0-windows-amd64.zip'
if (($null -eq $env:PANEL_PREBUILT_URL) -and ($PrebuiltUrl -eq $prebuiltDefault)) {
$PrebuiltUrl = ''
}
} }
if ($PrebuiltUrl) { if ($PrebuiltUrl) {
@@ -167,7 +182,8 @@ foreach ($i in 1..60) {
if (-not $up) { Fail "controller did not come up - see $ctrlLog" } if (-not $up) { Fail "controller did not come up - see $ctrlLog" }
$adminEmail = 'admin@panel.local' $adminEmail = 'admin@panel.local'
$adminShown = '(set via -AdminPassword)' # BUG-5b: always give the operator a path to the password.
$adminShown = "(the one you set via -AdminPassword; also in $EnvFile)"
if (-not $AdminPassword) { if (-not $AdminPassword) {
if ($firstBoot) { if ($firstBoot) {
$m = Select-String -Path $ctrlLog -Pattern 'password: ([a-f0-9]{16})' | Select-Object -First 1 $m = Select-String -Path $ctrlLog -Pattern 'password: ([a-f0-9]{16})' | Select-Object -First 1
+156 -18
View File
@@ -6,6 +6,8 @@
# Non-interactive knobs (env vars, all optional): # Non-interactive knobs (env vars, all optional):
# PANEL_REPO_URL git/https base of the panel source (default: placeholder, see below) # PANEL_REPO_URL git/https base of the panel source (default: placeholder, see below)
# PANEL_PREBUILT_URL tarball with prebuilt bin/{controller,agent,panelctl} + modules/ (skips Go build) # PANEL_PREBUILT_URL tarball with prebuilt bin/{controller,agent,panelctl} + modules/ (skips Go build)
# (default: the published v0.9.0 linux/amd64 release — so
# curl|bash needs no Go. Set it EMPTY to force a source build.)
# PANEL_VERSION version string baked into the binaries (default: git describe || dev) # PANEL_VERSION version string baked into the binaries (default: git describe || dev)
# PANEL_DIR install prefix (default: /opt/panel) # PANEL_DIR install prefix (default: /opt/panel)
# PANEL_DATA_DIR data directory (default: $PANEL_DIR/data) # PANEL_DATA_DIR data directory (default: $PANEL_DIR/data)
@@ -13,15 +15,30 @@
# PANEL_GRPC_PORT agent gRPC port (default: 8443) # PANEL_GRPC_PORT agent gRPC port (default: 8443)
# PANEL_ADMIN_PASSWORD stable first-boot admin password (default: generated by the controller) # PANEL_ADMIN_PASSWORD stable first-boot admin password (default: generated by the controller)
# PANEL_DB_URL use an EXISTING Postgres instead of the managed container # PANEL_DB_URL use an EXISTING Postgres instead of the managed container
# PANEL_DB_PASSWORD password for the managed panel-postgres container (default: generated) # PANEL_DB_PASSWORD password for the managed Postgres container (default: generated)
# PANEL_DB_CONTAINER name of the managed Postgres container (default: panel-postgres)
# PANEL_DB_PORT host port to bind Postgres on (127.0.0.1) (default: 5432)
# PANEL_DB_VOLUME docker volume for Postgres data (default: panel_pgdata)
# PANEL_SKIP_AGENT=1 install controller only (add agents on other boxes later) # PANEL_SKIP_AGENT=1 install controller only (add agents on other boxes later)
# #
# Run TWO independent panels on one host by giving each install distinct
# PANEL_DIR, PANEL_HTTP_PORT, PANEL_GRPC_PORT, PANEL_DB_CONTAINER,
# PANEL_DB_PORT and PANEL_DB_VOLUME values so nothing collides.
#
# Idempotent: safe to re-run — existing containers, units, CA, DB and # Idempotent: safe to re-run — existing containers, units, CA, DB and
# passwords are kept; binaries and modules are refreshed. # passwords are kept; binaries and modules are refreshed.
set -euo pipefail set -euo pipefail
PANEL_REPO_URL="${PANEL_REPO_URL:-https://git.pdxtechs.com/dbledeez/panel}" PANEL_REPO_URL="${PANEL_REPO_URL:-https://git.pdxtechs.com/dbledeez/panel}"
PANEL_PREBUILT_URL="${PANEL_PREBUILT_URL:-}" # Default to the published prebuilt release so `curl … | sudo bash` works with no
# Go toolchain and no env. Explicitly set PANEL_PREBUILT_URL= (empty) to force the
# git-clone + Go source-build fallback instead. A local checkout (running
# ./install.sh from inside the repo) always wins over this default — see §4.
PANEL_PREBUILT_DEFAULT="https://git.pdxtechs.com/dbledeez/panel/releases/download/v0.9.0/panel-v0.9.0-linux-amd64.tar.gz"
# Was the var set in the environment at all (even to empty)? Distinguishes an
# operator who explicitly wants prebuilt from the plain default.
if [ "${PANEL_PREBUILT_URL+set}" = set ]; then PANEL_PREBUILT_URL_EXPLICIT=1; else PANEL_PREBUILT_URL_EXPLICIT=""; fi
PANEL_PREBUILT_URL="${PANEL_PREBUILT_URL-$PANEL_PREBUILT_DEFAULT}"
PANEL_DIR="${PANEL_DIR:-/opt/panel}" PANEL_DIR="${PANEL_DIR:-/opt/panel}"
PANEL_DATA_DIR="${PANEL_DATA_DIR:-$PANEL_DIR/data}" PANEL_DATA_DIR="${PANEL_DATA_DIR:-$PANEL_DIR/data}"
PANEL_HTTP_PORT="${PANEL_HTTP_PORT:-8080}" PANEL_HTTP_PORT="${PANEL_HTTP_PORT:-8080}"
@@ -29,9 +46,19 @@ PANEL_GRPC_PORT="${PANEL_GRPC_PORT:-8443}"
PANEL_ADMIN_PASSWORD="${PANEL_ADMIN_PASSWORD:-}" PANEL_ADMIN_PASSWORD="${PANEL_ADMIN_PASSWORD:-}"
PANEL_DB_URL="${PANEL_DB_URL:-}" PANEL_DB_URL="${PANEL_DB_URL:-}"
PANEL_DB_PASSWORD="${PANEL_DB_PASSWORD:-}" PANEL_DB_PASSWORD="${PANEL_DB_PASSWORD:-}"
PANEL_DB_CONTAINER="${PANEL_DB_CONTAINER:-panel-postgres}"
PANEL_DB_PORT="${PANEL_DB_PORT:-5432}"
PANEL_DB_VOLUME="${PANEL_DB_VOLUME:-panel_pgdata}"
PANEL_SKIP_AGENT="${PANEL_SKIP_AGENT:-0}" PANEL_SKIP_AGENT="${PANEL_SKIP_AGENT:-0}"
PANEL_USER=panel PANEL_USER=panel
PG_CONTAINER=panel-postgres PG_CONTAINER="$PANEL_DB_CONTAINER"
# Legacy escape hatch (kept for compatibility): a release asset tried only when
# no prebuilt URL, no local checkout and no Go are available. Now that
# PANEL_PREBUILT_URL defaults to the published release above, this is rarely hit —
# it only matters if someone explicitly emptied PANEL_PREBUILT_URL but still has
# no Go and no checkout.
PANEL_RELEASE_TARBALL_URL="${PANEL_RELEASE_TARBALL_URL:-$PANEL_PREBUILT_DEFAULT}"
log() { printf '\033[1;32m[panel]\033[0m %s\n' "$*"; } log() { printf '\033[1;32m[panel]\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m[panel]\033[0m %s\n' "$*" >&2; } warn() { printf '\033[1;33m[panel]\033[0m %s\n' "$*" >&2; }
@@ -86,14 +113,14 @@ else
if [ -z "$PANEL_DB_PASSWORD" ]; then if [ -z "$PANEL_DB_PASSWORD" ]; then
PANEL_DB_PASSWORD="$(head -c 24 /dev/urandom | od -An -tx1 | tr -d ' \n')" PANEL_DB_PASSWORD="$(head -c 24 /dev/urandom | od -An -tx1 | tr -d ' \n')"
fi fi
log "starting postgres container '$PG_CONTAINER'" log "starting postgres container '$PG_CONTAINER' on 127.0.0.1:$PANEL_DB_PORT (volume $PANEL_DB_VOLUME)"
docker run -d --name "$PG_CONTAINER" --restart unless-stopped \ docker run -d --name "$PG_CONTAINER" --restart unless-stopped \
-e POSTGRES_USER=panel -e POSTGRES_PASSWORD="$PANEL_DB_PASSWORD" -e POSTGRES_DB=panel \ -e POSTGRES_USER=panel -e POSTGRES_PASSWORD="$PANEL_DB_PASSWORD" -e POSTGRES_DB=panel \
-p 127.0.0.1:5432:5432 \ -p "127.0.0.1:$PANEL_DB_PORT:5432" \
-v panel_pgdata:/var/lib/postgresql/data \ -v "$PANEL_DB_VOLUME:/var/lib/postgresql/data" \
postgres:17-alpine >/dev/null postgres:17-alpine >/dev/null
fi fi
PANEL_DB_URL="postgres://panel:${PANEL_DB_PASSWORD}@127.0.0.1:5432/panel?sslmode=disable" PANEL_DB_URL="postgres://panel:${PANEL_DB_PASSWORD}@127.0.0.1:$PANEL_DB_PORT/panel?sslmode=disable"
log "waiting for postgres to accept connections" log "waiting for postgres to accept connections"
for _ in $(seq 1 30); do for _ in $(seq 1 30); do
if docker exec "$PG_CONTAINER" pg_isready -U panel -d panel >/dev/null 2>&1; then break; fi if docker exec "$PG_CONTAINER" pg_isready -U panel -d panel >/dev/null 2>&1; then break; fi
@@ -108,6 +135,20 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" >/dev/null 2>&1 && pwd ||
if [ -n "$SCRIPT_DIR" ] && [ -f "$SCRIPT_DIR/go.work" ] && [ -d "$SCRIPT_DIR/controller" ]; then if [ -n "$SCRIPT_DIR" ] && [ -f "$SCRIPT_DIR/go.work" ] && [ -d "$SCRIPT_DIR/controller" ]; then
SRC="$SCRIPT_DIR" SRC="$SCRIPT_DIR"
log "using local checkout at $SRC" log "using local checkout at $SRC"
# A local checkout means the operator wants to build THIS tree. Don't silently
# override it with the prebuilt release default. (An explicit PANEL_PREBUILT_URL
# env still forces prebuilt — it survives because it was exported, not defaulted.)
if [ -z "${PANEL_PREBUILT_URL_EXPLICIT:-}" ] && [ "$PANEL_PREBUILT_URL" = "$PANEL_PREBUILT_DEFAULT" ]; then
PANEL_PREBUILT_URL=""
fi
fi
# If no prebuilt URL, no local checkout and no Go, but a release tarball URL is
# known, auto-fall-back to the published release so the one-liner still works.
if [ -z "$PANEL_PREBUILT_URL" ] && [ -z "$SRC" ] \
&& ! command -v go >/dev/null 2>&1 && [ -n "$PANEL_RELEASE_TARBALL_URL" ]; then
log "no Go toolchain — falling back to the published release tarball"
PANEL_PREBUILT_URL="$PANEL_RELEASE_TARBALL_URL"
fi fi
if [ -n "$PANEL_PREBUILT_URL" ]; then if [ -n "$PANEL_PREBUILT_URL" ]; then
@@ -121,6 +162,10 @@ else
if [ -z "$SRC" ]; then if [ -z "$SRC" ]; then
command -v git >/dev/null || die "git is required to fetch sources (or set PANEL_PREBUILT_URL)" command -v git >/dev/null || die "git is required to fetch sources (or set PANEL_PREBUILT_URL)"
SRC="$PANEL_DIR/src" SRC="$PANEL_DIR/src"
# BUG-4: the src tree is cloned as root then chowned to $PANEL_USER; on a
# re-run git would refuse with "detected dubious ownership". Mark it safe
# for whatever user runs git (root here) so re-runs are idempotent.
git config --global --add safe.directory "$SRC" 2>/dev/null || true
if [ -d "$SRC/.git" ]; then if [ -d "$SRC/.git" ]; then
log "updating source checkout at $SRC" log "updating source checkout at $SRC"
git -C "$SRC" pull --ff-only git -C "$SRC" pull --ff-only
@@ -129,7 +174,12 @@ else
git clone --depth 1 "$PANEL_REPO_URL" "$SRC" git clone --depth 1 "$PANEL_REPO_URL" "$SRC"
fi fi
fi fi
command -v go >/dev/null || die "Go toolchain not found. Install Go >= 1.26 (https://go.dev/dl/) or set PANEL_PREBUILT_URL." if ! command -v go >/dev/null 2>&1; then
die "Go toolchain not found. The one-line installer normally uses prebuilt
binaries; falling back to a source build requires Go >= 1.26 (https://go.dev/dl/).
Alternatively set PANEL_PREBUILT_URL to a prebuilt tarball, or
PANEL_RELEASE_TARBALL_URL to a published release asset."
fi
VERSION="${PANEL_VERSION:-$(git -C "$SRC" describe --tags --always --dirty 2>/dev/null || echo dev)}" VERSION="${PANEL_VERSION:-$(git -C "$SRC" describe --tags --always --dirty 2>/dev/null || echo dev)}"
LDFLAGS="-s -w -X github.com/dbledeez/panel/pkg/version.Version=$VERSION" LDFLAGS="-s -w -X github.com/dbledeez/panel/pkg/version.Version=$VERSION"
log "building panel $VERSION" log "building panel $VERSION"
@@ -162,15 +212,101 @@ umask 022
chown -R "$PANEL_USER:$PANEL_USER" "$PANEL_DIR" chown -R "$PANEL_USER:$PANEL_USER" "$PANEL_DIR"
chmod 600 "$ENV_FILE" chmod 600 "$ENV_FILE"
if [ -n "$SRC" ] && [ -d "$SRC/deploy/systemd" ]; then # BUG-2/3/5: do NOT install deploy/systemd/*.service verbatim — those hardcode
install -m 0644 "$SRC/deploy/systemd/panel-controller.service" \ # /opt/panel, ProtectHome=true and stale :8080/:8443 in the Description. Instead
"$SRC/deploy/systemd/panel-agent.service" /etc/systemd/system/ # GENERATE both units here, substituting the REAL install parameters.
else #
# Prebuilt path: units are expected inside the tarball at deploy/systemd. # BUG-3: systemd ProtectHome=true makes any binary under /home unreadable to the
[ -f "$PANEL_DIR/deploy/systemd/panel-controller.service" ] || die "no systemd units found (deploy/systemd missing)" # service (203/EXEC). When PANEL_DIR is under /home we emit the unit WITHOUT
install -m 0644 "$PANEL_DIR/deploy/systemd/panel-controller.service" \ # ProtectHome so it actually runs (reduced hardening, noted inline). The default
"$PANEL_DIR/deploy/systemd/panel-agent.service" /etc/systemd/system/ # /opt/panel keeps ProtectHome=true.
fi case "$PANEL_DIR/" in
/home/*) PROTECT_HOME=""; log "PANEL_DIR is under /home — generating units without ProtectHome (reduced hardening)";;
*) PROTECT_HOME="ProtectHome=true";;
esac
write_unit() {
# $1 = destination path
cat > "$1"
chmod 0644 "$1"
}
write_unit /etc/systemd/system/panel-controller.service <<EOF
[Unit]
Description=Panel controller (HTTP dashboard :${PANEL_HTTP_PORT} + agent gRPC :${PANEL_GRPC_PORT})
Documentation=https://github.com/dbledeez/panel
After=network-online.target docker.service
Wants=network-online.target
[Service]
Type=simple
User=${PANEL_USER}
Group=${PANEL_USER}
WorkingDirectory=${PANEL_DIR}
Environment=PANEL_HTTP_PORT=${PANEL_HTTP_PORT}
Environment=PANEL_GRPC_PORT=${PANEL_GRPC_PORT}
EnvironmentFile=-${ENV_FILE}
ExecStart=${PANEL_DIR}/bin/controller \\
-http :\${PANEL_HTTP_PORT} \\
-grpc :\${PANEL_GRPC_PORT} \\
-ca-dir ./data/ca
Restart=on-failure
RestartSec=3
# Hardening — the controller only needs its own tree + the network.
NoNewPrivileges=true
ProtectSystem=strict
${PROTECT_HOME}
ReadWritePaths=${PANEL_DIR}
PrivateTmp=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true
LockPersonality=true
[Install]
WantedBy=multi-user.target
EOF
write_unit /etc/systemd/system/panel-agent.service <<EOF
[Unit]
Description=Panel agent (Docker runtime for game-server instances)
Documentation=https://github.com/dbledeez/panel
After=network-online.target docker.service panel-controller.service
Wants=network-online.target
Requires=docker.service
[Service]
Type=simple
User=${PANEL_USER}
Group=${PANEL_USER}
SupplementaryGroups=docker
WorkingDirectory=${PANEL_DIR}
Environment=PANEL_GRPC_PORT=${PANEL_GRPC_PORT}
EnvironmentFile=-${ENV_FILE}
ExecStart=${PANEL_DIR}/bin/agent \\
--controller localhost:\${PANEL_GRPC_PORT} \\
--modules-dir ./modules \\
--data-root ./data/instances \\
--meta-dir ./data/instance-meta \\
--backup-dir ./data/backups \\
--cert-dir ./data/certs
Restart=on-failure
RestartSec=3
# Hardening is intentionally lighter than the controller's: the agent manages
# Docker containers and bind-mounts instance data.
NoNewPrivileges=true
${PROTECT_HOME}
ReadWritePaths=${PANEL_DIR}
PrivateTmp=true
LockPersonality=true
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload systemctl daemon-reload
# ---- 6. Controller first boot (CA + admin bootstrap) ---------------------- # ---- 6. Controller first boot (CA + admin bootstrap) ----------------------
@@ -192,7 +328,9 @@ done
curl -fsS "http://127.0.0.1:$PANEL_HTTP_PORT/login" >/dev/null 2>&1 || die "controller did not come up — check: journalctl -u panel-controller" curl -fsS "http://127.0.0.1:$PANEL_HTTP_PORT/login" >/dev/null 2>&1 || die "controller did not come up — check: journalctl -u panel-controller"
ADMIN_EMAIL="admin@panel.local" ADMIN_EMAIL="admin@panel.local"
ADMIN_PW_SHOWN="(set via PANEL_ADMIN_PASSWORD)" # BUG-5b: always give the operator a path to the password. If they supplied one,
# say so and point at the env file; otherwise scrape the generated one below.
ADMIN_PW_SHOWN="(the one you set via PANEL_ADMIN_PASSWORD; also in $ENV_FILE)"
if [ -z "$PANEL_ADMIN_PASSWORD" ]; then if [ -z "$PANEL_ADMIN_PASSWORD" ]; then
if [ "$FIRST_BOOT" = 1 ]; then if [ "$FIRST_BOOT" = 1 ]; then
# The controller prints the generated password once in its log. # The controller prints the generated password once in its log.