panel v0.9.2 — open-source game server manager
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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"
|
||||
Reference in New Issue
Block a user