Architecture · July 2026

From guild operator to edge: how the games estate fits together

Five views of the same system, wide to narrow. Two games — iwars and pontu — are built by an in-cluster CI, deployed by GitOps, spun up per-guild by a Kubernetes operator, fed art by a GPU/NPU asset factory on the workstation, and published to the internet through a hardened edge box with zero inbound connections to the home network.

edge / public kubernetes / cluster CI / GitOps AI / asset generation hardware / hosts identity / data

View 01 · wide shot

The estate at a glance

Everything public flows through napuka, a rented dedicated server that reverse-proxies into the homelab over WireGuard — the home network accepts no inbound connections at all. Inside, the xlab Kubernetes cluster runs the games and guild services, Gitea and Jenkins drive builds, and the zlab workstation is the AI asset factory whose output lands back in git like any other change.

Home lab — 192.168.0.0/24

napuka — public edge

Internet

xlab Kubernetes cluster

WebRTC relay

promoted assets
committed to git

Players and
guild members

Owner / dev

nginx front
zekup.com · iphone-wargames.com

coturn
TURN / TURNS :5349

WireGuard tunnel
zero inbound home

cilium-ingress 10.2.25.2
*.xlab.expansible.io

Game web + WS servers
iwars · pontu, per guild

Guild stack
Synapse · Element · Postgres

Jenkins · Argo CD
app-of-apps GitOps

Gitea on host lab
git + container registry

zlab workstation
Asset Manager · ComfyUI · Lemonade

Edge: napuka, Rocky Linux dedi — nginx + coturn + WireGuard egress Home ingress: cilium-ingress via MetalLB 10.2.25.2 Public games: board.pontu / board.iwars .zekup.com · *-test.iphone-wargames.com

View 02 · guild platform

zekup: one CR per guild, a full stack out

A guild is a single GuildSiteInstance custom resource. The owner registers and creates it through the portal; the Go API translates that into the CR; the operator reconciles it into a per-guild namespace holding a Matrix homeserver, a chat client, a database, both games' web and websocket servers, and a leaderboard. Identity is shared: OpenLDAP holds accounts and groups, Dex fronts them with OIDC for every client (portal, API, games) using cross-client audiences.

Identity

Per-guild namespace

shared TURN secret

Guild owner

zekup-portal
register · create guild · /admin
build-promotion selector

zekup-api — Go, gin + client-go
M2M API keys · assertOwner

GuildSiteInstance CR
model.zekup.com

zekup-operator
Helm chart baked into image

Synapse
Matrix homeserver

Element Web

PostgreSQL
Zalando operator

Game web sites
iwars · pontu

Authoritative WS servers
server-side dice · AI seats

Leaderboards — game.board
W/L/D, streaks, ≥2-human ranking

Dex OIDC
cross-client audience

OpenLDAP
zekup-registrar · zekup-admins

napuka coturn
TURN relay for WebRTC

Portal: zekup.xlab.expansible.io — registration 201 via LDAP registrar Promotion: portal PATCH re-pins a guild's game build tag, pods roll Chat: Synapse server_name is immutable — chosen once per guild web / ws: independently optional per guild in the CR spec

View 03 · build and deploy loop

CI & GitOps: push once, everything follows

A push to Gitea fires a webhook into in-cluster Jenkins, which schedules ephemeral pod agents on whichever architecture the job needs — including a Podman agent on the zlab workstation for heavy ROCm image builds. Images land in the Gitea registry; ArgoCD Image Updater notices new tags and writes the pin back into git, where the argo-apps app-of-apps syncs it onto the cluster. No one deploys by hand: the cluster is whatever k8s/live/ says it is.

git write-back
.argocd-source-*.yaml

kaniko build + push

Ephemeral build agents

amd64 pods
control-plane toleration

arm64 — armwork1
dedicated=arm-build

riscv64 — riscwork1
dedicated=riscv-build

zlab-rocm Podman agent
heavy ROCm image builds

Playwright E2E
ci-playwright-node image

git push

Gitea — git.home
webhooks to Jenkins

Jenkins, in-cluster
multibranch pipelines · JCasC

Gitea container registry
iwars 0.1.N · pontu 2.0.0-bN

ArgoCD Image Updater
newest-build strategy

expansible-xlab repo
k8s/live app-of-apps

Argo CD — argo-apps
auto-sync

Cluster workloads
iwars-beta · pontu-beta · zekup

site-sync CronJobs
hourly rsync of web builds

napuka vhosts
*-test.iphone-wargames.com

Secrets: SOPS + age, decrypted by Argo CD at sync time Registry creds: gitea-registry JCasC credential shared across 7 repos Version scheme: tag = build number; Image Updater ranks by push time, not semver Rule: no kubectl apply on managed workloads — changes go through git

View 04 · asset factory on zlab

Asset factory: prompt in, committed game art out

The Asset Manager (React SPA + FastAPI, a rootless Podman quadlet on zlab) is the front door for all generated art: 3D race figures, terrain maps, and each game's UI art. Its scheduler fills one GPU slot and two CPU slots, cheapest jobs first, and dispatches to a single ComfyUI instance — one container only, because two on the Radeon 890M silently degrade kernels mid-generation — or to the Lemonade stack for fast Flux text-to-image and NPU-hosted LLM work. Promotion is the key move: it applies each game's size and post-process SSOT, enforces the triangle budget, and drops finished assets straight into the game repos, where the CI loop from View 03 takes over.

ComfyUI :8188 — one container, ROCm gfx1100 on Radeon 890M

promote: SSOT sizes from UI_ART_PROMPTS.md
derive-button.py · 30k-tri game budget

git push, then View 03 takes over

Lemonade stack on zlab

Flux-2-Klein-4B GGUF
sd-cpp text-to-image

FLM on XDNA NPU :11435
gpt-oss:20b — digests, triage

Asset Manager — assets.home.expansible.io
React SPA + FastAPI · rootless quadlet
tabs: 3D figures · maps · UI Art hub · Jobs

Job scheduler, cheap-first
1 GPU slot + 2 CPU slots

ComfyUI-Terrain
MapGen4ToTerrain: heightmap to textured GLB

ComfyUI-IwarsFigures
race figure pipeline

Hunyuan3D texgen
custom_rasterizer built for gfx1100

TRELLIS.2 image-to-3D
150k faces + vertex weld

Qwen image gen
Qwen3-VL map description

LTX-2 video + native audio

mapgen4 exporter
maps.json · territories · POIs · fords

Generated assets
GLB figures · map albedos · UI art

Game repos
iwars-react · pontu-react · mapgen4

Beta sites + guild instances

Deploy: image-baked — Jenkins builds iwars-asset-manager:0.1.N, re-pin the quadlet Discipline: drain the generation queue before any restart — overnight qwen batches run long Provenance: gallery tracks origin; "IN GAME v{n}" badge follows the live promoted sample Models: checkpoints, controlnets, LoRAs, VAEs under comfyui/models/, bind-mounted

View 05 · iron and wires

Hosts & network: what it all runs on

The control plane is three KVM virtual machines on one Ryzen mini-PC hypervisor, joined over a WireGuard mesh by an ARM worker and a RISC-V worker, each tainted so only build jobs land there. zlab is both the owner's workstation and the GPU/NPU inference host; the Synology NAS is the lone Docker holdout in an otherwise Podman-everywhere estate. napuka reaches the cluster only through its own WireGuard tunnel.

xlab hypervisor — Minisforum EliteMini · Ryzen 9 8945HS · 61 GiB · Ubuntu 24.04 headless

dedicated WireGuard egress
napuka-wg-gw

LAN + wg0 client VPN

LAN · SNMP-scraped

xlab-node1 · 192.168.0.19
KVM VM · control-plane

xlab-node2 · 192.168.0.115
KVM VM · control-plane

xlab-node3 · 192.168.0.72
KVM VM · control-plane

armwork1 · 192.168.0.106
arm64 worker · taint arm-build

riscwork1 · 192.168.0.104
riscv64 worker · taint riscv-build

zlab workstation
Radeon 890M ROCm + XDNA NPU
rootless Podman quadlets

Synology freestation NAS
the one Docker exception

napuka — public dedi, Rocky Linux
nginx · coturn · WireGuard gateway

wg1 mesh 10.2.21.0/24 — kubeadm and cluster traffic ride this

CIDR / addressPurpose
192.168.0.0/24Physical LAN
10.2.20.0/24WireGuard wg0 — external client VPN
10.2.21.0/24WireGuard wg1 — peer mesh, carries kubeadm traffic
10.2.23.0/24Kubernetes service CIDR
10.2.24.0/24Pod CIDR, /26 per node
10.2.25.0/24MetalLB LoadBalancer pool (L2)
10.2.25.1argocd-server
10.2.25.2cilium-ingress — *.xlab.expansible.io
Provisioning: Ansible (site.yml) — VMs, WireGuard, kubeadm, Cilium, Argo CD Storage: Longhorn on the VM nodes, raw LVM on Kingston NVMe Observability: Prometheus + Grafana + Loki; systemd node_exporters on hosts