panel v0.9.1 — open-source game server manager

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 23:25:11 -07:00
commit 03a281d009
2161 changed files with 300880 additions and 0 deletions
+61
View File
@@ -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"]
+52
View File
@@ -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` | 10006000; 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 25 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.
+114
View File
@@ -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}"
+131
View File
@@ -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"