panel public release

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 01:13:02 -07:00
commit 8a94ffd58f
2165 changed files with 301493 additions and 0 deletions
+326
View File
@@ -0,0 +1,326 @@
# 7 Days to Die — panel-native dedicated server module.
#
# Unlike the earlier vinanrra/LGSM-based revision, this module owns the
# whole stack:
#
# 1. A minimal Debian runtime image (see ./Dockerfile) is built locally
# as panel-7dtd:latest. It contains ONLY the libraries the game binary
# needs — no LGSM, no tmux, no GitHub-fetch-on-start, no surprise
# backups. Just the server process.
# 2. Game files are installed + validated into a Docker volume by the
# panel's generic SteamCMD updater (`agent/internal/updater/steamcmd.go`).
# That sidecar uses the official steamcmd/steamcmd:latest image.
# 3. The entrypoint renders our serverconfig.xml template, drops into the
# `panel` user, and execs startserver.sh under tini with line-buffered
# stdout so progress streams live to the panel console.
#
# First-run flow for operators:
# - Create instance in the panel → container spec created, volumes empty
# - Click "Update" on the instance → SteamCMD sidecar downloads ~17 GB
# - Click "Start" → entrypoint launches 7DTD directly
#
# Subsequent starts skip SteamCMD entirely — instant launch.
id: 7dtd
name: "7 Days to Die"
version: 0.4.0
authors:
- panel contributors
supported_modes:
- docker
# Create the instance but DON'T auto-start it. The operator finishes setup
# (world/RWG settings, cluster join + mod-seed, RefugeBot) and starts it
# manually. This also removes the warm-seed-then-immediate-start race that
# could OOM the agent host. The first SteamCMD update / warm-seed still runs on
# create (auto_update_on_create stays default-true); only the START is
# suppressed — enforced by a shouldAutoStart() guard in the agent's
# onUpdateComplete (the warm-seed completion hook), since that path isn't
# otherwise gated by this flag.
auto_start_on_create: false
runtime:
docker:
# Built locally from ./Dockerfile. Rebuild with:
# docker build -t panel-7dtd:latest modules/7dtd
image: panel-7dtd:latest
# The Files tab exposes TWO roots — operators can switch between them
# with a dropdown at the top of the tab:
#
# Saves & Configs (/game-saves) — world data, serverconfig.xml,
# admin files. Default view.
# Game Files (/game) — binaries + Mods/ folder + Data/.
# Where you drop a mod tarball to install it.
#
# `browseable_root` is kept for backward compat (single-root pre-
# feature clients) but `browseable_roots` wins if both are set.
# Host networking — the container shares the host's network namespace,
# so any port the game OR a mod binds is directly on the LAN. No
# `-p host:container` publishing required; mods like RefugeBot can
# port-hunt freely (the AMP model — drop the .dll, mod sniffs free
# port, LAN_IP:<port> just works).
#
# Tradeoff: every 7DTD on this agent shares lo. The panel's port
# allocator + the env-mapped ports below keep distinct instances
# from clobbering each other on the canonical 7DTD ports.
network_mode: host
browseable_root: /game
browseable_roots:
- name: "Game Files"
path: /game
hint: "AMP-style flat layout: Data/, Mods/, steamapps/, User/, serverconfig.xml"
- name: "Saves & Configs"
path: /game-saves
hint: "Underlying save volume — telnet password, rendered template, raw .local tree"
# Pre-declare the port env keys so the agent's resolve.go filter lets
# panel-allocated values flow through to the container env (only keys
# already present in docker.env get overridden by config_values).
# Defaults match the manifest port defaults below.
env:
GAME_PORT: "26900"
TELNET_PORT: "8081"
WEBDASH_PORT: "8080"
REFUGEBOT_PORT: "8082"
# Cluster membership marker. Empty for standalone servers. The
# controller's 7dtd cluster manager sets this (via config_values)
# when the server joins a cluster; the entrypoint reads it to
# symlink this world's Player save folder into /cluster/Player.
# Pre-declared here so resolve.go's filter lets the override flow
# through (only keys already present in docker.env get overridden).
CLUSTER_ID: ""
# Operator-selected world whose Player folder is shared into
# /cluster/Player ("<World>/<GameName>"). Set via the Cluster tab's
# "Shared player world" picker (config_values). Pre-declared here so
# resolve.go's env filter lets it flow through to the container; the
# entrypoint reads it to override the literal GameWorld/GameName symlink
# target — required for RWG worlds whose save folder is named after the
# generated seed (e.g. "North Tuxera Valley"), not the literal "RWG".
CLUSTER_PLAYER_SAVE: ""
volumes:
# Saves, configs, admin files. Never touched by SteamCMD validate.
- { type: volume, name: "panel-$INSTANCE_ID-saves", container: "/game-saves" }
# Game install — populated by SteamCMD sidecar. Differential updates
# on a named volume avoid Windows NTFS bind-mount misery.
- { type: volume, name: "panel-$INSTANCE_ID-game", container: "/game" }
# Cluster shared dir. Defaults to a private per-instance named volume
# so a standalone server has a harmless empty /cluster. The controller's
# 7dtd cluster manager OVERRIDES this (mount_overrides) to a shared host
# dir when the server joins a cluster, so every member's entrypoint can
# symlink its Player save folder into the same /cluster/Player.
- { type: volume, name: "panel-$INSTANCE_ID-cluster", container: "/cluster" }
# Bind logs to host so operators can tail / rotate.
- { target: "$DATA_PATH/logs", container: "/game-saves/logs" }
# Bind the panel-rendered serverconfig.xml into the saves volume so
# the entrypoint picks it up. Without this, the entrypoint falls back
# to the stock serverconfig.xml shipped with the game (ServerName=
# "My Game Host" etc.) and the operator's config_values are ignored.
- { target: "$DATA_PATH/serverconfig.xml.rendered", container: "/game-saves/serverconfig.xml.rendered", read_only: true }
ports:
# 7DTD always binds three contiguous UDP ports starting at ServerPort:
# the game port, then game+1 (LiteNetLib alt) and game+2 (peer-to-peer
# backup). We only inject `game` into the container env (entrypoint
# patches ServerPort in serverconfig.xml). gameA/gameB are declared so
# the allocator records them as taken — preventing a sibling instance
# from grabbing game+1 or game+2 — but their env field is intentionally
# blank (the binary computes those from ServerPort, not env).
- { name: game, proto: udp, default: 26900, required: true, env: GAME_PORT }
- { name: gameA, proto: udp, default: 26901, required: true }
- { name: gameB, proto: udp, default: 26902, required: true }
- { name: telnet, proto: tcp, default: 8081, internal: true, env: TELNET_PORT }
# Vanilla 7DTD's built-in web dashboard. The TFP entrypoint binds
# this when WebDashboardEnabled=true in serverconfig.xml.
- { name: webdash, proto: tcp, default: 8080, required: false, env: WEBDASH_PORT }
# RefugeBot mod's web UI (Admin Panel + Player Shop). Not required —
# only listens when RefugeBot is installed via Settings → Bundled
# mods. Declared here so the panel allocates a unique host port for
# it (otherwise multiple 7DTDs would all collide on 8082) and Docker
# publishes the binding so LAN clients can reach LAN_IP:<host_port>.
# If a 7DTD instance doesn't have RefugeBot installed, the port is
# still bound but nothing answers — harmless.
- { name: refugebot, proto: tcp, default: 8082, required: false, env: REFUGEBOT_PORT }
resources:
min_ram_mb: 4096
recommended_ram_mb: 8192
# First-start config: the entrypoint copies /game/serverconfig.xml (the one
# shipped with the game install) into /game-saves on first run.
rcon:
adapter: telnet
host_port: telnet
auth: password_prompt
# Our entrypoint generates a random TelnetPassword on first boot (7DTD
# only listens on 0.0.0.0 when a password is set — otherwise it's
# container-loopback only and the agent can't reach it). The plaintext
# lands here inside the container for the agent to re-read on each RCON
# dial attempt.
password_from_file: /game-saves/.panel-telnet-password
commands:
list_players: "lp"
say: "say \"{msg}\""
kick: "kick {player} \"{reason}\""
ban: "ban add {player} {duration} \"{reason}\""
shutdown: "shutdown"
save: "saveworld"
state_sources:
# "lp" (list players) returns "Total of N in the game".
- type: rcon
command: "lp"
every: 30s
parse:
kind: regex
pattern: 'Total of (?P<players>\d+) in the game'
fields:
players_online: players
# 7DTD writes its real game log to /game/output_log_*.txt when invoked
# via startserver.sh. Our entrypoint bypasses that and streams to stdout
# (so docker logs → agent → panel console is the primary path). We still
# watch the log files as a belt-and-braces source for event parsing.
- type: log_tail
files:
- "$DATA_PATH/logs/*.log"
- "/game/output_log_*.txt"
events:
join:
pattern: "PlayerSpawnedInWorld \\(by .*?\\): PltfmId='(?P<platform_id>[^']+)', CrossId='(?P<cross_id>[^']*)', OwnerID='(?P<owner>[^']+)', PlayerName='(?P<name>[^']+)'"
kind: join
leave:
pattern: "Player disconnected: EntityID=\\d+, PltfmId='(?P<platform_id>[^']+)', OwnerID='(?P<owner>[^']+)', PlayerName='(?P<name>[^']+)'"
kind: leave
chat:
pattern: "Chat \\(from '.*?', entity id '\\d+', to '.*?'\\): (?:\\[.*?\\])?'(?P<name>[^']+)': (?P<msg>.*)"
kind: chat
death:
pattern: "GMSG: Player '(?P<name>[^']+)' died"
kind: death
# Render serverconfig.xml from templates/serverconfig.xml.tmpl on every
# (re)create, using the merged operator config_values + generated secrets
# as `.Values.*`. The output lands at $DATA_PATH/serverconfig.xml.rendered
# on the agent host and is bind-mounted into /game-saves/ where the
# entrypoint copies it over the live serverconfig.xml.
config_files:
- path: serverconfig.xml.rendered
format: xml
template: templates/serverconfig.xml.tmpl
# V3.0 "Dead Hot Summer" overhauls serverconfig.xml: ~29 gameplay
# properties are removed and folded into one encoded SandboxCode property
# (the 150 in-game Sandbox Options). A 3.0 server therefore needs a
# different template than ≤2.6. Selection is by the instance's normalized
# Steam branch (resolveCreateBranch): the 3.0 experimental branch normalizes
# to "latest_experimental", so it renders serverconfig.v3.xml.tmpl; every
# other branch (v2.6 default, public, historical) keeps the untouched 2.6
# template. When TFP cuts a frozen `v3.0`/`v3.x` branch near stable, add the
# matching normalized key(s) here — no code change needed (RenderForBranch /
# ConfigFile.TemplateForBranch are data-driven). See
# panel/memory/7dtd-v3-version-and-sandboxcode-plan.md (Workstream D).
template_by_branch:
latest_experimental: templates/serverconfig.v3.xml.tmpl
# 7DTD ships per-version LOCKED Steam branches on app 294420 alongside the
# moving `public` branch. Branch names + frozen buildids confirmed live via
# `steamcmd +app_info_print 294420` on 2026-06-14:
# public -> buildid 22422094 (MOVING default; becomes 3.0 on 2026-06-29)
# v2.6 -> buildid 22422094 ("Version 2.6 Stable" — frozen; == public today)
# v2.5/v2.4/v2.3/v2.0/v1.4/alpha21.2 ... -> older frozen stables
# (latest_experimental + the 3.0 experimental live under privatebranches)
#
# ORDER MATTERS. UpdateProviders[0] is the default the agent uses for
# warm-seed install-path resolution AND for the create-time auto-update when
# no provider_id is chosen (pickProviderFromConfigValues). We put the FROZEN
# `v2_6` branch FIRST so a server that doesn't explicitly pick a version stays
# locked to 2.6 and can NEVER drift onto the moving public branch when it flips
# to 3.0. Pinning a server to `-beta v2.6` freezes it at buildid 22422094
# permanently — the corruption guard the whole V3.0 readiness work is built on.
#
# Operators pick a version at create via the "Game version" dropdown (which
# writes config_values.provider_id); the chosen id maps to the matching
# provider here. See panel/memory/7dtd-v3-version-and-sandboxcode-plan.md.
update_providers:
- id: v2_6 # DEFAULT — Version 2.6 Stable, frozen (buildid 22422094)
kind: steamcmd
app_id: "294420"
beta: "v2.6"
install_path: /game # goes directly into our /game volume
- id: latest_experimental # rolling experimental — V3.0 "Dead Hot Summer" lands here 2026-06-15
kind: steamcmd
app_id: "294420"
beta: latest_experimental
install_path: /game
- id: current # public/stable — MOVING. == 2.6 today; becomes 3.0 on 2026-06-29.
kind: steamcmd
app_id: "294420"
install_path: /game
- id: stable # back-compat alias for `current` (pre-2026-06-14 rows persisted provider_id=stable)
kind: steamcmd
app_id: "294420"
install_path: /game
# Historical version-locked branches — pin here to run an older client.
- id: v2_5
kind: steamcmd
app_id: "294420"
beta: "v2.5"
install_path: /game
- id: v2_4
kind: steamcmd
app_id: "294420"
beta: "v2.4"
install_path: /game
- id: v2_3
kind: steamcmd
app_id: "294420"
beta: "v2.3"
install_path: /game
- id: v2_0
kind: steamcmd
app_id: "294420"
beta: "v2.0"
install_path: /game
- id: v1_4
kind: steamcmd
app_id: "294420"
beta: "v1.4"
install_path: /game
# Backup scope — capture the whole user folder + server config.
#
# 7DTD writes saves, admin files, and titlestorage into the per-user
# tree at $HOME/.local/share/7DaysToDie/ (our entrypoint pins HOME to
# /game-saves so this becomes /game-saves/.local/share/7DaysToDie/).
# We grab that, the rendered serverconfig.xml, the persisted telnet
# password, and any 7DTD-side config under .config — that's everything
# an operator would lose if the volume vanished.
#
# Excluded paths:
# - logs/ — large, never useful for restore
# - Steam/ — Steam runtime cache, regenerated on next start
backup:
# Saves + admin files live in /game-saves, not /game. The file browser
# defaults to /game so operators can see the binary tree, but the
# backup must target /game-saves where the world data actually lives.
root: /game-saves
include:
- .local/share/7DaysToDie
- .config
- serverconfig.xml
- .panel-telnet-password
exclude:
- logs
- Steam
# --- 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: '/StartGame done|"StartGame" finished|GameStartDone|by Telnet from/i'
appearance:
emoji: "🧟"
grad: "linear-gradient(135deg,#7b3b12,#d97706)"
art: "/game-art/7dtd.jpg"
steam: "251570"