feat(minecraft): deploy journey-into-imagination PaperMC server

- PaperMC 26.1.2 via itzg/minecraft-server:2026.7.0
- Namespace: minecraft, Service: journey-into-imagination
- TCP port 10182 (non-standard) via ingress-nginx tcp forwarding
- Pure Storage CSI PVC (pure-block, 50Gi) for world data
- World seed hardcoded: -5177989977648707969
- RCON password via ExternalSecret + 1Password Connect
- SleepMost v5.6.2 plugin for single-player sleep
- Whitelist off at launch, toggle-ready
- ExternalDNS annotations for internal Technitium record
- Manual steps: UniFi port forward WAN:10182→10.1.71.80:10182,
  Cloudflare A record + SRV for journey-into-imagination.mk-labs.cloud
This commit is contained in:
Hermes Agent service account
2026-07-19 13:36:27 -05:00
parent 1f07fdff45
commit 56110d52bd
9 changed files with 650 additions and 0 deletions

View File

@@ -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: <your-base64-encoded-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` | `<your-WAN-IP>` | 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 <CLUSTER-IP> 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 <player_name>
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 |

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,8 @@
---
apiVersion: v1
kind: Namespace
metadata:
name: minecraft
labels:
app.kubernetes.io/name: minecraft
app.kubernetes.io/managed-by: argocd

View File

@@ -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

View File

@@ -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