4ccccc6fe2
Self-hostable game server control panel: Go controller + agent, 26 game modules, embedded web UI. One-line install via install.sh / install.ps1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
380 lines
19 KiB
YAML
380 lines
19 KiB
YAML
# ARK: Survival Ascended — dedicated server module.
|
||
#
|
||
# ASA has no native Linux server binary — it runs the Windows build under
|
||
# Proton. Rather than bundle our own 500 MB+ Wine/Proton image, this module
|
||
# uses `acekorneya/asa_server:latest`, the most actively-maintained
|
||
# community image for ASA on Linux. It packages:
|
||
#
|
||
# - Proton + Wine runtime
|
||
# - SteamCMD (handles install + auto-update on start)
|
||
# - An entrypoint that reads env vars and assembles the ASA command line
|
||
# - BattlEye / RCON / cluster flags
|
||
#
|
||
# So: the SteamCMD-style "download game via our sidecar" pattern we use for
|
||
# 7DTD doesn't apply here — the image downloads ASA itself on first boot
|
||
# via its internal SteamCMD. No external update provider needed, which is
|
||
# why this module has NO `update_providers:` section (the `Update` button
|
||
# won't do anything useful for ASA; just restart the server to pull game
|
||
# patches — the image auto-updates when UPDATE_SERVER=true is set).
|
||
#
|
||
# Operators who want to own the stack can publish their own image (Proton
|
||
# + SteamCMD + ASA) and swap `image:` below. Keep the env vars the same —
|
||
# the community entrypoint reads `SESSION_NAME`, `SERVER_ADMIN_PASSWORD`,
|
||
# `SERVER_MAP` etc.
|
||
#
|
||
# First-run flow:
|
||
# 1. Create the server in the panel → empty container + volume.
|
||
# 2. Click Start → the image's entrypoint runs SteamCMD inside the
|
||
# container and downloads ~18 GB of ASA into /home/pok/arkserver.
|
||
# First boot takes 15–30 minutes depending on bandwidth.
|
||
# 3. After the install finishes the server actually starts. Open Console
|
||
# and wait for "Server started on port 7777".
|
||
#
|
||
# Subsequent starts are quick (<= 2 min) since the files are cached on the
|
||
# named volume.
|
||
|
||
id: ark-sa
|
||
name: "ARK: Survival Ascended"
|
||
version: 0.3.0
|
||
authors:
|
||
- panel contributors
|
||
|
||
supported_modes:
|
||
- docker
|
||
|
||
# The acekorneya image handles its own SteamCMD download on first Start
|
||
# (UPDATE_SERVER=TRUE env). Opt out of the panel's auto-update-on-create
|
||
# sidecar — its install path doesn't match the image's layout and would
|
||
# just waste ~20 minutes creating files the server won't use.
|
||
auto_update_on_create: false
|
||
# But DO auto-start after create — the image's entrypoint runs its own
|
||
# SteamCMD on first boot and then launches the server, so Create → Start
|
||
# is the one-gesture flow operators expect. (True is the default across
|
||
# all modules; declared here so the interaction with auto_update_on_create
|
||
# being false is explicit to anyone reading this file.)
|
||
auto_start_on_create: true
|
||
|
||
runtime:
|
||
docker:
|
||
image: acekorneya/asa_server: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
|
||
env:
|
||
TZ: "Etc/UTC"
|
||
# acekorneya's entrypoint expects these. They're defaults — operators
|
||
# override per-instance once the config-values layer is wired.
|
||
INSTANCE_NAME: "panel-asa"
|
||
SERVER_MAP: "TheIsland_WP"
|
||
SESSION_NAME: "Panel ARK SA Server"
|
||
SERVER_ADMIN_PASSWORD: "changeme-please"
|
||
SERVER_PASSWORD: ""
|
||
ASA_PORT: "7777"
|
||
# Steam query port. The agent rewrites this to the actually-allocated
|
||
# host port at create time (see resolve.go's port-env mapping) — for
|
||
# the first ARK on an agent this stays 27015; the second gets 27016
|
||
# or whatever the allocator picked. The acekorneya image translates
|
||
# this into the launch arg `?QueryPort=<n>` on its end.
|
||
QUERY_PORT: "27015"
|
||
RCON_PORT: "27020"
|
||
MAX_PLAYERS: "70"
|
||
# Pull any ASA patches from Steam on every start. Cheap when up to
|
||
# date (SteamCMD validate is a few seconds) and means the server is
|
||
# always current.
|
||
UPDATE_SERVER: "TRUE"
|
||
CHECK_FOR_UPDATE_INTERVAL: "24"
|
||
UPDATE_WINDOW_MINIMUM_TIME: "12:00 AM"
|
||
UPDATE_WINDOW_MAXIMUM_TIME: "11:59 PM"
|
||
RESTART_NOTICE_MINUTES: "30"
|
||
ENABLE_MOTD: "FALSE"
|
||
MOTD: "Welcome!"
|
||
MOTD_DURATION: "30"
|
||
MAP_NAME: "TheIsland"
|
||
NOTIFY_ADMIN_COMMANDS_IN_CHAT: "FALSE"
|
||
RANDOM_STARTUP_DELAY: "FALSE"
|
||
# BattlEye under Wine/Proton on Linux containers silently crashes the
|
||
# server ~2-3 minutes after "Server has successfully started!" (no
|
||
# log output between the success line and the process exit — classic
|
||
# UE5 silent death on BE handshake failure). Operators can enable it
|
||
# per-instance once BE for ASA/Linux gets sorted upstream; for now
|
||
# the default is off so servers actually stay up.
|
||
BATTLEEYE: "FALSE"
|
||
RCON_ENABLED: "TRUE"
|
||
DISPLAY_POK_MONITOR_MESSAGE: "FALSE"
|
||
MOD_IDS: ""
|
||
PASSIVE_MODS: ""
|
||
CUSTOM_SERVER_ARGS: ""
|
||
CLUSTER_ID: ""
|
||
# ----- EOS / session-browser stability knobs (see post_start) -----
|
||
# ASA reports the host's auto-detected adapter IP to Epic Online
|
||
# Services when it registers the server session for the in-game
|
||
# browser. Under Docker (host-networking, multiple bridges, IPv6
|
||
# scopes) EOS sometimes picks the wrong adapter on a re-registration
|
||
# and advertises an unreachable IP. The server stays up + RCON works
|
||
# + connect-by-IP works, but it vanishes from the browser. ASA
|
||
# exposes `-PublicIPForEpic=<ipv4>` to override the auto-detect.
|
||
# Leave empty to keep ASA's default behavior; set to the WAN IPv4
|
||
# players reach (the WG-edge VPS for this fleet) to pin EOS to it.
|
||
# Reference: forums.unrealengine.com EOS-DS browser-visibility thread.
|
||
PUBLIC_IP_FOR_EPIC: ""
|
||
# Run as UID/GID 1000 so writes land on the volume as a normal user.
|
||
PUID: "1000"
|
||
PGID: "1000"
|
||
# The acekorneya image runs ARK under Wine — the live install lives at
|
||
# the Wine-virtualized Windows path below. Early module versions mounted
|
||
# /home/pok/arkserver thinking it was a symlink; it isn't — that dir
|
||
# doesn't exist at image build time, so the volume was essentially
|
||
# unused, and saves/configs lived only in the container's ephemeral
|
||
# overlay FS. Fixed by mounting DIRECTLY at the Wine path, matching
|
||
# acekorneya's own docker-compose example. Saves, configs, logs, and
|
||
# admin lists all live under this Saved/ dir.
|
||
browseable_root: "/usr/games/.wine/drive_c/POK/Steam/steamapps/common/ARK Survival Ascended Dedicated Server/ShooterGame/Saved"
|
||
volumes:
|
||
# Whole ASA install (game binaries + ShooterGame/Saved/{configs,
|
||
# world data, logs}). One volume keeps things simple and mirrors
|
||
# the upstream docker-compose pattern.
|
||
- { type: volume, name: "panel-$INSTANCE_ID-server",
|
||
container: "/usr/games/.wine/drive_c/POK/Steam/steamapps/common/ARK Survival Ascended Dedicated Server" }
|
||
# Cluster transfer dir — may be overridden at instance create time
|
||
# to point at a shared `panel-ark-cluster-<id>` volume for
|
||
# multi-server clustering (see the Cluster button in the ARK modal).
|
||
- { type: volume, name: "panel-$INSTANCE_ID-cluster",
|
||
container: "/usr/games/.wine/drive_c/POK/Steam/steamapps/common/ARK Survival Ascended Dedicated Server/ShooterGame/Saved/clusters" }
|
||
|
||
ports:
|
||
# `env:` tells the agent which container env var to set with the
|
||
# allocated host port. The acekorneya entrypoint reads ASA_PORT /
|
||
# QUERY_PORT / RCON_PORT and assembles ARK's launch args from them
|
||
# (?Port=N ?QueryPort=N ?RCONPort=N). Without env-mirror, the second
|
||
# ARK instance on the same agent gets distinct host bindings but
|
||
# ARK still advertises 7777/27015 to Steam, so the browser shows
|
||
# only one of them.
|
||
- { name: game, proto: udp, default: 7777, required: true, env: ASA_PORT }
|
||
- { name: raw, proto: udp, default: 7778, required: true }
|
||
# Steam server-browser query port. ASA registers itself with the Steam
|
||
# master at this port, and `?QueryPort=<n>` on the ASA launch command
|
||
# tells the engine which port to bind + advertise. Without this entry
|
||
# in the manifest, the allocator can't pick distinct host ports for
|
||
# multiple instances on the same agent — they all clobber 27015 and
|
||
# only one shows up in the Steam server list.
|
||
- { name: query, proto: udp, default: 27015, required: true, env: QUERY_PORT }
|
||
- { name: rcon, proto: tcp, default: 27020, internal: true, env: RCON_PORT }
|
||
|
||
# post_start: shell commands executed inside the container by the agent
|
||
# after a successful Start. Best-effort — failures log a warning but don't
|
||
# fail the Start.
|
||
#
|
||
# Why this hook exists: ASA writes RCONPort=<n> under [ServerSettings] of
|
||
# GameUserSettings.ini at first boot, which is enough for the port to bind
|
||
# but the server silently refuses to respond to Source-RCON AUTH packets.
|
||
# Adding a duplicate RCONPort line under [SessionSettings] fixes it — a
|
||
# real, reproducible ASA quirk (hit with raw Python + the image's bundled
|
||
# rcon-cli, not panel-specific). Verified working on princess 2026-04-22.
|
||
#
|
||
# The hook: (a) waits up to 3 min for the ini to exist (cold first-boot
|
||
# SteamCMD install can take 15-30 min but the ini is written well before
|
||
# ASA launches), (b) bails out if the [SessionSettings] RCONPort already
|
||
# exists (idempotent — subsequent starts are no-ops), (c) injects the
|
||
# line, (d) signals the monitor_ark_server.sh watchdog to relaunch ASA so
|
||
# the server re-reads the patched config on its next boot cycle. The
|
||
# relaunch is the clean way — pkill is safe because the image's
|
||
# monitor_ark_server.sh is designed to re-spawn ArkAscendedServer.exe
|
||
# whenever it dies.
|
||
lifecycle:
|
||
post_start:
|
||
- |
|
||
set -e
|
||
LAUNCH="/usr/games/scripts/launch_ASA.sh"
|
||
INI="/usr/games/.wine/drive_c/POK/Steam/steamapps/common/ARK Survival Ascended Dedicated Server/ShooterGame/Saved/Config/WindowsServer/GameUserSettings.ini"
|
||
WINEPREFIX_DIR="/usr/games/.wine"
|
||
CA_MARKER="$WINEPREFIX_DIR/.panel-amazon-roots-installed-v1"
|
||
changed=0
|
||
# ----- Fix 1: launch_ASA.sh URL is missing ?QueryPort= -----
|
||
# The acekorneya image hard-codes "?Port=${ASA_PORT}" in the launch
|
||
# URL but never appends "?QueryPort=", so every sibling ARK instance
|
||
# defaults to 27015 — only the first to bind wins, the rest have
|
||
# NO query port. Without a query port, EOS / Steam server browser
|
||
# can't advertise the server, so clients can't connect.
|
||
# Idempotent: a second run sees the marker comment and no-ops.
|
||
if [ -f "$LAUNCH" ] && ! grep -q 'QueryPort=\${QUERY_PORT' "$LAUNCH"; then
|
||
echo "[panel-hook] patching $LAUNCH to add ?QueryPort=\${QUERY_PORT}"
|
||
sed -i 's|?Port=${ASA_PORT}|?Port=${ASA_PORT}?QueryPort=${QUERY_PORT}|' "$LAUNCH"
|
||
changed=1
|
||
fi
|
||
# ----- Fix 2: GameUserSettings.ini RCONPort under [SessionSettings] -----
|
||
# ARK ASA's RCON server silently rejects AUTH unless RCONPort also
|
||
# appears under [SessionSettings] (verified on princess 2026-04-22).
|
||
# ASA accepts the launch-URL ?RCONPort=N for binding, but the
|
||
# adapter handshake also reads from this INI key.
|
||
for i in $(seq 1 180); do [ -f "$INI" ] && break; sleep 1; done
|
||
if [ -f "$INI" ] && ! awk '/^\[SessionSettings\]/{f=1; next} /^\[/{f=0} f && /^RCONPort=/{found=1} END{exit !found}' "$INI"; then
|
||
echo "[panel-hook] injecting RCONPort=${RCON_PORT:-27020} under [SessionSettings]"
|
||
awk -v port="${RCON_PORT:-27020}" '/^\[SessionSettings\]/{print; print "RCONPort=" port; next} {print}' "$INI" > "${INI}.new"
|
||
mv "${INI}.new" "$INI"
|
||
changed=1
|
||
fi
|
||
# ----- Fix 3: pin EOS-advertised IP via -PublicIPForEpic ----------------
|
||
# WHY: ASA's EOS session registration auto-detects the host's outbound
|
||
# IP each time it re-registers. Under Docker host-net with multiple
|
||
# interfaces (docker0, br-*, IPv6 link-local) EOS sometimes picks the
|
||
# wrong adapter on a re-register and advertises an unreachable IP to
|
||
# the in-game browser → "server invisible but RCON works" + "restart
|
||
# fixes it" pattern. ASA exposes -PublicIPForEpic=<ipv4> to override.
|
||
# Reference: Epic dev-forum confirmation (multihome → wrong-EOS-IP).
|
||
# Idempotent: marker on the wine binary line means we only inject once.
|
||
if [ -n "$PUBLIC_IP_FOR_EPIC" ] && [ -f "$LAUNCH" ] && ! grep -q -- '-PublicIPForEpic=' "$LAUNCH"; then
|
||
echo "[panel-hook] injecting -PublicIPForEpic=${PUBLIC_IP_FOR_EPIC} into $LAUNCH"
|
||
# Insert the flag right after the ArkAscendedServer.exe binary path.
|
||
# The launch line ends with a backslash continuation, so the next
|
||
# line (the URL + extra args) is unaffected.
|
||
sed -i 's|ArkAscendedServer.exe" \\$|ArkAscendedServer.exe" -PublicIPForEpic='"${PUBLIC_IP_FOR_EPIC}"' \\|' "$LAUNCH"
|
||
changed=1
|
||
fi
|
||
# ----- Fix 4: ensure system CA bundle is up-to-date (idempotent) -------
|
||
# WHY: EOS endpoints (*.epicgames.dev, *.on.epicgames.com) are on AWS
|
||
# CloudFront and chain to Amazon Root CA 1-4 + Starfield Services G2.
|
||
# Whether the EOS SDK under Wine uses Wine's HKLM root store or the
|
||
# statically-linked openssl/curl bundle inside the game, missing CAs
|
||
# in the system bundle have been correlated with hours-later EOS
|
||
# token-refresh failures (session ages out → server vanishes from
|
||
# in-game browser → container restart fixes by replaying cached auth).
|
||
# Reference: acekorneya/Ark-Survival-Ascended-Server#8 (open).
|
||
# The acekorneya image's Debian base lists the Amazon PEMs as enabled
|
||
# in /etc/ca-certificates.conf but may have a stale bundle if
|
||
# update-ca-certificates didn't run during image build. Marker file
|
||
# makes this once-per-prefix-lifetime — the run itself is ~1s if
|
||
# nothing needs rebuilding, so the marker is just for log noise.
|
||
if [ ! -f "$CA_MARKER" ]; then
|
||
echo "[panel-hook] running update-ca-certificates to refresh system CA bundle"
|
||
if update-ca-certificates >/dev/null 2>&1; then
|
||
echo "[panel-hook] system CA bundle refreshed"
|
||
touch "$CA_MARKER" 2>/dev/null || true
|
||
else
|
||
echo "[panel-hook] warn: update-ca-certificates failed (non-fatal)"
|
||
fi
|
||
fi
|
||
if [ "$changed" = "1" ]; then
|
||
# Relaunch ASA so it picks up the new launch script + INI. The
|
||
# image's monitor_ark_server.sh spots the missing PID within ~60s
|
||
# and respawns ArkAscendedServer.exe with the fixed config.
|
||
pkill -f ArkAscendedServer.exe 2>/dev/null || true
|
||
echo "[panel-hook] fixups applied; ASA will relaunch via image monitor"
|
||
else
|
||
echo "[panel-hook] all fixups already in place — no-op"
|
||
fi
|
||
|
||
resources:
|
||
min_ram_mb: 12288
|
||
recommended_ram_mb: 16384
|
||
|
||
# What gets archived when an operator clicks Back-up-now. Without this
|
||
# the agent tarballs the whole 18 GB ASA install (game binaries +
|
||
# Proton runtime + saves), which is wasteful — the only files that
|
||
# actually need preserving are under ShooterGame/Saved/. Cluster
|
||
# transfers live in a sibling clusters/ dir which IS bind-mounted to
|
||
# a shared volume on cluster setup, so we exclude that — restoring
|
||
# saves shouldn't clobber cluster data the other servers are using.
|
||
backup:
|
||
# Paths are RELATIVE to browseable_root (which is already
|
||
# ".../ShooterGame/Saved"), NOT relative to the container root. The
|
||
# earlier "ShooterGame/Saved/SavedArks" form pointed at a doubled path
|
||
# that doesn't exist, so every include filtered out and busybox tar
|
||
# got an empty stdin → exited non-zero → "sidecar exited 1".
|
||
include:
|
||
- "SavedArks" # world saves (the main thing)
|
||
- "Config" # GameUserSettings.ini, Game.ini
|
||
- "Logs" # tribe logs, server logs
|
||
exclude:
|
||
- "clusters" # shared volume; restoring it would clobber siblings
|
||
- "SavedArks/*/Mods" # mod cache; rebuilds on next start
|
||
- "Crashes" # crash dumps — never useful in a restore
|
||
notes: "Saves + configs + tribe logs (~250 MB typical). Game install + Proton runtime are preserved by SteamCMD on next start; no need to back them up."
|
||
|
||
rcon:
|
||
adapter: docker_exec_rcon
|
||
host_port: rcon
|
||
auth: password
|
||
# Source-of-truth precedence (highest first, per agent's resolveRCONAddress + tracker):
|
||
# 1. config_values["rcon_password"] — explicit override (rare)
|
||
# 2. config_values["SERVER_ADMIN_PASSWORD"] via password_secret —
|
||
# the env we set at create time. THIS is what actually works
|
||
# for the acekorneya image.
|
||
# 3. password_from_ini lazy-read at dial time — only effective
|
||
# AFTER the operator changes the password via the Config tab
|
||
# and the server has restarted to pick it up.
|
||
#
|
||
# Why both: the acekorneya image takes SERVER_ADMIN_PASSWORD and
|
||
# passes it on the launch command line as `?ServerAdminPassword=...`.
|
||
# ARK SA accepts the launch-arg version BUT does NOT write it back
|
||
# into GameUserSettings.ini's [ServerSettings]ServerAdminPassword
|
||
# — that key stays empty until the operator edits it. So the env
|
||
# value is the working credential. The INI fallback is for after
|
||
# the operator has changed the password via the Config tab (which
|
||
# writes to the INI but doesn't propagate back into the env).
|
||
password_secret: SERVER_ADMIN_PASSWORD
|
||
password_from_ini:
|
||
file: "/usr/games/.wine/drive_c/POK/Steam/steamapps/common/ARK Survival Ascended Dedicated Server/ShooterGame/Saved/Config/WindowsServer/GameUserSettings.ini"
|
||
section: ServerSettings
|
||
key: ServerAdminPassword
|
||
commands:
|
||
list_players: "ListPlayers"
|
||
say: "ServerChat {msg}"
|
||
broadcast: "Broadcast {msg}"
|
||
kick: "KickPlayer {player}"
|
||
ban: "BanPlayer {player}"
|
||
save: "SaveWorld"
|
||
shutdown: "DoExit"
|
||
give_item: "GiveItemToPlayer {player} {item} {qty} 1 0"
|
||
set_time: "SetTimeOfDay {time}"
|
||
|
||
state_sources:
|
||
- type: rcon
|
||
command: "ListPlayers"
|
||
every: 30s
|
||
parse:
|
||
kind: regex
|
||
pattern: '^(?P<index>\d+)\.\s+(?P<name>.+?),\s+(?P<steamid>\d+)'
|
||
fields:
|
||
players_online: index
|
||
- type: log_tail
|
||
files:
|
||
- "$DATA_PATH/logs/*.log"
|
||
- "/home/pok/arkserver/ShooterGame/Saved/Logs/ShooterGame.log"
|
||
|
||
events:
|
||
join:
|
||
pattern: "(?P<name>[^ ]+) joined"
|
||
kind: join
|
||
leave:
|
||
pattern: "(?P<name>[^ ]+) left"
|
||
kind: leave
|
||
chat:
|
||
pattern: "Chat: \\[(?P<name>[^\\]]+)\\] (?P<msg>.+)"
|
||
kind: chat
|
||
|
||
# The panel manifest validator requires at least one update_provider. For
|
||
# ARK: SA the *real* update path is the image's built-in SteamCMD loop
|
||
# (env UPDATE_SERVER=TRUE). The stub below exists so the manifest loads;
|
||
# don't rely on it — the install path won't match the image's expected
|
||
# layout so running the panel's SteamCMD sidecar against it is a no-op at
|
||
# best. Click "Restart" instead; that triggers the image's update cycle.
|
||
update_providers:
|
||
- id: builtin
|
||
kind: steamcmd
|
||
app_id: "2430930"
|
||
install_path: "/usr/games/.wine/drive_c/POK/Steam/steamapps/common/ARK Survival Ascended Dedicated Server"
|
||
|
||
# --- 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 has completed startup and is now advertising/i'
|
||
appearance:
|
||
emoji: "🦖"
|
||
grad: "linear-gradient(135deg,#3a2e1e,#d97706)"
|
||
art: "/game-art/ark-sa.jpg"
|
||
steam: "2430930"
|