Skip to content

Commit 54775b3

Browse files
hyperpolymathclaude
andcommitted
feat: CryoFall server deployment + schema-driven provisioner
- container/cryofall/: Full CryoFall dedicated server container - Containerfile: multi-stage debian build (SteamCMD 32-bit glibc requirement documented as justified exception to Chainguard policy) - entrypoint.sh: first-run SteamCMD install, config init, graceful shutdown - Settings.xml: default config (PvE, private, 2× rates, Joshua + Jonathan ops) - gsa-cryofall.container: Podman Quadlet for Verpex VPS (1.5 GB RAM, UDP 6000/6001) - scripts/backup.sh: GSA `backup` action target, 10-archive retention - manifest.toml: Cerro Torre bundle manifest - scripts/provision-server.sh: schema-driven server minter 9-step pipeline: preflight → build → volumes → firewall → quadlet → start → health verify → VeriSimDB register → report Profile registry maps A2ML @port/@container/@volumes to provisioning params. Supports dry-run (PROVISION_DRY_RUN=1), skip-build, skip-firewall. - Justfile: game-deploy, cryofall-deploy, verpex-deploy, cryofall-{build, start,stop,restart,logs,status} recipes - container/compose.toml: cryofall service + volumes + Podman socket mount for GSA to manage game containers via server_actions.zig - profiles/cryofall.a2ml: add Operators field (Jonathan + Joshua admin access) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 1713417 commit 54775b3

10 files changed

Lines changed: 1089 additions & 1 deletion

File tree

Justfile

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,84 @@ verisimdb-deploy:
413413
systemctl --user status gsa-verisimdb --no-pager 2>/dev/null | head -5 || true
414414
systemctl --user status gsa-verisimdb-backup --no-pager 2>/dev/null | head -5 || true
415415
416+
# ─── Game server provisioner ──────────────────────────────────────────────────
417+
#
418+
# Schema-driven game server minter. Reads profiles/<game>.a2ml and
419+
# container/<game>/ to provision a complete running server:
420+
# build → volumes → firewall → quadlet → start → verify → register in VeriSimDB
421+
#
422+
# Usage:
423+
# just game-deploy GAME=cryofall # any supported profile
424+
# just cryofall-deploy # CryoFall shorthand
425+
# just game-deploy GAME=cryofall DRY=1 # preview without executing
426+
# just verpex-deploy GAME=cryofall # SSH deploy to Verpex VPS
427+
428+
# Generic game server deploy — GAME must be a profile ID (profiles/<GAME>.a2ml)
429+
game-deploy GAME="" DRY="0":
430+
#!/usr/bin/env bash
431+
set -euo pipefail
432+
GAME="{{ GAME }}"
433+
if [ -z "${GAME}" ]; then
434+
echo "Usage: just game-deploy GAME=<profile-id>"
435+
echo "Profiles: $(ls profiles/*.a2ml | sed 's|profiles/||; s|\.a2ml||' | tr '\n' ' ')"
436+
exit 1
437+
fi
438+
PROVISION_DRY_RUN="{{ DRY }}" bash scripts/provision-server.sh "${GAME}"
439+
440+
# CryoFall shorthand — equivalent to: just game-deploy GAME=cryofall
441+
cryofall-deploy:
442+
just game-deploy GAME=cryofall
443+
444+
# Build the CryoFall container image only (no full provision)
445+
cryofall-build:
446+
#!/usr/bin/env bash
447+
set -euo pipefail
448+
echo "Building CryoFall server image..."
449+
podman build \
450+
-t localhost/cryofall-server:latest \
451+
-f container/cryofall/Containerfile \
452+
.
453+
echo "Built localhost/cryofall-server:latest"
454+
455+
# CryoFall service management (via systemd Quadlet)
456+
cryofall-start:
457+
systemctl --user start gsa-cryofall
458+
459+
cryofall-stop:
460+
systemctl --user stop gsa-cryofall
461+
462+
cryofall-restart:
463+
systemctl --user restart gsa-cryofall
464+
465+
cryofall-logs:
466+
podman logs --tail 100 -f cryofall
467+
468+
cryofall-status:
469+
#!/usr/bin/env bash
470+
echo "CryoFall container:"
471+
podman inspect --format " Status: {{.State.Status}} Health: {{.State.Health.Status}}" cryofall 2>/dev/null || echo " Not running"
472+
echo ""
473+
echo "Systemd service:"
474+
systemctl --user status gsa-cryofall --no-pager 2>/dev/null | head -8 || echo " Not installed"
475+
476+
# Deploy to Verpex VPS via SSH (run this from local machine)
477+
# Requires SSH key access: root@209.42.26.106
478+
verpex-deploy GAME="cryofall":
479+
#!/usr/bin/env bash
480+
set -euo pipefail
481+
GAME="{{ GAME }}"
482+
VERPEX_HOST="209.42.26.106"
483+
VERPEX_USER="root"
484+
echo "Deploying ${GAME} server to Verpex VPS (${VERPEX_HOST})..."
485+
# Copy provisioner + profiles + container definition
486+
ssh "${VERPEX_USER}@${VERPEX_HOST}" "mkdir -p ~/gsa-provision/${GAME}"
487+
rsync -az --progress \
488+
profiles/ scripts/ container/"${GAME}"/ \
489+
"${VERPEX_USER}@${VERPEX_HOST}:~/gsa-provision/${GAME}/"
490+
# Run provisioner on the VPS
491+
ssh "${VERPEX_USER}@${VERPEX_HOST}" "cd ~/gsa-provision/${GAME} && bash provision-server.sh ${GAME}"
492+
echo "Remote provisioning complete."
493+
416494
# Check VeriSimDB systemd service status
417495
verisimdb-status:
418496
#!/usr/bin/env bash

container/compose.toml

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,52 @@ environment = {
3131
APP_PORT = "8090",
3232
APP_LOG_FORMAT = "json",
3333
APP_DATA_DIR = "/data",
34+
# GSA uses the Podman socket to execute lifecycle actions against
35+
# managed game server containers (start/stop/restart/update/backup).
36+
# The socket path follows XDG_RUNTIME_DIR for rootless Podman on AlmaLinux 9.7.
37+
PODMAN_SOCKET = "/run/podman/podman.sock",
38+
GSA_GAME_PROFILES_DIR = "/app/profiles",
3439
}
35-
volumes = ["gsa-data:/data"]
40+
volumes = [
41+
"gsa-data:/data",
42+
# Mount the host Podman socket read-write so GSA can manage the cryofall
43+
# container via `podman start/stop/exec cryofall`.
44+
# On Verpex (AlmaLinux 9.7, rootless): ${XDG_RUNTIME_DIR}/podman/podman.sock
45+
# Set PODMAN_SOCKET_HOST in the environment before running selur-compose.
46+
"${PODMAN_SOCKET_HOST:-/run/user/1000/podman/podman.sock}:/run/podman/podman.sock",
47+
]
3648
restart = "always"
49+
depends_on = ["gsa-verisimdb"]
3750
healthcheck = { test = "curl -sf http://localhost:8090/health", interval = "30s", timeout = "5s", retries = 3 }
3851

52+
# CryoFall dedicated server
53+
# Managed by GSA via the Podman socket — do not start/stop this directly.
54+
# Use: GSA Gossamer GUI → CryoFall → Actions
55+
# Or: gsa probe 209.42.26.106 6000 to fingerprint the live server
56+
#
57+
# Ports are bound to 0.0.0.0 (all interfaces) because Cloudflare cannot
58+
# proxy UDP. DNS record cryofall.jewell.nexus must be grey-cloud (DNS-only).
59+
[services.cryofall]
60+
image = "localhost/cryofall-server:latest"
61+
ports = [
62+
"0.0.0.0:6000:6000/udp",
63+
"0.0.0.0:6001:6001/udp",
64+
]
65+
environment = {
66+
CRYOFALL_INSTALL_DIR = "/opt/cryofall",
67+
CRYOFALL_DATA_DIR = "/data/cryofall",
68+
CRYOFALL_BACKUP_DIR = "/data/backups",
69+
CRYOFALL_APPID = "1200170",
70+
}
71+
volumes = [
72+
"cryofall-game-data:/opt/cryofall",
73+
"cryofall-world-data:/data/cryofall",
74+
"cryofall-backups:/data/backups",
75+
]
76+
restart = "on-failure"
77+
# No healthcheck via HTTP — CryoFall is UDP-only.
78+
# The Quadlet unit (gsa-cryofall.container) uses `ss` to verify port 6000 is bound.
79+
3980
# Svalinn edge gateway: validates requests, enforces policies, TLS termination
4081
[services.svalinn]
4182
image = "ghcr.io/hyperpolymath/svalinn:latest.ctp"
@@ -60,6 +101,19 @@ driver = "local"
60101
[volumes.svalinn-config]
61102
driver = "local"
62103

104+
# CryoFall server volumes
105+
# cryofall-game-data ~3 GB SteamCMD game installation (persists across updates)
106+
# cryofall-world-data Settings.xml + world saves (GSA config editor owns this)
107+
# cryofall-backups Timestamped world archives (GSA `backup` action)
108+
[volumes.cryofall-game-data]
109+
driver = "local"
110+
111+
[volumes.cryofall-world-data]
112+
driver = "local"
113+
114+
[volumes.cryofall-backups]
115+
driver = "local"
116+
63117
# ============================================================================
64118
# Networks
65119
# ============================================================================

container/cryofall/Containerfile

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
# SPDX-License-Identifier: AGPL-3.0-or-later
2+
# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3+
#
4+
# CryoFall Dedicated Server container — managed by Game Server Admin (GSA)
5+
#
6+
# ── Runtime base note ──────────────────────────────────────────────────────────
7+
# Standard practice is cgr.dev/chainguard/wolfi-base. This image uses
8+
# debian:12-slim as a justified technical exception: SteamCMD is a 32-bit
9+
# glibc binary (lib32gcc-s1) with no musl-compatible or Wolfi alternative.
10+
# The GSA `update` action (profiles/cryofall.a2ml) re-invokes SteamCMD inside
11+
# the running container to update game files without rebuilding the image, so
12+
# SteamCMD must remain present in the runtime layer.
13+
# ──────────────────────────────────────────────────────────────────────────────
14+
#
15+
# Build:
16+
# podman build -t localhost/cryofall-server:latest -f container/cryofall/Containerfile .
17+
#
18+
# The image contains SteamCMD but NOT the game itself. Game files are
19+
# downloaded to the /opt/cryofall volume on first start via entrypoint.sh.
20+
# This means image updates never require re-downloading ~3 GB of game data.
21+
#
22+
# Managed via GSA:
23+
# GSA admin panel → CryoFall → Actions → Start / Stop / Update / Backup
24+
# gsa probe 209.42.26.106 6000 ← fingerprints the live server
25+
# gsa run config cryofall ← extracts Settings.xml into A2ML
26+
27+
# ============================================================================
28+
# Stage 1: SteamCMD installer
29+
# ============================================================================
30+
#
31+
# Downloads and bootstraps SteamCMD from Valve's CDN. Only the SteamCMD
32+
# files are copied forward; the full Debian toolchain is discarded.
33+
34+
FROM debian:12-slim AS steamcmd-installer
35+
36+
# Enable 32-bit architecture needed by SteamCMD
37+
RUN dpkg --add-architecture i386 && \
38+
apt-get update && \
39+
apt-get install -y --no-install-recommends \
40+
lib32gcc-s1 \
41+
ca-certificates \
42+
curl && \
43+
rm -rf /var/lib/apt/lists/*
44+
45+
# Install SteamCMD to /opt/steamcmd
46+
RUN mkdir -p /opt/steamcmd && \
47+
curl -sqL "https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz" | \
48+
tar -xzC /opt/steamcmd
49+
50+
# Run once to extract the bootstrapper and download the SteamCMD binary
51+
RUN /opt/steamcmd/steamcmd.sh +quit || true
52+
53+
# ============================================================================
54+
# Stage 2: Runtime image
55+
# ============================================================================
56+
57+
FROM debian:12-slim
58+
59+
# Labels — OCI standard + Stapeln/cerro-torre integration
60+
LABEL org.opencontainers.image.title="cryofall-server" \
61+
org.opencontainers.image.description="CryoFall dedicated server — managed by Game Server Admin" \
62+
org.opencontainers.image.url="https://github.com/hyperpolymath/game-server-admin" \
63+
org.opencontainers.image.source="https://github.com/hyperpolymath/game-server-admin" \
64+
org.opencontainers.image.vendor="hyperpolymath" \
65+
org.opencontainers.image.licenses="AGPL-3.0-or-later" \
66+
org.opencontainers.image.authors="Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>" \
67+
dev.stapeln.app="game-server-admin" \
68+
dev.stapeln.role="cryofall-server" \
69+
dev.stapeln.clade="gm" \
70+
dev.cerrotorre.manifest="container/cryofall/manifest.toml"
71+
72+
# ── Runtime dependencies ───────────────────────────────────────────────────
73+
# lib32gcc-s1 SteamCMD bootstrap binary (32-bit)
74+
# libstdc++6:i386 SteamCMD runtime (32-bit)
75+
# libgcc-s1:i386 Shared support for 32-bit code
76+
# libstdc++6 CryoFall_Server.x86_64 (64-bit .NET/Mono runtime)
77+
# ca-certificates HTTPS for SteamCMD CDN and GSA health endpoint
78+
# ss (iproute2) Health check — verify UDP port 6000 is listening
79+
80+
RUN dpkg --add-architecture i386 && \
81+
apt-get update && \
82+
apt-get install -y --no-install-recommends \
83+
lib32gcc-s1 \
84+
libstdc++6:i386 \
85+
libgcc-s1:i386 \
86+
libstdc++6 \
87+
ca-certificates \
88+
curl \
89+
iproute2 && \
90+
rm -rf /var/lib/apt/lists/*
91+
92+
# ── Non-root user ─────────────────────────────────────────────────────────
93+
# The CryoFall server runs as 'cryofall' (UID 1500). SteamCMD updates and
94+
# backup scripts run under the same UID so all volume-mounted files share a
95+
# consistent owner.
96+
97+
RUN groupadd -r -g 1500 cryofall && \
98+
useradd -r -u 1500 -g cryofall -m -d /home/cryofall -s /bin/sh cryofall
99+
100+
# ── SteamCMD ──────────────────────────────────────────────────────────────
101+
# Copied from the installer stage — includes the bootstrapped binary and
102+
# the linux32/ runtime directory it downloads on first run.
103+
104+
COPY --from=steamcmd-installer /opt/steamcmd /opt/steamcmd
105+
RUN chown -R cryofall:cryofall /opt/steamcmd && \
106+
ln -sf /opt/steamcmd/steamcmd.sh /usr/local/bin/steamcmd
107+
108+
# ── Application files ─────────────────────────────────────────────────────
109+
WORKDIR /app
110+
111+
# Default Settings.xml — entrypoint copies this to /data/cryofall on first run
112+
# if the operator has not already placed a custom config there.
113+
COPY container/cryofall/Settings.xml /app/defaults/Settings.xml
114+
115+
# Backup script — invoked by GSA `backup` action:
116+
# podman exec cryofall /scripts/backup.sh
117+
COPY container/cryofall/scripts/backup.sh /scripts/backup.sh
118+
RUN chmod +x /scripts/backup.sh
119+
120+
# Startup entrypoint
121+
COPY container/cryofall/entrypoint.sh /app/entrypoint.sh
122+
RUN chmod +x /app/entrypoint.sh
123+
124+
# ── Persistent volumes ────────────────────────────────────────────────────
125+
# /opt/cryofall Game installation (SteamCMD writes here — ~3 GB)
126+
# /data/cryofall Server config (Settings.xml) and world saves
127+
# /data/backups Timestamped world save archives
128+
129+
RUN mkdir -p /opt/cryofall /data/cryofall/Saves /data/backups && \
130+
chown -R cryofall:cryofall /opt/cryofall /data
131+
132+
VOLUME ["/opt/cryofall", "/data/cryofall", "/data/backups"]
133+
134+
# ── Ports ─────────────────────────────────────────────────────────────────
135+
# UDP 6000 Primary game traffic (all client connections)
136+
# UDP 6001 Server discovery and status (in-game server browser)
137+
# Both must be DNS grey-cloud (Cloudflare cannot proxy UDP).
138+
139+
EXPOSE 6000/udp 6001/udp
140+
141+
# ── Health check ──────────────────────────────────────────────────────────
142+
# The CryoFall server listens on UDP — no HTTP endpoint available.
143+
# We verify the port is bound; start-period allows 3 min for SteamCMD
144+
# to complete the first-run game installation (~3 GB download).
145+
146+
HEALTHCHECK --interval=30s --timeout=5s --start-period=180s --retries=3 \
147+
CMD ss -ulnp | grep -q ':6000 ' || exit 1
148+
149+
# ── Runtime ───────────────────────────────────────────────────────────────
150+
USER cryofall
151+
ENTRYPOINT ["/app/entrypoint.sh"]

0 commit comments

Comments
 (0)