A single Docker image that runs a Tribes 2 (2002) dedicated server under Wine on Debian, applies the Tribes 2 community patch, and ships an integrated ASP.NET Core 10 control panel (React SPA frontend, ASP.NET Identity auth) with role-based access. The panel is PID 1 and owns the game lifecycle.
Engine note: Tribes 2 runs on the Dynamix "V12" engine — the precursor to the Torque Game Engine (TGE), which preceded Torque Game Engine Advanced (TGEA), which preceded Torque 3D.
Published as a site at https://geekofwires.github.io/tribes2-server/ (built from
docs/ by .github/workflows/pages.yml).
One-time setup to turn the site on: repo Settings → Pages → Build and deployment → Source = "GitHub Actions". The workflow then deploys on every push that touches
docs/.
In-depth guides:
- Architecture — how the panel, Wine, the game, and the database fit together.
- Creating a custom mod image — build your own server image with a custom mod + ruleset (worked examples).
- Rulesets & mods — how
SERVER_RULESET/-modworks; baking vs uploading. - Web panel & roles — every page, the role model, the Developer capability, the API.
- Configuration reference — every env var and build arg.
- Networking & client IPs — make real player IPs reach the container (so bans/admin work).
- Building & deploying · Internals: patch & headless · Database · TLS · Troubleshooting
FROM mcr.microsoft.com/dotnet/aspnet:10.0— the ASP.NET Core runtime owns the image; Wine + the game are layered on top. (.NET has no linux-x86 runtime, so the image is amd64 with i386 multiarch for 32-bit Wine — not a pure-i386 base.)- Installs WineHQ (
winehq-stable, pinned to Wine 10 viaWINE_VERSION— Wine 11 regresses the T2 mission-start path) with 32-bit components and initializes awin32Wine prefix headlessly (wineboot --init; no winetricks, no xvfb). - Provides the Windows runtimes the way the Tribes 2 Linux community does
(ChocoTaco1/docker-tribesnext-server):
- Old VC++6 runtime — the game's own bundled
MSVCRT.dll+Tribes2.exe.local(DLL redirection), already inGameData. - Newer 32-bit Windows APIs (what the QoL patch needs) — the real Microsoft VC++ 2022
DLLs (
vcrun22:msvcp140,vcruntime140,concrt140, …) dropped intosystem32, with native DLL-overrides. No Ruby — the 2025 QoL is native code insideIFC22.dll.
- Old VC++6 runtime — the game's own bundled
- Installs the base game from the public GSI installer (
GSI_URLbuild ARG). It's a Wise installer with no working headless mode, so a tiny static build of REWise extractsGameDatafrom it without running it (no Wine, no display) toC:\Dynamix\Tribes2\GameData. - Downloads the Tribes 2 QoL patch (
PATCH_URLbuild ARG — the "variable"; defaults toTribesNEXT_20250922_preview.exe). It's an NSIS installer, so7zextracts its payload deterministically (IFC22.dll, Miles sound libs, SDL3/OpenAL/ Discord/libcurl,base/t2csri.vl2, …) straight ontoGameData— no Wine-run required. - Runs the Python PE patcher (
content/tribes_dual_patcher.py) overTribes2.exe: flips the PE subsystem GUI→CUI so the dedicated server is a console app — its console output goes to stdout and its console input is read from stdin. The supervisor then runs it on a PTY (a real TTY, still headless) so the engine'sReadConsoleInputconsole works with no display. - Adds the framework-dependent ASP.NET Core 10 panel (with the built React SPA) and sets it as the entrypoint (PID 1).
Why the PTY: on a plain pipe,
ReadConsoleInputfails and the server crashes at "starting mission countdown" (root-caused to the per-tick console-input poller over-reading an uninitialized event count). A TTY fixes that and gives us a command channel — no xvfb, no telnet (the engine'stelnetSetParametersis fatal in this head-less build).
PID 1: ASP.NET Core panel (Kestrel + React SPA + ASP.NET Identity)
└─ GameSupervisor (hosted Worker Service)
└─ python PTY bridge ─ wine Tribes2.exe $LAUNCH_PARAMS
├─ game stdout (PTY) -> bridge strips ANSI -> ConsoleHub ring buffer + SSE
├─ game stdin (PTY) <- panel console commands + quit();
└─ lifecycle: restart / force-restart / stop / start + internal auto-restart
browser --HTTPS+cookie (Identity)--> panel API/SSE --> GameSupervisor --> game
- The panel owns the lifecycle. The game runs inside the panel process; crashes are
auto-restarted internally so the panel stays available.
restart: on-failureonly relaunches the container if the panel itself exits (e.g. root's force-shutdown). - Console feed = the game's console output (over the PTY), ANSI-stripped, streamed via SSE.
- Commands /
quit();= written to the game's console stdin over the PTY (no telnet). - Tech: ASP.NET Core 10 · React (Vite) SPA served by the host · ASP.NET Core Identity (cookie auth) · EF Core 10 on a local Turso-compatible SQLite file · LettuceEncrypt for ACME.
The panel DB is a single local file (PANEL_DB_PATH, default /data/panel.db) on a Docker
volume. EF Core 10 talks to it via the official SQLite provider, and the file is plain
SQLite-format (so it's also libSQL/Turso-compatible) — a self-contained file, no database
server. Inspect or manage it locally with whichever CLI you prefer:
sqlite3 /data/panel.db # or the libsql / turso shells(There is no EF Core 10 provider for the new Turso/Limbo engine or for libSQL today, so EF Core's SQLite provider on the compatible file is the robust choice.)
| Role | Capabilities |
|---|---|
| User | View live server console only |
| Admin | + Restart (graceful quit();, auto-relaunch) / Start |
| Super Admin | + Graceful stop + run console commands + Force-restart the game (emergency kill + relaunch) |
| root | + Force-shutdown the panel (stops the container; restart policy relaunches it) + user management + edit any file + a container terminal |
Developer is an additive capability, not a rank — root can grant it to any User/Admin/Super Admin. It unlocks the Files editor scoped to the GameData tree. root holds it implicitly and is unrestricted (any path in the container).
All privileged actions are recorded in an audit log (visible to Super Admin+).
Every unexpected/unhandled game exit (access violations) is recorded to a read-only
Crash Reports page (Admin+): server start + crash timestamps, exit code, the 0x fault
address, faulting instruction, module, launch params, and the console tail + CRASHLOG.TXT,
so hosts can report reproducible crashes for the image to patch.
The Files page is a Monaco editor (VS Code Dark+ theme, with TorqueScript highlighting
for the engine's .cs/.gui/.mis scripts plus shell/ini/yaml/json/etc.). Developers browse and
edit under GameData; root anywhere. Every change is written to a FileEdits table with the
pre-change snapshot, so root can revert any edit/create/delete from the File History page.
Path access is canonicalized and scope-checked server-side (a ../ escape out of GameData is denied).
The Files page also supports uploads (multipart): Developers may upload into the GameData tree (any depth), root anywhere. Each uploaded file is audited like an edit.
root also gets a Terminal page — an interactive bash session on a real PTY inside the container
(xterm.js over a WebSocket), so vim, htop, etc. work. Terminal sessions are audited.
On a fresh database the server is unconfigured and does not run. Log in as root
(ROOT_USERNAME/ROOT_PASSWORD, seeded on first boot) — the panel shows a first-time setup
screen. Completing it (choose launch params + whether to Auto-Start) marks the server
configured in SQLite and starts it. Afterwards root can toggle Auto-Start any time; the
flag is persisted, and on every panel startup the ASP.NET host launches the game automatically
only when Auto-Start is true. Non-root users see a "setup required" notice until then.
cp .env.example .env
# Set at least PANEL_ROOT_USERNAME + PANEL_ROOT_PASSWORD (root is seeded on first boot).
# Optionally enable TLS (SELF_SIGNED_CERT=1 or LETS_ENCRYPT_CERT=1) — see .env.example.
docker compose build t2-base
docker compose up -d t2-base
docker compose logs -f # watch the panel + game console
# Panel: http://localhost:8080 (log in as your root user)Three images are defined. The derived ones build FROM the base, so build the base first.
| Image | Mod | Builds from | SERVER_RULESET |
|---|---|---|---|
tribes2-server:base |
base | Dockerfile | (empty → no -mod) |
tribes2-server:classic |
Classic | mods/classic/Dockerfile | Classic |
tribes2-server:construction |
Construction | mods/construction/Dockerfile | Construction |
- Classic overlays
content/classic_v152.zip(a zip-in-a-zip; its lowercaseclassic/is merged into the case-sensitiveClassic/), then clones TacoServer and overlays itsClassic/tree, and finally clones TacoMaps and flattens its.vl2map packs intobase/(the engine only mounts.vl2files placed directly inbase/). - Construction extracts
content/Construction_v0.70a.exe(a RAR self-extractor, unpacked with7zz) soConstruction/lands inGameData.
docker compose build t2-base # build base FIRST
docker compose --profile classic build t2-classic # then the derived images
docker compose --profile construction build t2-construction
docker compose up -d t2-base # standard server (panel :8080)
docker compose --profile classic up -d t2-classic # Classic (panel :8081)
docker compose --profile construction up -d t2-construction # Construction (panel :8082)Rulesets / -mod. The derived Classic/Construction images still install their mod files
at build time (the whole point of a baked image); what changed is only how the -mod
parameter is selected. The SERVER_RULESET env picks the ruleset and the supervisor
inserts -mod <ruleset> between -online and -dedicated. Empty or base means no -mod.
The derived images set this env (Classic/Construction); the base image leaves it empty.
root can also set the ruleset in the panel — during first-time setup (defaulting to the
SERVER_RULESET value) and later from Controls (applied on the next restart). The panel
suggests the installed rulesets it discovers (top-level GameData folders containing a
scripts/ dir — base plus any baked or uploaded mod) and lets you type a newer ruleset:
upload its files via the Files page, then enter its name. So the baked images and ad-hoc
rulesets coexist.
LAUNCH_PARAMS ordering still matters for anything you put there: -online (or -nologin
to host outside WON/Tribes 2 auth) first, -dedicated last.
During first-time setup root also edits serverprefs.cs for the chosen ruleset
(GameData/<base|ruleset>/prefs/serverprefs.cs, created if missing) right in the Monaco
editor; the save goes through the audited file pipeline.
docker run --rm --entrypoint bash tribes2-server:base -c '
ls -l "$GAME_DIR/IFC22.dll"; # ~2 MB (Tribes 2)
python3 /opt/patcher/tribes_dual_patcher.py --exe "$GAME_DIR/Tribes2.exe" --dry-run'
# "Current subsystem: CUI" confirms the headless console patch is applied.No game archive lives in the repo or CI. The base game ships as the public GSI installer
(tribes2gsi.exe, a Wise installer), and the build pulls GameData out of it directly:
- The build downloads the GSI from
GSI_URL(defaulthttps://depot.tribes2.net/legacy/tribes2gsi.exe; override with theGSI_URLrepo variable, or pin its hash withGSI_SHA256). - A tiny static build of REWise — a tool that
extracts files from Wise installers without executing them — pulls just
MAINDIR/Tribes2/GameData/*out of the installer. No Wine, no display, no GUI: the installer is never run. - The 538 MB installer is downloaded and removed in the same build layer, so only the
extracted
GameDatapersists in the image.
This makes both local and CI builds self-sufficient from a public source — nothing to host, no secrets, no committed game files. (The installer's silent mode does not work headlessly, which is why we extract rather than run it.)
A workflow at .github/workflows/build.yml builds all three images and pushes them to the GitHub
Container Registry on push to main, on v* tags, or manually:
ghcr.io/<owner>/<repo>/base, /classic, /construction (tagged with the commit SHA and
latest). The base job runs first; a mods matrix job then builds Classic and Construction
FROM the freshly pushed base. The base build pulls game data from the public GSI as described
above (nothing to provision); the Tribes 2 patch is fetched from PATCH_URL (overridable via the
PATCH_URL repo variable); the Classic mod zip and Construction installer are committed under
content/.
Feasibility notes: the runner needs free disk for a multi-GB Wine image (the workflow frees
space first) and the build installs Wine + downloads the VC++ runtime; expect a long first build.
GHCR auth uses the built-in GITHUB_TOKEN (no extra secret).
The panel listens on HTTP (HTTP_PORT, default 8080) and, when enabled, HTTPS (HTTPS_PORT,
default 8443). Pick at most one cert source via env (see .env.example):
SELF_SIGNED_CERT=1— generate (and persist to/data) a self-signed cert fromSELF_SIGNED_SUBJECT/SELF_SIGNED_CN,SELF_SIGNED_DNS,SELF_SIGNED_IP,SELF_SIGNED_DAYS.LETS_ENCRYPT_CERT=1— provision/renew via ACME (LettuceEncrypt) fromLETS_ENCRYPT_EMAILandLETS_ENCRYPT_DOMAINS(DNS names; IP certs depend on ACME-server support). Needs the HTTP port reachable from the internet for the HTTP-01 challenge.LETS_ENCRYPT_STAGING=1while testing.- Neither — plain HTTP; terminate TLS at an external reverse proxy.
PATCH_URL: defaults to the 2025-09-22 preview installer; pinPATCH_SHA256for reproducibility.- Root seeding:
ROOT_PASSWORD(plaintext) is used once on first boot to create the root user (hashed by Identity). Change it in the panel afterward. - Game UDP ports for your mod (default exposes 28000/udp).
- VC++ runtime: the modern VC++ 2022 DLLs are fetched from Microsoft's official
redistributable (
VCREDIST_URL, defaultaka.ms/vs/17/release/vc_redist.x86.exe) at build time and unpacked withcabextract— nothing is vendored in the repo. See docs: licensing. - CPU pinning: set
GAME_CPU_AFFINITY(e.g.0) totasksetthe single-threaded server onto one core. - Auth/master:
-onlineregisters with the Tribes 2 master; use-nologinto host standalone. - Transitive advisory: EF Core 10 pins
SQLitePCLRaw … e_sqlite3 2.1.11(GHSA-2m69-gcr7-jv3q). Low real-world risk for a local admin-only DB; clears when EF Core bumps the dependency.
Dockerfile base (standard) image (panel build stage + wine runtime)
mods/
classic/Dockerfile Classic image (classic_v152 + TacoServer) FROM base
construction/Dockerfile Construction image (Construction_v0.70a) FROM base
docker-compose.yml base + classic + construction services (profiles)
content/ (game data is fetched from the GSI at build time; not committed)
classic_v152.zip Classic v1.52 mod (committed)
Construction_v0.70a.exe Construction mod RAR self-extractor (committed)
tribes_dual_patcher.py PE patcher (subsystem GUI->CUI for headless console I/O)
panel/ ASP.NET Core 10 control panel (PID 1)
Program.cs host wiring: TLS, EF/Identity, RBAC, supervisor, endpoints
Bootstrap.cs EnsureCreated + seed roles/root
Endpoints.cs minimal-API: account, console SSE, lifecycle, users, audit
Auth/ ApplicationUser/Role + rank-based Roles/policies
Data/ AppDbContext (IdentityDbContext) + AuditEntry
Services/ GameSupervisor (worker; PTY bridge + stdin commands), ConsoleHub
Tls/TlsConfigurator.cs self-signed / Let's Encrypt / plain HTTP from env
ClientApp/ React + Vite + TS SPA (built into wwwroot)
.github/workflows/build.yml CI: build + push base/classic/construction to GHCR
This project's own code, Dockerfiles, scripts, and documentation are released under the MIT License — © 2026 GeekOfWires.
It does not relicense third-party components, which keep their own terms:
- Tribes 2 (the game) — proprietary; not redistributed here.
GameDatais extracted at build time from the publicly hosted GSI installer (see Game data). - The community QoL patch — fetched at build time from its own host (
PATCH_URL). - Bundled community mods —
content/classic_v152.zip,content/Construction_v0.70a.exe, redistributed under their authors' terms. - Fonts — Electrolize and Share Tech Mono, under the SIL Open Font License (see the
OFL-*.txtfiles alongside the bundled.woff2). - Microsoft VC++ runtime — fetched from Microsoft at build time under its own redistribution terms.
- REWise (build-time GSI extraction) — under its own license.