ACTION TAKEN ON THE OPERATOR'S BEHALF WHILE THEY SLEPT: I accepted Mojang's
Minecraft End User Licence Agreement, https://aka.ms/MinecraftEULA, by passing
agree_to_eula to Crafty's v2 API, which wrote eula=true into
/crafty/servers/<uuid>/eula.txt. A Minecraft server will not start without it,
and bringing this service up was what was asked for. Please review. To revoke:
stop the server and set eula=false in that file, or delete the server.
Crafty was healthy but had never had a server created, so 192.168.2.22:25565
refused connections. Created "homelab" via the API (not UI automation):
Paper 26.2, TCP 25565, heap -Xms2000M -Xmx2000M, autostart on
uuid 13c7b85d-00bb-4a58-8c86-2433ca47891c
Paper over Vanilla for tick performance and plugins, with no cost to players
(vanilla clients connect unchanged). 26.2 is Mojang's latest.release, out
2026-06-16; the 26.3-rc-2 snapshot and 26.2-rc-2 were deliberately avoided.
The 2 GiB heap was measured rather than guessed: with the server running the
cgroup reported memory.current 2.86Gi against the 4Gi limit, so a 3 GiB heap
would have risked an OOMKill.
Proven playable, all real output: Crafty reports running with 'Done (12.737s)!';
Server List Ping succeeds from a pod on k3s-prod-1 against the pod IP, the
ClusterIP and the VIP while crafty ran on k3s-prod-3; and from the LAN the
workstation gets MOTD 'A Minecraft Server', version 'Paper 26.2', protocol 776,
players 0/20 over a full handshake plus ping/pong. Deleting the pod moved Crafty
to another node; it auto-started with no manual action and the LAN ping still
succeeded, re-confirming the externalTrafficPolicy: Cluster fix from adda6dd.
Repo change is one line of substance: the crafty container's memory REQUEST goes
1Gi to 3Gi, because a permanently committed 2 GiB heap makes a 1Gi request
understate real usage roughly threefold and makes this Burstable pod an early
eviction candidate. Limit stays 4Gi. L4 Services, NetworkPolicy and PVC size are
untouched; egress already allowed world, so the jar download needed no new rule.
Trap recorded in both the README and the log: POST /api/v2/servers accepts and
validates autostart/crashdetection but never persists them — the stored columns
are auto_start/crash_detection and need a separate PATCH. Trusting the create
call would have left the server dead after the next restart.
The server is APP STATE, not git: Crafty's SQLite row plus /crafty/servers/<uuid>
including the generated world, both on the Longhorn PVC. A rebuild from git alone
gives an empty Crafty and a refused 25565. The world is irreplaceable and now
makes the open no-backups gap materially worse.
Co-Authored-By: Claude Opus 5 <[email protected]>
201 lines
11 KiB
YAML
201 lines
11 KiB
YAML
---
|
|
# Crafty Controller pod = TWO containers:
|
|
# 1. crafty — the panel + every Minecraft server it creates (Java + Bedrock) run as
|
|
# SUBPROCESSES inside THIS container. Serves its web UI over HTTPS (self-signed)
|
|
# on :8443. Never exposed directly — only the sidecar and the game-port LB reach it.
|
|
# 2. oauth2-proxy — a per-pod REVERSE-PROXY (Option B, platform/oauth2-proxy/README.md). The shared
|
|
# Gateway routes crafty.home.keithjackson.dev -> this sidecar (:4180); the sidecar
|
|
# authenticates the browser against Keycloak (homelab realm, group platform-admins)
|
|
# and only THEN proxies to the co-located Crafty UI at https://127.0.0.1:8443. This
|
|
# is how Crafty's UI (which has NO native OIDC) is Keycloak-gated without ext_authz
|
|
# on the shared Gateway (impossible on Cilium 1.16.5 — see the oauth2-proxy README).
|
|
#
|
|
# Rendered by its OWN non-gating post-platform Flux Kustomization (clusters/base/crafty.yaml,
|
|
# dependsOn platform-config, wait:false) — a leaf UI must NEVER sit in the gating `platform` tier
|
|
# (DECISIONS-LOG: "Headlamp gating stall").
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: crafty
|
|
namespace: crafty
|
|
labels:
|
|
app.kubernetes.io/name: crafty
|
|
spec:
|
|
replicas: 1
|
|
# Crafty owns a single RWO Longhorn volume with live SQLite + running server subprocesses; a second
|
|
# replica would double-mount/corrupt it. Recreate (not RollingUpdate) so the old pod releases the RWO
|
|
# PVC before the new one attaches.
|
|
strategy:
|
|
type: Recreate
|
|
revisionHistoryLimit: 3
|
|
selector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: crafty
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app.kubernetes.io/name: crafty
|
|
spec:
|
|
automountServiceAccountToken: false
|
|
# ── IDENTITY / STORAGE PERMISSIONS (the seerr fsGroup lesson, applied) ───────────────────────
|
|
# The Crafty image runs as the non-root user `crafty` (UID 1000) whose PRIMARY GROUP is root
|
|
# (GID 0): its Dockerfile does `useradd -g root -M crafty` + `chown -R crafty:root /crafty` and
|
|
# makes /crafty group-writable — the OpenShift/K8s "arbitrary-UID, root-group-perms" pattern the
|
|
# Crafty docs call out. A fresh Longhorn PVC is root:root and NOT group-writable, so the crafty
|
|
# process (GID 0) could not create /crafty/servers/* etc. -> CrashLoop (same failure seerr hit as
|
|
# UID 1000 on a root:root PVC). fsGroup=0 makes the kubelet group-own the mounted volume to GID 0,
|
|
# mark it group-writable + SGID, and add GID 0 as a supplemental group, so the crafty user can
|
|
# write — Longhorn's CSIDriver fsGroupPolicy: ReadWriteOnceWithFSType applies fsGroup to this RWO
|
|
# filesystem volume. runAsUser/runAsGroup pin the image's own crafty:root identity; runAsNonRoot
|
|
# asserts it. (HUMAN runtime check at bring-up: confirm the pod writes the DB and starts a server;
|
|
# if the image's crafty UID is not 1000, adjust runAsUser — see README.)
|
|
securityContext:
|
|
runAsNonRoot: true
|
|
runAsUser: 1000
|
|
runAsGroup: 0
|
|
fsGroup: 0
|
|
seccompProfile:
|
|
type: RuntimeDefault
|
|
containers:
|
|
# ─────────────────────────────── Crafty panel + MC servers ──────────────────────────────────
|
|
- name: crafty
|
|
# registry.gitlab.com/crafty-controller/crafty-4 — official image. Pinned to the current
|
|
# stable release (4.10.8, 2026-07-29; tags are bare semver, NO `v` prefix). Renovate maintains.
|
|
image: registry.gitlab.com/crafty-controller/crafty-4:4.10.8
|
|
imagePullPolicy: IfNotPresent
|
|
env:
|
|
- name: TZ
|
|
value: "America/Chicago"
|
|
ports:
|
|
- { name: https-ui, containerPort: 8443, protocol: TCP }
|
|
# Starter game ports the operator assigns to servers created in the panel. Declared here for
|
|
# documentation + to back the game-port LoadBalancer (service-l4.yaml). Add more as needed.
|
|
- { name: mc-java-0, containerPort: 25565, protocol: TCP }
|
|
- { name: mc-java-1, containerPort: 25566, protocol: TCP }
|
|
- { name: mc-java-2, containerPort: 25567, protocol: TCP }
|
|
- { name: mc-bedr-0, containerPort: 19132, protocol: UDP }
|
|
- { name: mc-bedr-1, containerPort: 19133, protocol: UDP }
|
|
- { name: mc-bedr-2, containerPort: 19134, protocol: UDP }
|
|
resources:
|
|
requests:
|
|
cpu: 250m
|
|
# Raised 1Gi -> 3Gi when the first Minecraft server ("homelab", Paper 26.2) was created:
|
|
# that JVM runs with -Xms2000M -Xmx2000M, so the container's steady-state floor is now the
|
|
# panel (~100Mi) PLUS a permanently-committed 2 GiB heap plus JVM overhead. Measured
|
|
# cgroup memory.current with the server idle-but-running: 2.86Gi. A 1Gi request understated
|
|
# that by ~3x, which both misleads the scheduler and makes this Burstable pod an early
|
|
# eviction candidate under node memory pressure (kubelet ranks by usage-over-request).
|
|
memory: 3Gi
|
|
# Minecraft JVMs are memory-hungry; cap generously for a starter set of small servers.
|
|
# Headroom check for the 4Gi cap: 2 GiB heap + ~0.5 GiB JVM non-heap (metaspace, code cache,
|
|
# GC structures, thread stacks, mmap'd region files) + ~0.1 GiB panel = ~2.9Gi observed,
|
|
# leaving ~1.2Gi. A 3 GiB heap would have landed near 4Gi and risked an OOMKill, which is
|
|
# why the server was sized at a 2 GiB heap rather than larger. Raise BOTH this limit and the
|
|
# server's own memory in Crafty together if a bigger heap is ever wanted.
|
|
limits:
|
|
memory: 4Gi
|
|
securityContext:
|
|
allowPrivilegeEscalation: false
|
|
capabilities:
|
|
drop:
|
|
- ALL
|
|
# readOnlyRootFilesystem intentionally UNSET: Crafty's launcher writes runtime state under
|
|
# /crafty (app cache, per-server working dirs) beyond the mounted subPaths; hardening it
|
|
# needs writable emptyDirs and is a deferred, runtime-verified follow-up (as headlamp notes).
|
|
volumeMounts:
|
|
# ONE Longhorn PVC mounted five times by subPath — exactly the dirs Crafty's docker-compose
|
|
# maps. Everything else under /crafty is read-only image content.
|
|
- { name: crafty-data, mountPath: /crafty/servers, subPath: servers }
|
|
- { name: crafty-data, mountPath: /crafty/backups, subPath: backups }
|
|
- { name: crafty-data, mountPath: /crafty/logs, subPath: logs }
|
|
- { name: crafty-data, mountPath: /crafty/import, subPath: import }
|
|
- { name: crafty-data, mountPath: /crafty/app/config, subPath: config }
|
|
# Crafty inits its SQLite DB + Tornado server on first boot (can take ~30-60s). TCP-probe the
|
|
# HTTPS UI port (an HTTPS GET would fight the self-signed cert). Generous startup budget.
|
|
startupProbe:
|
|
tcpSocket:
|
|
port: https-ui
|
|
periodSeconds: 10
|
|
failureThreshold: 30
|
|
livenessProbe:
|
|
tcpSocket:
|
|
port: https-ui
|
|
periodSeconds: 20
|
|
failureThreshold: 6
|
|
readinessProbe:
|
|
tcpSocket:
|
|
port: https-ui
|
|
periodSeconds: 15
|
|
# ─────────────────────────── oauth2-proxy reverse-proxy sidecar ─────────────────────────────
|
|
- name: oauth2-proxy
|
|
# quay.io/oauth2-proxy/oauth2-proxy — same authenticator family as platform/oauth2-proxy, but
|
|
# here in per-app REVERSE-PROXY mode (--upstream is the co-located Crafty UI, not static://202).
|
|
image: quay.io/oauth2-proxy/oauth2-proxy:v7.8.1
|
|
imagePullPolicy: IfNotPresent
|
|
args:
|
|
- --http-address=0.0.0.0:4180
|
|
- --reverse-proxy=true
|
|
- --provider=oidc
|
|
- --oidc-issuer-url=https://keycloak.${BASE_DOMAIN}/realms/homelab
|
|
# Gate the co-located Crafty UI. Crafty serves HTTPS with a SELF-SIGNED cert, so skip verify
|
|
# on this in-pod (127.0.0.1) hop; the browser-facing TLS is the trusted wildcard at the Gateway.
|
|
- --upstream=https://127.0.0.1:8443
|
|
- --ssl-upstream-insecure-skip-verify=true
|
|
- --email-domain=*
|
|
- --scope=openid email profile
|
|
- --oidc-groups-claim=groups
|
|
# R-IDP-06: a valid Keycloak session is NOT enough — the user must ALSO be in platform-admins,
|
|
# else /oauth2 returns 403. (Matches the platform oauth2-proxy floor.)
|
|
- --allowed-group=platform-admins
|
|
# Single provider => 302 straight to Keycloak, no oauth2-proxy interstitial button.
|
|
- --skip-provider-button=true
|
|
- --redirect-url=https://crafty.${BASE_DOMAIN}/oauth2/callback
|
|
- --cookie-domain=.${BASE_DOMAIN}
|
|
- --whitelist-domain=.${BASE_DOMAIN}
|
|
- --cookie-secure=true
|
|
- --pass-access-token=true
|
|
- --set-xauthrequest=true
|
|
env:
|
|
# client-id / client-secret MUST match the Keycloak `crafty` client
|
|
# (keycloak-realm-clients secret, crafty-client-secret). cookie-secret is a random 32-byte
|
|
# value. All three from the SOPS-encrypted crafty-oauth2-proxy Secret, by name only (R-IDP-04).
|
|
- name: OAUTH2_PROXY_CLIENT_ID
|
|
valueFrom:
|
|
secretKeyRef: { name: crafty-oauth2-proxy, key: client-id }
|
|
- name: OAUTH2_PROXY_CLIENT_SECRET
|
|
valueFrom:
|
|
secretKeyRef: { name: crafty-oauth2-proxy, key: client-secret }
|
|
- name: OAUTH2_PROXY_COOKIE_SECRET
|
|
valueFrom:
|
|
secretKeyRef: { name: crafty-oauth2-proxy, key: cookie-secret }
|
|
ports:
|
|
- { name: http, containerPort: 4180, protocol: TCP }
|
|
resources:
|
|
requests:
|
|
cpu: 25m
|
|
memory: 64Mi
|
|
limits:
|
|
memory: 128Mi
|
|
securityContext:
|
|
allowPrivilegeEscalation: false
|
|
readOnlyRootFilesystem: true
|
|
capabilities:
|
|
drop:
|
|
- ALL
|
|
livenessProbe:
|
|
httpGet:
|
|
path: /ping
|
|
port: http
|
|
initialDelaySeconds: 5
|
|
periodSeconds: 15
|
|
readinessProbe:
|
|
httpGet:
|
|
path: /ping
|
|
port: http
|
|
initialDelaySeconds: 5
|
|
periodSeconds: 15
|
|
volumes:
|
|
- name: crafty-data
|
|
persistentVolumeClaim:
|
|
claimName: crafty-data
|