0a941f3ba6
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
327 lines
15 KiB
YAML
327 lines
15 KiB
YAML
# 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"
|