# 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=` 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=` 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-` 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=` 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= 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= 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\d+)\.\s+(?P.+?),\s+(?P\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[^ ]+) joined" kind: join leave: pattern: "(?P[^ ]+) left" kind: leave chat: pattern: "Chat: \\[(?P[^\\]]+)\\] (?P.+)" 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"