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>
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
# panel-native Rust runtime image.
|
||||
#
|
||||
# Rust's dedicated server is a native Linux binary (SteamCMD app 258550),
|
||||
# so no Wine. We need:
|
||||
# - 32-bit runtime libs (RustDedicated is ELF64 but loads steamclient.so
|
||||
# which pulls in 32-bit deps).
|
||||
# - tini for signal forwarding (server does a graceful "Saving..." + exit
|
||||
# on SIGTERM when its RCON is unreachable).
|
||||
# - SDL/GL shims — Rust dedicated server does NOT need a display, but it
|
||||
# dlopens libGL and a couple of X libs at startup; we ship the stubs.
|
||||
#
|
||||
# Expected runtime layout inside the container:
|
||||
# /game — SteamCMD install (panel volume)
|
||||
# /game/RustDedicated — server binary
|
||||
# /game/server/<identity>/ — world + save data (symlinked to /game-saves)
|
||||
# /game-saves — save + logs volume
|
||||
# /entrypoint.sh — boot script (seeds cfg, launches binary)
|
||||
|
||||
FROM debian:12-slim
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
TZ=Etc/UTC \
|
||||
LANG=en_US.UTF-8 \
|
||||
LC_ALL=en_US.UTF-8
|
||||
|
||||
RUN dpkg --add-architecture i386 \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
locales \
|
||||
tini \
|
||||
curl \
|
||||
lib32gcc-s1 \
|
||||
lib32stdc++6 \
|
||||
libc6:i386 \
|
||||
libgdiplus \
|
||||
libgl1 \
|
||||
libx11-6 \
|
||||
libxcursor1 \
|
||||
libxext6 \
|
||||
libxi6 \
|
||||
libxrandr2 \
|
||||
libxrender1 \
|
||||
procps \
|
||||
&& sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen \
|
||||
&& locale-gen \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN groupadd -g 1000 panel \
|
||||
&& useradd -u 1000 -g 1000 -m -s /bin/bash panel \
|
||||
&& mkdir -p /game /game-saves \
|
||||
&& chown -R panel:panel /game /game-saves
|
||||
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
# Rust default ports (see module.yaml comment for reference).
|
||||
EXPOSE 28015/udp 28016/tcp 28082/tcp
|
||||
|
||||
ENTRYPOINT ["/usr/bin/tini", "--", "/entrypoint.sh"]
|
||||
@@ -0,0 +1,52 @@
|
||||
# Rust — panel-native module
|
||||
|
||||
Panel-native Rust dedicated server, native-Linux (no Wine), debian:12-slim base.
|
||||
|
||||
- **Image:** `panel-rust:latest` — built from `./Dockerfile`.
|
||||
- **Game files:** downloaded by the panel's SteamCMD updater sidecar (app 258550) into the `panel-<id>-game` named volume. Fresh install is ~7 GB; first-ever boot additionally generates a ~500 MB map on the save volume depending on `SERVER_WORLD_SIZE`.
|
||||
- **Saves:** `panel-<id>-saves` at `/game-saves`. Entrypoint symlinks `<game>/server` into `/game-saves/server/` so SteamCMD validate can't wipe worlds.
|
||||
- **RCON:** **WebSocket RCON** (Facepunch dropped Source RCON years ago). Panel speaks this via the `websocket_rcon` adapter — `ws://<ip>:28016/<password>` with JSON-framed `{Identifier, Message, Name}` requests. Authenticated via the password in the URL path, not headers.
|
||||
|
||||
## Ports
|
||||
|
||||
| Port | Proto | Purpose |
|
||||
|------|-------|---------|
|
||||
| 28015 | UDP | Main game traffic |
|
||||
| 28016 | TCP | WebSocket RCON (internal) |
|
||||
| 28082 | TCP | Rust+ push notifications (optional — only needed for the companion app) |
|
||||
|
||||
## RCON commands wired
|
||||
|
||||
- `list` → `playerlist`
|
||||
- `broadcast {msg}` → `say {msg}`
|
||||
- `kick {player}` → `kick "{player}"`
|
||||
- `ban {player}` → `banid {player}`
|
||||
- `save` → `server.save`
|
||||
- `shutdown` → `quit`
|
||||
|
||||
## Log events captured
|
||||
|
||||
- **Join:** `<name>[<pid>/<steamid>] has entered the game`
|
||||
- **Leave:** `<steamid>/<name> disconnecting: <reason>`
|
||||
|
||||
## Environment vars
|
||||
|
||||
| Var | Default | Notes |
|
||||
|-----|---------|-------|
|
||||
| `SERVER_NAME` | `panel Rust` | Shown in the Rust server browser |
|
||||
| `SERVER_DESCRIPTION` | `Powered by panel` | Browser description / tooltip |
|
||||
| `SERVER_IDENTITY` | `panel-rust` | Save-tree subdirectory name |
|
||||
| `SERVER_SEED` | `12345` | Procedural-gen seed |
|
||||
| `SERVER_WORLD_SIZE` | `3000` | 1000–6000; smaller = faster |
|
||||
| `MAX_PLAYERS` | `50` | |
|
||||
| `RCON_PASSWORD` | `panel_rcon` | RCON auth (URL path) |
|
||||
| `SERVER_LEVEL` | `Procedural Map` | Map preset |
|
||||
| `SERVER_URL` | `` | Website URL shown in the browser |
|
||||
| `SERVER_HEADER_IMAGE` | `` | 512×256 banner image URL |
|
||||
|
||||
## Known gotchas
|
||||
|
||||
- First-ever boot takes 2–5 min to generate the world. No players can connect until generation finishes (log: `Server startup complete` is the signal).
|
||||
- RCON port is published on the container only (panel talks to it over the Docker bridge). Don't expose 28016 externally — password in URL means anyone who sees the URL has full server control.
|
||||
- If you change `SERVER_WORLD_SIZE` or `SERVER_SEED` on a running server, the next boot wipes the existing world. Rust ties the world to a seed+size pair per identity.
|
||||
- `libgl1` + a handful of X libs are installed in the image even though the server runs headless; Rust's Unity runtime `dlopen`s them at startup.
|
||||
@@ -0,0 +1,114 @@
|
||||
#!/bin/bash
|
||||
# panel-native Rust entrypoint.
|
||||
#
|
||||
# Contract:
|
||||
# /game — SteamCMD-installed Rust Dedicated Server (app 258550).
|
||||
# Populated by the panel's SteamCMD updater sidecar.
|
||||
# /game-saves — world/save data + logs volume.
|
||||
#
|
||||
# Stage 1 runs as root: chown volumes, symlink server/<identity> into the
|
||||
# save volume, then drop to the `panel` user via setpriv.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
log() { printf '[panel-rust] %s\n' "$*"; }
|
||||
|
||||
# ----- stage 1: root-only bootstrap -----
|
||||
if [[ "$(id -u)" = "0" ]]; then
|
||||
chown -R panel:panel /game /game-saves /home/panel 2>/dev/null || true
|
||||
|
||||
log "dropping privileges to panel (UID 1000)"
|
||||
exec setpriv --reuid=panel --regid=panel --clear-groups \
|
||||
--inh-caps=-all --bounding-set=-all \
|
||||
env \
|
||||
HOME=/home/panel \
|
||||
SERVER_NAME="${SERVER_NAME:-panel Rust}" \
|
||||
SERVER_DESCRIPTION="${SERVER_DESCRIPTION:-Powered by panel}" \
|
||||
SERVER_IDENTITY="${SERVER_IDENTITY:-panel-rust}" \
|
||||
SERVER_SEED="${SERVER_SEED:-12345}" \
|
||||
SERVER_WORLD_SIZE="${SERVER_WORLD_SIZE:-3000}" \
|
||||
MAX_PLAYERS="${MAX_PLAYERS:-50}" \
|
||||
RCON_PASSWORD="${RCON_PASSWORD:-panel_rcon}" \
|
||||
SERVER_URL="${SERVER_URL:-}" \
|
||||
SERVER_HEADER_IMAGE="${SERVER_HEADER_IMAGE:-}" \
|
||||
SERVER_LEVEL="${SERVER_LEVEL:-Procedural Map}" \
|
||||
GAME_PORT="${GAME_PORT:-28015}" \
|
||||
RCON_PORT="${RCON_PORT:-28016}" \
|
||||
RUSTPLUS_PORT="${RUSTPLUS_PORT:-28082}" \
|
||||
LD_LIBRARY_PATH="/game:/game/RustDedicated_Data/Plugins:/game/RustDedicated_Data/Plugins/x86_64:${LD_LIBRARY_PATH:-}" \
|
||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
|
||||
/entrypoint.sh "$@"
|
||||
fi
|
||||
|
||||
GAME_DIR=/game
|
||||
SAVE_DIR=/game-saves
|
||||
DEDI_EXE="${GAME_DIR}/RustDedicated"
|
||||
|
||||
if [[ ! -x "${DEDI_EXE}" ]]; then
|
||||
log "ERROR: ${DEDI_EXE} not found or not executable."
|
||||
log " Run 'Update' on this server in the panel — that launches a"
|
||||
log " SteamCMD sidecar which installs Rust (app 258550) here."
|
||||
exit 78 # EX_CONFIG
|
||||
fi
|
||||
|
||||
# --- persist server/<identity>/ into the save volume ---
|
||||
# RustDedicated writes its world + save + cfg tree to <game>/server/<identity>.
|
||||
# First boot moves it to /game-saves + symlinks back, so SteamCMD validate
|
||||
# can't clobber world data. Idempotent on re-boot.
|
||||
mkdir -p "${SAVE_DIR}/server"
|
||||
if [[ -d "${GAME_DIR}/server" && ! -L "${GAME_DIR}/server" ]]; then
|
||||
# On fresh install this dir doesn't exist; server creates it on first boot.
|
||||
# If it does exist (e.g. from an earlier panel image without the symlink),
|
||||
# migrate and replace.
|
||||
if [[ ! -d "${SAVE_DIR}/server/${SERVER_IDENTITY}" ]]; then
|
||||
mv "${GAME_DIR}/server"/* "${SAVE_DIR}/server/" 2>/dev/null || true
|
||||
fi
|
||||
rm -rf "${GAME_DIR}/server"
|
||||
fi
|
||||
ln -sfn "${SAVE_DIR}/server" "${GAME_DIR}/server"
|
||||
|
||||
# Mirror logs to the bind-mounted logs dir for external access.
|
||||
mkdir -p "${SAVE_DIR}/logs"
|
||||
|
||||
cd "${GAME_DIR}"
|
||||
log "starting Rust Dedicated Server"
|
||||
log " name: ${SERVER_NAME}"
|
||||
log " identity: ${SERVER_IDENTITY}"
|
||||
log " level: ${SERVER_LEVEL}"
|
||||
log " seed: ${SERVER_SEED}"
|
||||
log " size: ${SERVER_WORLD_SIZE}"
|
||||
log " max: ${MAX_PLAYERS}"
|
||||
log " game port: ${GAME_PORT}/udp"
|
||||
log " rcon port: ${RCON_PORT}/tcp (internal, websocket)"
|
||||
log " rust+: ${RUSTPLUS_PORT}/tcp (push, optional)"
|
||||
|
||||
trap 'log "SIGTERM received; stopping server"; kill -TERM "${RUST_PID:-0}" 2>/dev/null || true; wait "${RUST_PID:-0}" 2>/dev/null || true; exit 0' TERM INT
|
||||
|
||||
# Rust's own flags — all `+` prefixed. `-batchmode` / `-nographics` come
|
||||
# before the `+` convars. We intentionally omit `-logFile`: Unity defaults
|
||||
# to stdout when absent, and passing `/dev/stdout` or `-` makes RustDedicated
|
||||
# fail with "Unable to open log file, exiting." on Linux-container Unity.
|
||||
exec stdbuf -oL -eL "${DEDI_EXE}" \
|
||||
-batchmode \
|
||||
-nographics \
|
||||
+server.ip 0.0.0.0 \
|
||||
+server.port "${GAME_PORT}" \
|
||||
+server.tickrate 10 \
|
||||
+server.hostname "${SERVER_NAME}" \
|
||||
+server.description "${SERVER_DESCRIPTION}" \
|
||||
+server.identity "${SERVER_IDENTITY}" \
|
||||
+server.level "${SERVER_LEVEL}" \
|
||||
+server.seed "${SERVER_SEED}" \
|
||||
+server.worldsize "${SERVER_WORLD_SIZE}" \
|
||||
+server.maxplayers "${MAX_PLAYERS}" \
|
||||
+server.saveinterval 600 \
|
||||
+server.url "${SERVER_URL}" \
|
||||
+server.headerimage "${SERVER_HEADER_IMAGE}" \
|
||||
+rcon.web 1 \
|
||||
+rcon.ip 0.0.0.0 \
|
||||
+rcon.port "${RCON_PORT}" \
|
||||
+rcon.password "${RCON_PASSWORD}" \
|
||||
+app.port "${RUSTPLUS_PORT}" &
|
||||
|
||||
RUST_PID=$!
|
||||
wait "${RUST_PID}"
|
||||
@@ -0,0 +1,131 @@
|
||||
# Rust (Facepunch Studios) dedicated server module.
|
||||
#
|
||||
# Native Linux binary via SteamCMD app 258550. Uses WebSocket RCON (Facepunch
|
||||
# dropped Source RCON years ago), which the panel speaks via the
|
||||
# `websocket_rcon` adapter added in this module's first shipping session.
|
||||
#
|
||||
# Reference for flags + ports: Rust dedicated server wiki + widespread
|
||||
# community configs. No AMP template exists for Rust in CubeCoders' public
|
||||
# repo (Rust is in AMP's built-in module set, not community-contributed).
|
||||
|
||||
id: rust
|
||||
name: "Rust"
|
||||
version: 0.1.0
|
||||
authors:
|
||||
- panel contributors
|
||||
|
||||
supported_modes:
|
||||
- docker
|
||||
|
||||
runtime:
|
||||
docker:
|
||||
# Built locally from ./Dockerfile. Rebuild with:
|
||||
# docker build -t panel-rust:latest modules/rust
|
||||
image: panel-rust:latest
|
||||
# Host networking -- container shares the host net namespace, so
|
||||
# any port the game/mod binds is directly on the LAN. AMP-like
|
||||
# model. Multi-instance safety comes from env-driven per-instance
|
||||
# ports (panel allocator + env: mapping on each ports entry).
|
||||
network_mode: host
|
||||
browseable_root: /game-saves
|
||||
browseable_roots:
|
||||
- name: "Saves & Configs"
|
||||
path: /game-saves
|
||||
hint: "server/<identity>/ save data, cfg/, user bans + mutes, oxide/"
|
||||
- name: "Game Files"
|
||||
path: /game
|
||||
hint: "RustDedicated binary + Bundles/ + shipped configs"
|
||||
env:
|
||||
SERVER_NAME: "panel Rust"
|
||||
SERVER_DESCRIPTION: "Powered by panel"
|
||||
SERVER_IDENTITY: "panel-rust"
|
||||
SERVER_SEED: "12345"
|
||||
SERVER_WORLD_SIZE: "3000" # smaller = faster load; default 4000
|
||||
MAX_PLAYERS: "50"
|
||||
RCON_PASSWORD: "panel_rcon"
|
||||
SERVER_URL: ""
|
||||
SERVER_HEADER_IMAGE: ""
|
||||
# Level selector: Procedural Map (default), Barren, HapisIsland, SavasIsland, CraggyIsland
|
||||
SERVER_LEVEL: "Procedural Map"
|
||||
# Pre-declared so allocator-derived ports survive resolve.go's filter.
|
||||
GAME_PORT: "28015"
|
||||
RCON_PORT: "28016"
|
||||
RUSTPLUS_PORT: "28082"
|
||||
volumes:
|
||||
- { type: volume, name: "panel-$INSTANCE_ID-saves", container: "/game-saves" }
|
||||
- { type: volume, name: "panel-$INSTANCE_ID-game", container: "/game" }
|
||||
- { target: "$DATA_PATH/logs", container: "/game-saves/logs" }
|
||||
|
||||
# Rust ports:
|
||||
# 28015/udp — main game traffic (required)
|
||||
# 28015/tcp — app port for some tooling (optional; not published by default)
|
||||
# 28016/tcp — WebSocket RCON (internal; panel talks to it)
|
||||
# 28082/tcp — Rust+ push notifications (optional)
|
||||
ports:
|
||||
- { name: game, proto: udp, default: 28015, required: true, env: GAME_PORT }
|
||||
- { name: rcon, proto: tcp, default: 28016, internal: true, env: RCON_PORT }
|
||||
- { name: rustplus, proto: tcp, default: 28082, required: false, env: RUSTPLUS_PORT }
|
||||
|
||||
resources:
|
||||
min_ram_mb: 6144
|
||||
recommended_ram_mb: 12288
|
||||
|
||||
# Per-instance generated RCON secret. The agent generates a random
|
||||
# 24-byte token at create time (pkg/module GenerateSecrets), which
|
||||
# overrides the env default for RCON_PASSWORD below — so every instance
|
||||
# gets a unique password. The weak env default only applies to
|
||||
# containers launched outside the panel.
|
||||
secrets:
|
||||
- name: RCON_PASSWORD
|
||||
description: "Rust RCON password — generated per instance"
|
||||
generated: true
|
||||
|
||||
rcon:
|
||||
adapter: websocket_rcon
|
||||
host_port: rcon
|
||||
auth: password_in_url
|
||||
password_secret: RCON_PASSWORD
|
||||
# Legacy fallback only (pre-generated-secret instances). New instances
|
||||
# always get the generated RCON_PASSWORD secret above.
|
||||
password_literal: "panel_rcon"
|
||||
commands:
|
||||
list: "playerlist"
|
||||
broadcast: "say {msg}"
|
||||
kick: "kick \"{player}\""
|
||||
ban: "banid {player}"
|
||||
save: "server.save"
|
||||
shutdown: "quit"
|
||||
|
||||
state_sources:
|
||||
- type: rcon
|
||||
command: "playerlist"
|
||||
every: 60s
|
||||
|
||||
# Log-line event patterns (RE2; named groups use (?P<name>...)).
|
||||
# Rust prints join/leave to both stdout and the save-side log; stdout is
|
||||
# what the log pump tails.
|
||||
events:
|
||||
join:
|
||||
pattern: '(?P<name>.+?)\[[0-9]+/(?P<id>\d{17})\] has entered the game'
|
||||
kind: join
|
||||
leave:
|
||||
pattern: '(?P<id>\d{17})/(?P<name>.+?) disconnecting: (?P<reason>.+)$'
|
||||
kind: leave
|
||||
|
||||
update_providers:
|
||||
- id: stable
|
||||
kind: steamcmd
|
||||
app_id: "258550"
|
||||
install_path: /game
|
||||
|
||||
# --- Manifest-driven UI metadata (WI-07) --------------------------
|
||||
# Consumed by the CONTROLLER/dashboard only (the controller loads its
|
||||
# own ./modules copy at startup); the agent ignores these at runtime.
|
||||
# ready_pattern is a JavaScript regex (slash-delimited when it needs
|
||||
# flags) moved verbatim from the dashboard's READY_PATTERNS map.
|
||||
ready_pattern: '/Server startup complete|Listening on 0\.0\.0\.0:28015/i'
|
||||
appearance:
|
||||
emoji: "🔧"
|
||||
grad: "linear-gradient(135deg,#8b4a2b,#d97706)"
|
||||
art: "/game-art/rust.jpg"
|
||||
steam: "252490"
|
||||
Reference in New Issue
Block a user