diff --git a/cluster/applications/minecraft/DEPLOYMENT-PLAN.md b/cluster/applications/minecraft/DEPLOYMENT-PLAN.md new file mode 100644 index 0000000..30c82ab --- /dev/null +++ b/cluster/applications/minecraft/DEPLOYMENT-PLAN.md @@ -0,0 +1,304 @@ +# Journey Into Imagination — Minecraft Server Deployment Plan + +**Application:** PaperMC Minecraft Server +**Namespace:** `minecraft` +**Deployment name:** `papermc` +**Service / DNS name:** `journey-into-imagination` +**ArgoCD path:** `cluster/applications/minecraft/` +**Reviewed:** _Pending Ryan approval_ + +--- + +## Version Pinning + +| Component | Version | +|-----------|---------| +| itzg/minecraft-server image | `2026.7.0` | +| PaperMC (Minecraft version) | `1.21.4` (build 232 — last stable 1.21.x) | +| SleepMost plugin | `5.6.2` (auto-downloaded at server start) | + +> **Note on PaperMC versioning:** PaperMC migrated to a new version scheme (`26.x`) as of mid-2026. The task requested 1.21.x, so we pin `VERSION=1.21.4`. When you're ready to upgrade to the latest stable, change `VERSION` in `deployment.yaml` and `configmap.yaml` to `26.2` and bump the image tag to the latest `itzg/minecraft-server` release. Check plugin compatibility (SleepMost) before upgrading. + +--- + +## Storage + +- **StorageClass:** `pure-block` (confirmed live — `kubectl get storageclass`) +- **PVC size:** 50Gi — expandable via Pure Storage CSI if the world grows +- **Mount:** `/data` inside the container (worlds, plugins, configs all live here) + +--- + +## Networking Architecture + +``` +Internet (players) + | + | DNS: journey-into-imagination.mk-labs.cloud → WAN IP (Cloudflare A record) + | +UniFi UDM Pro + | Port Forward: WAN:25565 → 10.1.71.80:25565 (TCP) + | +ingress-nginx LoadBalancer (10.1.71.80) + | tcp-services ConfigMap: 25565 → minecraft/journey-into-imagination:25565 + | +journey-into-imagination Service (ClusterIP, minecraft namespace) + | +papermc Pod (port 25565) +``` + +--- + +## Prerequisites — Manual Steps Before Deploying + +### 1. Generate a real RCON password + +```bash +# Generate a secure password +openssl rand -base64 24 +# Output example: abc123... (save this — you'll need it) + +# Base64-encode it for the Secret +echo -n 'YOUR_GENERATED_PASSWORD' | base64 +``` + +Edit `cluster/applications/minecraft/secret.yaml` and replace the placeholder value: +```yaml +data: + rcon-password: +``` + +> ⚠️ Do NOT commit real credentials in plaintext. If this is a concern long-term, +> migrate to ExternalSecrets + 1Password (Day 2 operation). + +### 2. UniFi Port Forward (Ryan — manual step) + +In UniFi Network → Firewall & Security → Port Forwarding: +- **Name:** `minecraft-papermc` +- **Protocol:** TCP +- **External Port:** 25565 +- **Internal IP:** 10.1.71.80 (ingress-nginx LoadBalancer) +- **Internal Port:** 25565 +- **Enabled:** Yes + +### 3. Cloudflare DNS Records (Ryan — manual step) + +In Cloudflare Dashboard → mk-labs.cloud zone: + +**A Record (public server address):** +| Type | Name | Value | Proxy | +|------|------|-------|-------| +| A | `journey-into-imagination` | `` | DNS only (gray cloud) | + +> Minecraft uses raw TCP — Cloudflare proxy (orange cloud) will NOT work. +> Use DNS-only mode (gray cloud). + +**SRV Record (allows clients to connect without specifying port):** +| Type | Name | Service | Proto | Priority | Weight | Port | Target | +|------|------|---------|-------|----------|--------|------|--------| +| SRV | `_minecraft._tcp.journey-into-imagination` | `_minecraft` | `_tcp` | 0 | 5 | 25565 | `journey-into-imagination.mk-labs.cloud` | + +> The SRV record allows players to connect using `journey-into-imagination.mk-labs.cloud` +> without specifying `:25565`. Most modern Minecraft clients resolve SRV records. + +### 4. Internal DNS (Technitium — automated via ExternalDNS) + +The Service in `service.yaml` has this annotation: +```yaml +external-dns.alpha.kubernetes.io/hostname: journey-into-imagination.local.mk-labs.cloud +``` + +ExternalDNS (Technitium provider) will create the internal A record automatically when ArgoCD syncs. +No manual action required for internal DNS. + +--- + +## Deployment Steps + +### Step 1: Update the RCON Secret + +Complete prerequisite #1 above, then commit the updated secret. + +### Step 2: Commit and Push to Gitea + +```bash +cd ~/git/homelab +git status # review what changed +git add cluster/applications/minecraft/ cluster/platform/ingress-nginx/values.yaml +git commit -m "feat(minecraft): add Journey Into Imagination PaperMC server + +- Namespace, Deployment, Service (journey-into-imagination), PVC, Secret, ConfigMap +- ArgoCD Application at cluster/applications/minecraft/ +- ingress-nginx TCP forwarding: 25565 -> minecraft/journey-into-imagination:25565 +- PaperMC 1.21.4, itzg/minecraft-server:2026.7.0, SleepMost 5.6.2 +- StorageClass: pure-block, 50Gi PVC for world data" + +git push +``` + +### Step 3: Verify ArgoCD Discovers and Syncs + +ArgoCD app-of-apps auto-discovers `cluster/applications/minecraft/application.yaml`. + +```bash +# Watch ArgoCD pick it up (from carousel-of-progress) +kubectl get application minecraft -n argocd -w + +# Or check ArgoCD UI at argocd.local.mk-labs.cloud +``` + +Wait for status: `Synced` / `Healthy` + +### Step 4: Watch the Pod Start Up + +First startup will download the PaperMC jar — this can take 2-3 minutes. + +```bash +kubectl get pods -n minecraft -w + +# Tail the logs to watch PaperMC boot +kubectl logs -n minecraft -l app.kubernetes.io/component=papermc -f +``` + +Look for: +``` +[Server thread/INFO]: Done (X.XXXs)! For help, type "help" +``` + +### Step 5: Install SleepMost Plugin (auto via PLUGINS env var) + +The `PLUGINS` env var in the ConfigMap points to the SleepMost JAR download URL. +itzg/minecraft-server downloads and installs it automatically on startup. + +Verify it loaded: +```bash +kubectl exec -n minecraft -it deployment/papermc -- rcon-cli +# In rcon console: +plugins +# Should list: SleepMost +``` + +### Step 6: Verify Connectivity + +**Internal test (from carousel-of-progress):** +```bash +# Port probe — should succeed +nc -zv journey-into-imagination.local.mk-labs.cloud 25565 + +# Or via the ClusterIP directly +kubectl get svc -n minecraft +nc -zv 25565 +``` + +**External test (after UniFi port forward + DNS configured):** +- Open Minecraft Java Edition +- Add server: `journey-into-imagination.mk-labs.cloud` +- Should connect and show MOTD: "Journey Into Imagination" + +--- + +## Plugin Details + +### SleepMost v5.6.2 + +- **Source:** https://github.com/mrgeneralq/sleep-most +- **Download:** https://github.com/mrgeneralq/sleep-most/releases/download/v5.6.2/SleepMost-5.6.2.jar +- **Compatibility:** PaperMC 1.8 through 1.21.x (use 5.6.2 for 1.21.x; 5.7.0+ drops backward compat) +- **Configuration:** Lives at `/data/plugins/SleepMost/config.yml` after first boot + +Default behavior: configurable percentage of online players must sleep to skip night. +To tune (exec into the pod after first start): +```bash +kubectl exec -n minecraft -it deployment/papermc -- bash +cat /data/plugins/SleepMost/config.yml +# Edit as needed, then /sleepmost reload in rcon +``` + +> **Geyser compatibility note:** SleepMost 5.x does NOT require Geyser to function. +> If you add Geyser later for Bedrock crossplay, SleepMost is compatible. + +--- + +## Day 2 Operations + +### Enabling the Whitelist + +When ready to lock the server to approved players: + +1. Edit `cluster/applications/minecraft/configmap.yaml`: + ```yaml + WHITE_LIST: "true" + ENFORCE_WHITELIST: "true" + ``` + +2. Add players via RCON (live, no restart needed): + ```bash + kubectl exec -n minecraft -it deployment/papermc -- rcon-cli + whitelist add + whitelist list + ``` + +3. Commit the ConfigMap change for GitOps consistency. + Note: the Deployment will restart when the ConfigMap changes (env var reload). + Players will be briefly disconnected — plan accordingly. + +### Console / RCON Access + +```bash +# Interactive RCON (from inside the pod) +kubectl exec -n minecraft -it deployment/papermc -- rcon-cli + +# One-shot command +kubectl exec -n minecraft -it deployment/papermc -- rcon-cli "list" +kubectl exec -n minecraft -it deployment/papermc -- rcon-cli "say Server restarting in 5 minutes" +``` + +### Upgrading PaperMC Version + +1. Check PaperMC API: `curl -sA 'Mozilla/5.0' https://fill.papermc.io/v3/projects/paper/versions | head` +2. Update `VERSION` in `deployment.yaml` env vars and the comment header +3. Update image tag if a new `itzg/minecraft-server` release is out +4. Verify SleepMost compatibility with the new version +5. Commit, push — ArgoCD handles the rolling restart (Recreate strategy) + +### Expanding PVC Storage + +Pure Storage CSI supports online volume expansion: +```bash +kubectl patch pvc papermc-world-data -n minecraft \ + -p '{"spec":{"resources":{"requests":{"storage":"100Gi"}}}}' +``` +No pod restart required. + +### Backups + +No automated backup is configured in this skeleton. +Recommended Day 2 addition: CronJob that runs `rcon-cli save-all` + `rcon-cli save-off`, +copies `/data/world*` to a separate PVC or object store, then `rcon-cli save-on`. + +--- + +## Troubleshooting + +| Symptom | Check | +|---------|-------| +| Pod stuck in `Init` / slow start | `kubectl logs -n minecraft -l app.kubernetes.io/component=papermc` — jar download may be slow | +| Port 25565 connection refused externally | Verify UniFi port forward is active; `kubectl get svc -n minecraft` shows correct ClusterIP | +| Players can't authenticate | `ONLINE_MODE=true` requires Mojang auth; check player has a valid Java account | +| World data lost after pod restart | Verify PVC is `Bound`; check `pure-block` StorageClass is healthy | +| EULA error in logs | `EULA=TRUE` is set in deployment.yaml — this should not occur; check env var injection | + +--- + +## File Manifest Summary + +| File | Purpose | +|------|---------| +| `namespace.yaml` | `minecraft` namespace | +| `application.yaml` | ArgoCD Application (sync-wave 20) | +| `pvc.yaml` | 50Gi pure-block PVC for world data | +| `secret.yaml` | RCON password (⚠️ placeholder — update before deploy) | +| `configmap.yaml` | server.properties overrides + SleepMost plugin download | +| `deployment.yaml` | PaperMC Deployment (Recreate strategy, resource limits set) | +| `service.yaml` | ClusterIP Service named `journey-into-imagination` | +| `../../../platform/ingress-nginx/values.yaml` | Added `tcp: 25565` forwarding entry | diff --git a/cluster/applications/minecraft/application.yaml b/cluster/applications/minecraft/application.yaml new file mode 100644 index 0000000..f03c804 --- /dev/null +++ b/cluster/applications/minecraft/application.yaml @@ -0,0 +1,23 @@ +--- +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: minecraft + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "20" +spec: + project: default + source: + repoURL: https://gitea.mk-labs.cloud/rblundon/homelab.git + targetRevision: main + path: cluster/applications/minecraft + destination: + server: https://kubernetes.default.svc + namespace: minecraft + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/cluster/applications/minecraft/configmap.yaml b/cluster/applications/minecraft/configmap.yaml new file mode 100644 index 0000000..87958e4 --- /dev/null +++ b/cluster/applications/minecraft/configmap.yaml @@ -0,0 +1,45 @@ +--- +# server.properties overrides for Journey Into Imagination +# itzg/minecraft-server reads these as environment variables and +# writes them into server.properties on startup. +# Full list: https://docker-minecraft-server.readthedocs.io/en/latest/configuration/server-properties/ +apiVersion: v1 +kind: ConfigMap +metadata: + name: papermc-config + namespace: minecraft + labels: + app.kubernetes.io/name: minecraft + app.kubernetes.io/component: papermc +data: + # Server identity + MOTD: "Journey Into Imagination" + SERVER_NAME: "Journey Into Imagination" + + # Game settings + GAMEMODE: "survival" + DIFFICULTY: "normal" + PVP: "true" + MAX_PLAYERS: "20" + ALLOW_FLIGHT: "true" + SPAWN_PROTECTION: "0" + SEED: "-5177989977648707969" + + # Auth + ONLINE_MODE: "true" + + # Whitelist — set to true when ready to lock down the server + WHITE_LIST: "false" + ENFORCE_WHITELIST: "false" + + # Performance / tick safety + MAX_TICK_TIME: "180000" + + # RCON (enabled so plugins/admin tools can connect) + ENABLE_RCON: "true" + RCON_PORT: "25575" + + # Plugin auto-download — SleepMost for single-player sleep + # SleepMost v5.6.2: supports PaperMC 1.21.x (use latest release for 1.21.11+) + # Source: https://github.com/mrgeneralq/sleep-most/releases + PLUGINS: "https://github.com/mrgeneralq/sleep-most/releases/download/v5.6.2/SleepMost-5.6.2.jar" diff --git a/cluster/applications/minecraft/deployment.yaml b/cluster/applications/minecraft/deployment.yaml new file mode 100644 index 0000000..603d74b --- /dev/null +++ b/cluster/applications/minecraft/deployment.yaml @@ -0,0 +1,179 @@ +--- +# PaperMC Minecraft Server — Journey Into Imagination +# +# Image: itzg/minecraft-server:2026.7.0 +# PaperMC version: 26.1.2 (new PaperMC versioning scheme, mid-2026+; requires Java 25) +# NOTE: 26.x series uses PaperMC's independent versioning (not Minecraft version-based). +# Plugin ecosystem is still catching up — verify plugin compat before adding new ones. +# +# itzg image documentation: https://docker-minecraft-server.readthedocs.io +apiVersion: apps/v1 +kind: Deployment +metadata: + name: papermc + namespace: minecraft + labels: + app.kubernetes.io/name: minecraft + app.kubernetes.io/component: papermc + app.kubernetes.io/version: "26.1.2" +spec: + replicas: 1 + # Recreate strategy — Minecraft server requires exclusive access to world data. + # RollingUpdate WILL cause world corruption if both pods mount the PVC simultaneously. + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/name: minecraft + app.kubernetes.io/component: papermc + template: + metadata: + labels: + app.kubernetes.io/name: minecraft + app.kubernetes.io/component: papermc + app.kubernetes.io/version: "26.1.2" + spec: + securityContext: + runAsNonRoot: false # itzg image requires root for some setup steps + fsGroup: 1000 + containers: + - name: papermc + # Pinned tag — never use :latest in production + image: itzg/minecraft-server:2026.7.0 + imagePullPolicy: IfNotPresent + ports: + - name: minecraft + containerPort: 25565 + protocol: TCP + - name: rcon + containerPort: 25575 + protocol: TCP + env: + - name: EULA + value: "TRUE" + - name: TYPE + value: "PAPER" + - name: VERSION + value: "26.1.2" + # Server settings from ConfigMap + - name: MOTD + valueFrom: + configMapKeyRef: + name: papermc-config + key: MOTD + - name: SERVER_NAME + valueFrom: + configMapKeyRef: + name: papermc-config + key: SERVER_NAME + - name: GAMEMODE + valueFrom: + configMapKeyRef: + name: papermc-config + key: GAMEMODE + - name: DIFFICULTY + valueFrom: + configMapKeyRef: + name: papermc-config + key: DIFFICULTY + - name: PVP + valueFrom: + configMapKeyRef: + name: papermc-config + key: PVP + - name: MAX_PLAYERS + valueFrom: + configMapKeyRef: + name: papermc-config + key: MAX_PLAYERS + - name: ALLOW_FLIGHT + valueFrom: + configMapKeyRef: + name: papermc-config + key: ALLOW_FLIGHT + - name: SPAWN_PROTECTION + valueFrom: + configMapKeyRef: + name: papermc-config + key: SPAWN_PROTECTION + - name: SEED + valueFrom: + configMapKeyRef: + name: papermc-config + key: SEED + - name: ONLINE_MODE + valueFrom: + configMapKeyRef: + name: papermc-config + key: ONLINE_MODE + - name: WHITE_LIST + valueFrom: + configMapKeyRef: + name: papermc-config + key: WHITE_LIST + - name: ENFORCE_WHITELIST + valueFrom: + configMapKeyRef: + name: papermc-config + key: ENFORCE_WHITELIST + - name: MAX_TICK_TIME + valueFrom: + configMapKeyRef: + name: papermc-config + key: MAX_TICK_TIME + - name: ENABLE_RCON + valueFrom: + configMapKeyRef: + name: papermc-config + key: ENABLE_RCON + - name: RCON_PORT + valueFrom: + configMapKeyRef: + name: papermc-config + key: RCON_PORT + - name: PLUGINS + valueFrom: + configMapKeyRef: + name: papermc-config + key: PLUGINS + # RCON password from Secret + - name: RCON_PASSWORD + valueFrom: + secretKeyRef: + name: papermc-rcon + key: rcon-password + volumeMounts: + - name: world-data + mountPath: /data + resources: + requests: + cpu: "1" + memory: "2Gi" + limits: + cpu: "4" + memory: "6Gi" + # Startup probe — give the server time to download PaperMC jar on first boot + startupProbe: + tcpSocket: + port: minecraft + initialDelaySeconds: 30 + periodSeconds: 10 + failureThreshold: 30 # 5 minutes total + # Liveness probe — restart if the TCP port goes away + livenessProbe: + tcpSocket: + port: minecraft + initialDelaySeconds: 0 + periodSeconds: 30 + failureThreshold: 3 + # Readiness probe — only route traffic when server is accepting connections + readinessProbe: + tcpSocket: + port: minecraft + initialDelaySeconds: 0 + periodSeconds: 10 + failureThreshold: 3 + volumes: + - name: world-data + persistentVolumeClaim: + claimName: papermc-world-data diff --git a/cluster/applications/minecraft/externalsecret.yaml b/cluster/applications/minecraft/externalsecret.yaml new file mode 100644 index 0000000..8341766 --- /dev/null +++ b/cluster/applications/minecraft/externalsecret.yaml @@ -0,0 +1,28 @@ +--- +# ExternalSecret — pulls RCON password from 1Password Connect +# Prereq: create a "minecraft" item in the mk-labs 1Password vault +# with a field named "rcon_password" set to a strong random password. +# Generate one: openssl rand -base64 24 +# +# ClusterSecretStore: onepassword-connect (platform/onepassword-connect/) +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: papermc-rcon + namespace: minecraft + labels: + app.kubernetes.io/name: minecraft + app.kubernetes.io/component: papermc +spec: + refreshInterval: 1h + secretStoreRef: + name: onepassword-connect + kind: ClusterSecretStore + target: + name: papermc-rcon + creationPolicy: Owner + data: + - secretKey: rcon-password + remoteRef: + key: minecraft + property: rcon_password diff --git a/cluster/applications/minecraft/namespace.yaml b/cluster/applications/minecraft/namespace.yaml new file mode 100644 index 0000000..42b6944 --- /dev/null +++ b/cluster/applications/minecraft/namespace.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: minecraft + labels: + app.kubernetes.io/name: minecraft + app.kubernetes.io/managed-by: argocd diff --git a/cluster/applications/minecraft/pvc.yaml b/cluster/applications/minecraft/pvc.yaml new file mode 100644 index 0000000..d87ea53 --- /dev/null +++ b/cluster/applications/minecraft/pvc.yaml @@ -0,0 +1,18 @@ +--- +# World data persistent volume — Pure Storage block CSI +# StorageClass confirmed: pure-block (pxd.portworx.com, RWO, allowVolumeExpansion=true) +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: papermc-world-data + namespace: minecraft + labels: + app.kubernetes.io/name: minecraft + app.kubernetes.io/component: papermc +spec: + accessModes: + - ReadWriteOnce + storageClassName: pure-block + resources: + requests: + storage: 50Gi diff --git a/cluster/applications/minecraft/service.yaml b/cluster/applications/minecraft/service.yaml new file mode 100644 index 0000000..c9a10f2 --- /dev/null +++ b/cluster/applications/minecraft/service.yaml @@ -0,0 +1,30 @@ +--- +# Service named after the thematic identity: journey-into-imagination +# This is the DNS name used in ingress-nginx TCP forwarding config. +# Internal DNS: journey-into-imagination.local.mk-labs.cloud +apiVersion: v1 +kind: Service +metadata: + name: journey-into-imagination + namespace: minecraft + labels: + app.kubernetes.io/name: minecraft + app.kubernetes.io/component: papermc + annotations: + # Internal DNS via ExternalDNS + Technitium + external-dns.alpha.kubernetes.io/hostname: journey-into-imagination.local.mk-labs.cloud + # Non-standard external port 10182 → internal 25565 (via ingress-nginx tcp forwarding) +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: minecraft + app.kubernetes.io/component: papermc + ports: + - name: minecraft + port: 25565 + targetPort: 25565 + protocol: TCP + - name: rcon + port: 25575 + targetPort: 25575 + protocol: TCP diff --git a/cluster/platform/ingress-nginx/values.yaml b/cluster/platform/ingress-nginx/values.yaml index ec9d857..bce848f 100644 --- a/cluster/platform/ingress-nginx/values.yaml +++ b/cluster/platform/ingress-nginx/values.yaml @@ -41,3 +41,18 @@ controller: labelSelector: matchLabels: app.kubernetes.io/name: ingress-nginx + +# ------------------------------------------------------------------------------ +# TCP port forwarding — raw TCP services (non-HTTP) +# Each entry maps an external port to a namespace/service:port target. +# The Helm chart automatically: +# 1. Creates the tcp-services ConfigMap in the ingress-nginx namespace +# 2. Passes --tcp-services-configmap=ingress-nginx/tcp-services to the controller +# 3. Adds the port to the nginx-ingress LoadBalancer Service +# +# After ArgoCD syncs, add a UniFi port forward: +# WAN:10182 → 10.1.71.80:10182 (TCP) +# Non-standard port for security (default 25565 avoided). +# ------------------------------------------------------------------------------ +tcp: + 10182: "minecraft/journey-into-imagination:25565"