#!/usr/bin/env bash # Panel one-line installer — controller + agent + Postgres on a single Linux box. # # curl -fsSL https://git.pdxtechs.com/dbledeez/panel/raw/branch/main/install.sh | sudo bash # # Non-interactive knobs (env vars, all optional): # 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_VERSION version string baked into the binaries (default: git describe || dev) # PANEL_DIR install prefix (default: /opt/panel) # PANEL_DATA_DIR data directory (default: $PANEL_DIR/data) # PANEL_HTTP_PORT dashboard port (default: 8080) # PANEL_GRPC_PORT agent gRPC port (default: 8443) # 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_PASSWORD password for the managed panel-postgres container (default: generated) # PANEL_SKIP_AGENT=1 install controller only (add agents on other boxes later) # # Idempotent: safe to re-run — existing containers, units, CA, DB and # passwords are kept; binaries and modules are refreshed. set -euo pipefail PANEL_REPO_URL="${PANEL_REPO_URL:-https://git.pdxtechs.com/dbledeez/panel}" PANEL_PREBUILT_URL="${PANEL_PREBUILT_URL:-}" PANEL_DIR="${PANEL_DIR:-/opt/panel}" PANEL_DATA_DIR="${PANEL_DATA_DIR:-$PANEL_DIR/data}" PANEL_HTTP_PORT="${PANEL_HTTP_PORT:-8080}" PANEL_GRPC_PORT="${PANEL_GRPC_PORT:-8443}" PANEL_ADMIN_PASSWORD="${PANEL_ADMIN_PASSWORD:-}" PANEL_DB_URL="${PANEL_DB_URL:-}" PANEL_DB_PASSWORD="${PANEL_DB_PASSWORD:-}" PANEL_SKIP_AGENT="${PANEL_SKIP_AGENT:-0}" PANEL_USER=panel PG_CONTAINER=panel-postgres log() { printf '\033[1;32m[panel]\033[0m %s\n' "$*"; } warn() { printf '\033[1;33m[panel]\033[0m %s\n' "$*" >&2; } die() { printf '\033[1;31m[panel]\033[0m ERROR: %s\n' "$*" >&2; exit 1; } [ "$(id -u)" = 0 ] || die "run as root (sudo bash install.sh)" command -v curl >/dev/null || die "curl is required" # ---- 1. Distro detection ------------------------------------------------- DISTRO=unknown if [ -r /etc/os-release ]; then # shellcheck disable=SC1091 . /etc/os-release DISTRO="${ID:-unknown}" fi log "distro: $DISTRO" # ---- 2. Docker ----------------------------------------------------------- if command -v docker >/dev/null 2>&1; then log "docker present: $(docker --version)" else case "$DISTRO" in ubuntu|debian|fedora|centos|rhel|raspbian) log "installing Docker via get.docker.com" curl -fsSL https://get.docker.com | sh ;; *) die "Docker not found and no auto-install path for '$DISTRO'. Install Docker Engine first: https://docs.docker.com/engine/install/ then re-run." ;; esac fi systemctl enable --now docker >/dev/null 2>&1 || true docker info >/dev/null 2>&1 || die "docker daemon is not running/reachable" # ---- 3. Postgres --------------------------------------------------------- mkdir -p "$PANEL_DIR/bin" "$PANEL_DATA_DIR" ENV_FILE="$PANEL_DIR/panel.env" if [ -n "$PANEL_DB_URL" ]; then log "using external Postgres from \$PANEL_DB_URL" else if docker inspect "$PG_CONTAINER" >/dev/null 2>&1; then log "postgres container '$PG_CONTAINER' already exists — starting" docker start "$PG_CONTAINER" >/dev/null # Recover the password persisted by a previous run. if [ -z "$PANEL_DB_PASSWORD" ] && [ -f "$ENV_FILE" ]; then PANEL_DB_PASSWORD="$(sed -n 's/^PANEL_DB_PASSWORD=//p' "$ENV_FILE" | head -n1)" fi [ -n "$PANEL_DB_PASSWORD" ] || die "existing $PG_CONTAINER but no PANEL_DB_PASSWORD (set it, or remove the container to start fresh)" else if [ -z "$PANEL_DB_PASSWORD" ]; then PANEL_DB_PASSWORD="$(head -c 24 /dev/urandom | od -An -tx1 | tr -d ' \n')" fi log "starting postgres container '$PG_CONTAINER'" docker run -d --name "$PG_CONTAINER" --restart unless-stopped \ -e POSTGRES_USER=panel -e POSTGRES_PASSWORD="$PANEL_DB_PASSWORD" -e POSTGRES_DB=panel \ -p 127.0.0.1:5432:5432 \ -v panel_pgdata:/var/lib/postgresql/data \ postgres:17-alpine >/dev/null fi PANEL_DB_URL="postgres://panel:${PANEL_DB_PASSWORD}@127.0.0.1:5432/panel?sslmode=disable" log "waiting for postgres to accept connections" for _ in $(seq 1 30); do if docker exec "$PG_CONTAINER" pg_isready -U panel -d panel >/dev/null 2>&1; then break; fi sleep 1 done docker exec "$PG_CONTAINER" pg_isready -U panel -d panel >/dev/null 2>&1 || die "postgres did not become ready" fi # ---- 4. Source / binaries ------------------------------------------------ SRC="" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" >/dev/null 2>&1 && pwd || true)" if [ -n "$SCRIPT_DIR" ] && [ -f "$SCRIPT_DIR/go.work" ] && [ -d "$SCRIPT_DIR/controller" ]; then SRC="$SCRIPT_DIR" log "using local checkout at $SRC" fi if [ -n "$PANEL_PREBUILT_URL" ]; then log "downloading prebuilt binaries: $PANEL_PREBUILT_URL" TMP_TGZ="$(mktemp)" curl -fsSL "$PANEL_PREBUILT_URL" -o "$TMP_TGZ" tar -xzf "$TMP_TGZ" -C "$PANEL_DIR" rm -f "$TMP_TGZ" [ -x "$PANEL_DIR/bin/controller" ] || die "prebuilt tarball did not contain bin/controller" else if [ -z "$SRC" ]; then command -v git >/dev/null || die "git is required to fetch sources (or set PANEL_PREBUILT_URL)" SRC="$PANEL_DIR/src" if [ -d "$SRC/.git" ]; then log "updating source checkout at $SRC" git -C "$SRC" pull --ff-only else log "cloning $PANEL_REPO_URL" git clone --depth 1 "$PANEL_REPO_URL" "$SRC" 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." 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" log "building panel $VERSION" (cd "$SRC" && \ CGO_ENABLED=0 go build -trimpath -ldflags "$LDFLAGS" -o "$PANEL_DIR/bin/controller" ./controller/cmd/controller && \ CGO_ENABLED=0 go build -trimpath -ldflags "$LDFLAGS" -o "$PANEL_DIR/bin/agent" ./agent/cmd/agent && \ CGO_ENABLED=0 go build -trimpath -ldflags "$LDFLAGS" -o "$PANEL_DIR/bin/panelctl" ./controller/cmd/panelctl) log "syncing game modules" rm -rf "$PANEL_DIR/modules.new" cp -r "$SRC/modules" "$PANEL_DIR/modules.new" rm -rf "$PANEL_DIR/modules" mv "$PANEL_DIR/modules.new" "$PANEL_DIR/modules" fi # ---- 5. User + env file + systemd units ---------------------------------- if ! id "$PANEL_USER" >/dev/null 2>&1; then useradd --system --home-dir "$PANEL_DIR" --shell /usr/sbin/nologin "$PANEL_USER" fi usermod -aG docker "$PANEL_USER" 2>/dev/null || true umask 077 { echo "PANEL_DB_URL=$PANEL_DB_URL" [ -n "$PANEL_DB_PASSWORD" ] && echo "PANEL_DB_PASSWORD=$PANEL_DB_PASSWORD" [ -n "$PANEL_ADMIN_PASSWORD" ] && echo "PANEL_ADMIN_PASSWORD=$PANEL_ADMIN_PASSWORD" echo "PANEL_HTTP_PORT=$PANEL_HTTP_PORT" echo "PANEL_GRPC_PORT=$PANEL_GRPC_PORT" } > "$ENV_FILE" umask 022 chown -R "$PANEL_USER:$PANEL_USER" "$PANEL_DIR" chmod 600 "$ENV_FILE" if [ -n "$SRC" ] && [ -d "$SRC/deploy/systemd" ]; then install -m 0644 "$SRC/deploy/systemd/panel-controller.service" \ "$SRC/deploy/systemd/panel-agent.service" /etc/systemd/system/ else # Prebuilt path: units are expected inside the tarball at deploy/systemd. [ -f "$PANEL_DIR/deploy/systemd/panel-controller.service" ] || die "no systemd units found (deploy/systemd missing)" install -m 0644 "$PANEL_DIR/deploy/systemd/panel-controller.service" \ "$PANEL_DIR/deploy/systemd/panel-agent.service" /etc/systemd/system/ fi systemctl daemon-reload # ---- 6. Controller first boot (CA + admin bootstrap) ---------------------- FIRST_BOOT=1 if [ -f "$PANEL_DATA_DIR/ca/root.crt" ] || [ -d "$PANEL_DATA_DIR/ca" ]; then FIRST_BOOT=0 fi BOOT_MARK="$(date '+%Y-%m-%d %H:%M:%S')" systemctl enable --now panel-controller >/dev/null systemctl restart panel-controller log "waiting for the controller on :$PANEL_HTTP_PORT" for _ in $(seq 1 60); do if curl -fsS "http://127.0.0.1:$PANEL_HTTP_PORT/login" >/dev/null 2>&1; then break fi sleep 1 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" ADMIN_EMAIL="admin@panel.local" ADMIN_PW_SHOWN="(set via PANEL_ADMIN_PASSWORD)" if [ -z "$PANEL_ADMIN_PASSWORD" ]; then if [ "$FIRST_BOOT" = 1 ]; then # The controller prints the generated password once in its log. GEN_PW="$(journalctl -u panel-controller --since "$BOOT_MARK" --no-pager 2>/dev/null \ | sed -n 's/.*password: \([a-f0-9]\{16\}\).*/\1/p' | head -n1 || true)" if [ -n "$GEN_PW" ]; then ADMIN_PW_SHOWN="$GEN_PW" PANEL_ADMIN_PASSWORD="$GEN_PW" else ADMIN_PW_SHOWN="(see: journalctl -u panel-controller | grep 'password:')" fi else ADMIN_PW_SHOWN="(unchanged from previous install)" fi fi # ---- 7. Pair token + local agent ------------------------------------------ PAIR_TOKEN="" if [ -n "$PANEL_ADMIN_PASSWORD" ]; then COOKIES="$(mktemp)" if curl -fsS -c "$COOKIES" -H 'Content-Type: application/json' \ -d "{\"email\":\"$ADMIN_EMAIL\",\"password\":\"$PANEL_ADMIN_PASSWORD\"}" \ "http://127.0.0.1:$PANEL_HTTP_PORT/api/login" >/dev/null 2>&1; then PAIR_TOKEN="$(curl -fsS -b "$COOKIES" -H 'Content-Type: application/json' \ -d '{"description":"install.sh local agent","expires_minutes":60}' \ "http://127.0.0.1:$PANEL_HTTP_PORT/api/admin/pair-tokens" 2>/dev/null \ | sed -n 's/.*"token":"\([^"]*\)".*/\1/p' || true)" fi rm -f "$COOKIES" fi PAIR_CMD="$PANEL_DIR/bin/agent --pair-url http://:$PANEL_HTTP_PORT --pair-token --cert-dir ./data/certs" if [ "$PANEL_SKIP_AGENT" != 1 ]; then if [ -f "$PANEL_DATA_DIR/certs/agent.crt" ]; then log "local agent already paired — restarting" systemctl enable --now panel-agent >/dev/null systemctl restart panel-agent elif [ -n "$PAIR_TOKEN" ]; then log "pairing local agent" (cd "$PANEL_DIR" && sudo -u "$PANEL_USER" \ "$PANEL_DIR/bin/agent" --pair-url "http://127.0.0.1:$PANEL_HTTP_PORT" \ --pair-token "$PAIR_TOKEN" --cert-dir ./data/certs) systemctl enable --now panel-agent >/dev/null else warn "could not auto-create a pair token (admin password unknown to this run)." warn "Create one in the dashboard (Admin → Pair tokens) and run:" warn " cd $PANEL_DIR && sudo -u $PANEL_USER $PAIR_CMD" warn "then: systemctl enable --now panel-agent" fi fi # ---- 8. Summary ------------------------------------------------------------ HOST_IP="$(hostname -I 2>/dev/null | awk '{print $1}' || echo 127.0.0.1)" echo log "──────────────────────────────────────────────────────────" log " Panel is installed." log " Dashboard : http://${HOST_IP:-127.0.0.1}:$PANEL_HTTP_PORT" log " Login : $ADMIN_EMAIL" log " Password : $ADMIN_PW_SHOWN" log " (log in once and change the password in the UI)" log "" log " To pair an agent on ANOTHER machine, create a pair token in the" log " dashboard, copy the panel binaries + modules/ there, then run:" log " $PAIR_CMD" log " and start it: ./bin/agent --controller ${HOST_IP:-}:$PANEL_GRPC_PORT --cert-dir ./data/certs" log "──────────────────────────────────────────────────────────"