4
minecraft server
Justin Klein edited this page 2026-10-14 14:57:00 +00:00
This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

minecraft-server

Deploys a Minecraft server (itzg/minecraft-server) as a single-replica StatefulSet on one RWO world volume. No Ingress, no HTTP: clients speak the Minecraft protocol over raw TCP, routed in the cluster by the separate minecraft-router release. Only the newest Java line is maintained out of this directory.

Image itzg/minecraft-server — the tag fixes the JVM
Versions not listed on purpose — read the current pair off minecraft-server/Chart.yaml, see Chart index
Workloads StatefulSet (1 replica), plus an init container with curseforgeServerpack
Storage one RWO PVC, <release>-mc-volume-claim, two subPaths (mc-data → /data, mc-modpacks → /modpacks)
Ingress none — ClusterIP by default, NodePort optional
External dependencies Playerdb (operator name resolution), CurseForge or FTB API when a modpack source is on
Secrets 1 × OnePasswordItem, rendered unconditionally
Install-test ci/minecraft-server/ (fixtures + test values + settings.env)

Prerequisites

  • 1Password operator in the cluster, and a 1Password item — the CR is rendered unconditionally, so secrets.minecraftServerRef must be set even when nothing consumes a key from it. See the fields below.
  • A storage class that provisions RWO volumes, named in minecraft.resources.storageClassName (default longhorn).
  • A way in for players: either an entry in the minecraft-router release's static mappings table pointing at this Service, or server.service.type: NodePort. See Nothing reaches this server by itself.
  • A minecraft.version that fits the Java line of the image tag. Since chart 5.0.0 the chart checks the pairing and fails the render — see The Java line is the chart's central fact.

Installation

The registry URL and its credentials live in 1Password (item jlab-registry-access: username, password, charts-url) and never in a repository or a shell history file.

helm repo add jlab "$CHARTS_URL" --username "$REG_USER" --password "$REG_PASS"
helm repo update

helm install mc jlab/minecraft-server \
  --namespace minecraft --create-namespace \
  -f my-values.yaml

Minimal my-values.yaml:

secrets:
  minecraftServerRef: op://vault/minecraft-server

Everything else has a default that matches the published Java line. helm install prints the chart's NOTES.txt, which renders the same information against the values actually set — including the effective image tag, its Java line, the two memory numbers and whether the whitelist currently locks everyone out. Read it — it is the per-release version of this page.

helm install returns before any of this happens: without --wait, success is reported the moment the objects are applied, while the image is still downloading the server jar (and the whole modpack). Watch the rollout instead of trusting the exit code:

kubectl -n <namespace> rollout status statefulset/<release>-minecraft-server-sfs

What the 1Password item must contain

The chart renders a OnePasswordItem CR; the operator turns it into a Kubernetes Secret whose keys are the field names of the 1Password item. A missing field does not fail the install: the release goes through and the pod then sits in CreateContainerConfigError while the Secret itself exists, so it looks as though the operator had done its job.

Values key Kubernetes Secret Fields
secrets.minecraftServerRef <release>-minecraft-server-secret rcon-password → RCON_PASSWORD, only when server.rcon.enabled is true; curseforge-api-key → CF_API_KEY, only when curseforge.enabled is true

With both features off the item may be empty — but the values key is still required. Since chart 6.0.0 required() enforces that: unset, the render aborts instead of producing a CR with an itemPath of null. What the operator actually delivered:

kubectl -n <namespace> get secret <release>-minecraft-server-secret -o jsonpath='{.data}'

Configurable values

Global

Key Type Default Description
scaleDown bool false Renders the StatefulSet with replicas: 0 without uninstalling. Takes precedence over server.replicas. The world PVC, the Service and the OnePasswordItem stay.
global object unset Helm's conventional cross-chart values. Unused by this chart; declared in the schema and kept open so an umbrella install cannot break on it.

image

Key Type Default Description
image.tagOverride string "" Tag of the itzg/minecraft-server image. Empty means the chart's appVersion. An emergency exit, not a knob — see Running an older Java line. Deliberately outside Renovate's reach. The Java line of the effective tag is validated against minecraft.version at render time.

The whole image block may be null; the tag then falls back to appVersion, which is the pre-5.0.0 behaviour.

secrets

Key Type Default Description
secrets.minecraftServerRef string null Required, always — the CR is rendered unconditionally, and required() enforces the value since 6.0.0 (the render aborts, naming the key). op:// item path; the item needs fields only for the features actually switched on.

server

Key Type Default Description
server.replicas int 1 Replica count; 0 is allowed. More than one cannot work — see One replica, one world.
server.port int 25565 Port inside the container. Passed as SERVER_PORT, used as the containerPort and read by the image's mc-health script, so an override stays consistent everywhere.
server.service.type string ClusterIP ClusterIP or NodePort (enumerated in the schema). ClusterIP is the default because routing is done by minecraft-router over the in-cluster DNS name.
server.service.port int 25565 Port the Service publishes inside the cluster.
server.service.outwardPort int 30565 Node port, used only with type: NodePort. Must be inside the cluster's node-port range (30000–32767 by default); NOTES.txt warns when it is not.
server.motd string Running + modpack name and version, with Java-properties unicode escapes (backslash-u00A7) for the Minecraft colour-code character Those escapes must stay literal backslash sequences — server.properties is a Java properties file and unescapes them itself. Copy the default out of values.yaml rather than retyping it.
server.rcon.enabled bool false Only when true does the chart set ENABLE_RCON, declare the RCON container port and wire RCON_PASSWORD from the Secret.
server.rcon.port int 25575 RCON port. The Service does not publish it — see RCON is declared, not published.
server.initImage string alpine:3.24 Init container that downloads and unpacks a CurseForge server pack; used only with curseforgeServerpack.enabled. Pinned per repo convention — auxiliary images are never unpinned.
server.resources.limitFactor number 3 Memory and ephemeral-storage limits are computed as factor × request.
server.resources.requests.cpu string/number "2"
server.resources.requests.ephemeral-storage string/number "100Mi" Both spellings (ephemeral-storage, ephemeralStorage) are accepted; the helper normalizes to the Kubernetes key.
server.resources.requests.memory string/number null — derived Deliberately unset: the chart derives it from minecraft.resources.memory (the JVM heap) so heap and container memory cannot drift apart. With the default 4G heap that is a 4Gi request and a 12Gi limit. Setting it explicitly decouples the two — see Two memory numbers that must not drift apart.
server.resources.limits object unset An explicit limit always wins. A CPU limit is only rendered when set here.

env

Key Type Default Description
env list [] Extra environment variables for the server container, appended to everything the chart sets. Rendered through tpl; value, secretKeyRef, configMapKeyRef forms. Because the chart's own variables come first, an entry repeating one of them is what the image sees last — the escape hatch for anything the chart does not model, and equally the way to break a setting it does. A plain value is readable by anyone who can read pods in the namespace; anything sensitive belongs in valueFrom.secretKeyRef.

Probes

All three run the image's own mc-health script — a real Minecraft server-list ping against SERVER_PORT via mc-monitor, which is the only health check this image offers. An erased block falls back to these defaults rather than removing the probe.

Key Type Default Description
livenessProbe object exec: ["mc-health"], period 30s, timeout 10s, 3 failures
readinessProbe object exec: ["mc-health"], period 15s, timeout 10s, 3 failures
startupProbe object exec: ["mc-health"], period 15s, timeout 10s, 80 failures (= 20 minutes) Gates liveness and readiness, so a slow start can no longer be killed. The budget has to cover a fresh volume: server jar download and, for FTB/CurseForge, the whole modpack, before the world even begins to load.

securityContext

Key Type Default Description
securityContext.pod object non-root 1000:1000, fsGroup: 1000, fsGroupChangePolicy: OnRootMismatch uid/gid 1000 is the image's minecraft user. Starting as that user directly makes the entrypoint take its non-root branch, which skips a recursive chown of /data and a write to /etc/nsswitch.conf — the latter would fail on a read-only root filesystem. OnRootMismatch instead of Always avoids a recursive chown of the whole world on every pod start (minutes on a 32Gi world) while still running once if the volume root lacks the group. This block is read twice: as the pod's securityContext and by the serverpack init script, which chowns migrated files to the same uid/gid.
securityContext.container object readOnlyRootFilesystem: true, runAsNonRoot: true, all caps dropped, no privilege escalation Applied to the server container and the serverpack init container. The read-only root filesystem works because the chart mounts an emptyDir at /tmp: the image writes its server-icon scratch file and mc-server-runner's console named pipe there; everything else goes to the PVC.

minecraft

Key Type Default Description
minecraft.difficulty string easy peaceful, easy, normal, hard (enumerated in the schema).
minecraft.whitelist.enabled bool false Wired to ENABLE_WHITELIST in both positions since chart 2.0.0, which is what makes false actually switch the whitelist off again in a persisted server.properties. Also drives ENFORCE_WHITELIST. true without players locks everyone out, including the ops — see The whitelist can lock out everyone, invisibly.
minecraft.whitelist.overridePolicy string MERGE EXISTING_WHITELIST_FILE — how an already existing whitelist.json is treated: SKIP leaves it alone, MERGE adds these players, SYNCHRONIZE makes it match exactly and removes everyone else. Only applies while enabled.
minecraft.whitelist.players string null Comma-separated names, passed as WHITELIST only while enabled — a non-empty WHITELIST enables the whitelist in the image regardless of ENABLE_WHITELIST=false, so it must stay unset while disabled.
minecraft.management.opPlayers string null Comma-separated operators, passed as OPS with EXISTING_OPS_FILE=SYNCHRONIZE pinned — see Operators are synchronized from values. Every name is resolved against the external Playerdb API at startup, and a name that does not resolve aborts the start.
minecraft.management.maxPlayers int 10 MAX_PLAYERS.
minecraft.management.forceGameMode bool true FORCE_GAMEMODE.
minecraft.management.gameMode string survival creative, survival, adventure, spectator (enumerated).
minecraft.management.worldSaveName string world LEVEL.
minecraft.management.serverName string mc-server SERVER_NAME.
minecraft.resources.storage string 32Gi Size of the world PVC. Growing it works only if the storage class allows expansion; shrinking is refused by Kubernetes and fails the next helm upgrade on the PVC.
minecraft.resources.storageClassName string longhorn Immutable once the volume is bound — changing it does nothing to an existing release.
minecraft.resources.memory string/number "4" JVM heap in gigabytes, passed as MEMORY=<value>G. Also drives the container's memory request/limit unless server.resources.requests.memory is set.
minecraft.version string/number "26.2" Minecraft version, pinned rather than LATEST (which would drift on every restart). Must match the Java line of the effective image tag; checked at render time. Quote it — unquoted YAML reads 1.20 as the number 1.2, and the chart would pass "1.2" to the image as VERSION.

Modpack sources

curseforge, ftb and curseforgeServerpack are mutually exclusive; enabling more than one fails the render by name. Before chart 1.0.0 it silently rendered a duplicate TYPE entry and the last one won.

Key Type Default Description
curseforge.enabled bool false TYPE=AUTO_CURSEFORGE. The image resolves the pack against the CurseForge API at every start, using curseforge-api-key — so each start depends on a third-party API and on that key still being valid; an expired key is a server that stops coming up, not a warning.
curseforge.forceReload bool false CF_FORCE_SYNCHRONIZE. True re-synchronizes the pack on every single start; turn it off again once the reload has happened.
curseforge.modpack.slug string null CF_SLUG.
curseforge.modpack.versionId string/int null CF_FILE_ID.
curseforge.modpack.exclude string/int null CF_EXCLUDE_MODS.
curseforge.modpack.excludeOverride string/int null CF_OVERRIDES_EXCLUSIONS.
curseforge.modpack.include string/int null CF_FORCE_INCLUDE_MODS.
ftb.enabled bool false TYPE=FTBA. No API key, but the start depends on the Feed-the-Beast API being reachable, and the first one downloads the whole pack before the world loads.
ftb.modpackId string/int null FTB_MODPACK_ID.
ftb.versionId string/int null FTB_MODPACK_VERSION_ID.
curseforgeServerpack.enabled bool false TYPE=CURSEFORGE; an init container downloads and the server unpacks. downloadUrl becomes required — the chart fails the render otherwise, rather than letting the init container download an empty URL.
curseforgeServerpack.downloadUrl string null Required when curseforgeServerpack.enabled is true. Rendered into the init.sh ConfigMap.
curseforgeServerpack.workingDirectory string /data/serverpack CF_BASE_DIR, and where the init script unpacks. Also rendered into the ConfigMap.

A change to either of the last two rolls the pod (checksum/config on the StatefulSet), which is what makes it take effect at all — init.sh is mounted with subPath and only read at pod start. The restart is not free: ~124 s measured for ftb-skies on Longhorn.

networkPolicy

The tightest policy in the repository, and the reason is its probes: all three are exec probes, which run inside the container and put no packet on the network. So there is no kubelet traffic to accommodate on any port, every port can be restricted by source, and there is no probeSources value at all.

  • the game port (server.port): open when server.service.type is NodePort (players arrive from the internet, already masqueraded), and restricted to this namespace plus allowedNamespaces otherwise — which is the default. Both branches read the same value the Service does, so they cannot disagree.
  • RCON (server.rcon.port, rendered only while server.rcon.enabled) and metrics (19565): always restricted by source.

Outbound: DNS and TCP 443 (Mojang's session server for online-mode, plus resource packs and mods).

Value Type Default Meaning
networkPolicy.enabled bool true Renders the policy. Only a boundary where the CNI enforces one — unenforced, the API server accepts it and nothing is filtered, so everything reads as configured.
networkPolicy.allowedNamespaces list [] Namespaces whose pods may reach the restricted ports, additionally to the release's own. Matched on the kubernetes.io/metadata.name label Kubernetes sets itself — no labelling work, and no typo protection either: a name that does not exist matches nothing and is silently ineffective. Namespace-granular, so an entry opens the port to every pod in it.
networkPolicy.ingressController object kube-system / app.kubernetes.io/name: rke2-traefik The controller allowed to reach the routed port once probeSources restricts it. The two keys render as one from entry, i.e. an AND — which is why this is not a line in allowedNamespaces, where it would open the port to every pod in kube-system. Default measured on the cluster, not assumed.
networkPolicy.egress.extraPorts list [] Additional outbound TCP ports, to any destination. The escape hatch from the egress cap, and the only alternative to switching the policy off.

Operating notes

The Java line is the chart's central fact

The JVM is fixed by the image tag, not by a value of the game. appVersion is that tag, and since chart 2.0.0 it is a dated upstream release (2026.8.2-java25) rather than the rolling line tag (java25): rolling tags are rebuilt continuously, so a pod restart could pull a different image than the last one without anything being recorded anywhere. The dated tags are immutable, so the image is pinned by the chart version like every other chart here.

The boundaries are Mojang's, not this chart's — javaVersion.majorVersion from the piston-meta version manifest, swept over all 102 releases on 2026-09-02. There are exactly four transitions and they are monotonic in release order:

Minecraft Java line
up to 1.16.5 java8
1.17 – 1.17.1 java16 (never published as a chart line here)
1.18 – 1.20.4 java17
1.20.5 – 1.21.11 java21
26.1 and later java25

Since chart 5.0.0 the pairing is checked at render time and a mismatch fails with both values named and the corrected tag suggested. Two cases pass unchecked on purpose: a tag without a recognizable java<N> part, and a minecraft.version that is not a plain numeric x[.y[.z]] (a snapshot, a pre-release, LATEST). The chart cannot compare those, and a guard that rejects everything it does not understand gets worked around instead of respected. In those two the old failure mode still applies: the release installs, and the container fails afterwards — CrashLoopBackOff once the 20-minute startup budget is spent, with the reason in the log and nowhere else.

Only the newest line is maintained; the older ones are frozen

Up to chart 4.1.0 the chart was published once per line (java8 / java17 / java21 / java25) out of this single directory, which is what made the lines drift apart: whatever change landed while a line happened to lie in the directory reached that line only. That is over (#158). This directory carries exactly one line — the newest — and only it gets chart changes.

The older lines stay published as frozen artifacts and remain installable, but will never be updated again:

Line Frozen at chart version
java8 4.0.0
java17 4.0.0
java21 4.1.0

Running an older Java line: image.tagOverride

Installing a frozen chart version means giving up every later fix, so the line became a decision at rollout time instead of a property of the chart:

image:
  tagOverride: "2026.8.2-java21"
minecraft:
  version: "1.21.11"

It is called tagOverride, not tag, because it decouples the running image from the chart version: helm list then reports 6.0.0+up2026.8.2.java25 while the pod runs something else, and NOTES.txt says so. Prefer moving to the current line; use this when a modpack pins you to an older Minecraft version.

It is also deliberately outside Renovate's reach — renovate.json5 tracks appVersion in Chart.yaml and nothing else. A tag set here is never bumped by anything; it is yours to review.

Moving across a line boundary is therefore no longer a change of chart version: set image.tagOverride and minecraft.version in the same upgrade. Changing only one now fails the render instead of leaving a broken pod behind.

Nothing reaches this server by itself

There is no Ingress, no certificate and no URL.

  • ClusterIP (default): the server answers at <release>-minecraft-server-service.<namespace>.svc.cluster.local:<server.service.port>. The way in for players is the separate minecraft-router release, and that router does not discover backends: it routes on the hostname out of the Minecraft handshake, using the static mappings table in its own values file. Until an entry pointing at this Service is added there, the server is unreachable from outside no matter how healthy it is here — and no annotation on this release changes that. (Before chart 1.0.0 an mcRouterIntegration.enabled value added a misleading auto-discovery annotation; it is gone, only the service type remains, now named for what it does.)
  • NodePort: players dial <any-node-ip>:<server.service.outwardPort>. Note which of the three ports a player types — not server.service.port (in-cluster) and not server.port (in the container).

Either way, a port-forward reaches it without changing the Service:

kubectl -n <namespace> port-forward svc/<release>-minecraft-server-service 25565:25565

What a green rollout does and does not prove

mc-health is a real server-list ping, so Ready means more here than "the process is alive": the server is up and answering pings. It does not mean anyone can play. A server-list ping is answered before any login is attempted, so every reason a player is refused at login is invisible to all three probes — the whitelist is the usual one, and the rollout stays green throughout. The probes also run inside the pod and say nothing about whether players can reach it: with ClusterIP, a missing router mapping is a perfectly healthy server nobody can join.

The whitelist can lock out everyone, invisibly

minecraft.whitelist.enabled: true with players unset emits ENABLE_WHITELIST=true and no WHITELIST, which is white-list=true against an empty whitelist.json: every login is refused, including the operators. That is the image's own semantics and it is intended — and it is exactly the state the probes cannot see. NOTES.txt prints the case explicitly.

server.properties lives on the PVC, which is why this matters historically: before chart 2.0.0 the chart only ever emitted whitelist variables while enabled was true and had no way to say "off", so a release that had once written white-list=true kept it forever. ENFORCE_WHITELIST follows the same switch for the same reason — the image only ever sets it to true and never resets it.

Operators are synchronized from values, not from the game

EXISTING_OPS_FILE=SYNCHRONIZE is pinned: the image makes ops.json on the volume match minecraft.management.opPlayers exactly on every start. A player promoted in-game with /op is not in the value, so the next restart removes them again. Promote people in the values file.

Those names are resolved against the external Playerdb API during startup, and a name that does not resolve aborts the start. A typo is therefore not a missing operator but a server that never comes up — and an outage of that third-party lookup delays every pod start.

Two memory numbers that must not drift apart

minecraft.resources.memory is the JVM heap (MEMORY=<n>G). While server.resources.requests.memory is unset, the chart derives the container request from it and the two cannot drift; the limit follows via limitFactor. Setting the request explicitly decouples them — keep the limit above the heap plus the JVM's own overhead. A heap that does not fit the limit is not a Java OutOfMemoryError: the kernel kills the container, the pod shows OOMKilled, and nothing in the server log explains it.

The world lives on one PVC that helm uninstall deletes

/data and /modpacks are two subPaths of a single ReadWriteOnce claim, <release>-mc-volume-claim. The name is a deliberate exception from the naming scheme (style guide): renaming a PVC makes Kubernetes provision a new, empty volume and orphan the existing world.

It is an ordinary PVC in this release, not a StatefulSet volumeClaimTemplate. helm uninstall therefore deletes it, and with a Delete reclaim policy the world goes with it. Back it up before uninstalling — a resource-policy annotation added to the live PVC afterwards does not help, because Helm reads that annotation from the release manifest.

server.properties lives on this volume too, which is why settings the chart does not send as an environment variable persist across upgrades unchanged.

One replica, one world

Every pod of the StatefulSet mounts the same RWO claim — there is no volumeClaimTemplate and no per-pod volume. Further pods stay stuck before start on a node that does not hold the volume, and two Minecraft servers on one world directory would corrupt it in any case. NOTES.txt says so when server.replicas is raised.

Rotating a secret does not restart the pod

The StatefulSet hashes the rendered OnePasswordItem into checksum/secret-ref, so pointing secrets.minecraftServerRef at a different item rolls the pod. Changing the value inside the same item does not: Helm never sees that content, and the keys are consumed as environment variables, which Kubernetes never refreshes in a running container (style guide §9).

The operator's own operator.1password.io/auto-restart: true annotation does not close that gap here and is therefore deliberately not set: verified in the operator source (main, 9c3b1b9, 2026-08-11), restartWorkloadsWithUpdatedSecrets lists nothing but DeploymentList and getPodTemplate returns "unsupported type" for everything else. This chart is a StatefulSet, so the annotation would do nothing while suggesting the opposite.

kubectl -n <namespace> rollout restart statefulset/<release>-minecraft-server-sfs

Budget for that restart: a full server start — ~124 s measured for ftb-skies on Longhorn.

RCON is declared, not published

With server.rcon.enabled: true the container declares the RCON port and the chart wires RCON_PASSWORD. The Service publishes only the Minecraft port, so RCON is reachable from inside the pod's network and through a forward:

kubectl -n <namespace> port-forward <release>-minecraft-server-sfs-0 25575:25575

Exposing it more widely is a decision to make deliberately: it is a remote console with full server rights, protected by that one password.

The metrics port is declared, not served

The container declares port 19565 named metrics. Declaring a containerPort opens nothing: the Service does not publish it, the chart ships no ServiceMonitor, and it sets no variable that would switch the image's metrics on. Treat that entry as documentation, not as a scrape target.

The serverpack init script has one sharp edge

Its migration branch runs only when curseforgeServerpack.workingDirectory already exists; on a first install the script just downloads and the server unpacks. When it does run, it copies banned-ips.json, banned-players.json, ops.json and whitelist.json out of the backup unconditionally under set -e — so an existing serverpack directory missing any one of those four fails the init container and the pod never starts. Look there first when a serverpack upgrade hangs before the server logs anything:

kubectl -n <namespace> logs <release>-minecraft-server-sfs-0 -c download-serverpack

Its closing chown reads the same pod security context helper as the pod spec (they used to be able to disagree in silence) and is explicitly non-fatal: an unprivileged process cannot chown foreign files, and under set -e a failure there would keep the whole pod from starting.

With a ClusterIP Service, the router's namespace must be allowed

In this cluster players reach the server through minecraft-router, which runs in its own namespace. With server.service.type left at ClusterIP the game port is restricted to this namespace, so that namespace has to be in networkPolicy.allowedNamespaces — otherwise the server is unreachable and the symptom is a connection timeout in a game client with a perfectly healthy pod behind it. The same applies to a Prometheus scraping 19565 from elsewhere.

RCON is the rule with the most security value here, and it costs nothing

RCON is full administrative control of the server. It has no Service and no probe, so restricting it to this namespace is free — and it is gated on server.rcon.enabled, exactly as the containerPort is: a rule for a port the pod does not declare is one nobody can verify, and the render test rejects it.

A NetworkPolicy matches CIDRs and selectors and never hostnames, and this cluster's CNI (Canal — Flannel plus Calico) has no FQDN rule, so an egress rule can only ever name a port and not a destination.

The policy is only a boundary where the CNI enforces one

The API server accepts a NetworkPolicy whatever the cluster does with it. Where the CNI does not enforce policies, everything looks configured and nothing is filtered — which is worse than no policy, because it invites the assumption that something is being filtered. Verify by connecting from an unrelated namespace rather than by reading it off the manifest.

Measured for this cluster: the CNI is Canal (Flannel for the network, Calico for policy), 6/6, and it does enforce. Measured for CI: k3s brings its own policy controller and enforces too — but it exempts the kubelet's probe traffic, so a blocked probe path does not turn the install-test red. What CI does catch is anything the application itself needs.

Upgrades and backup

  • Back up the world before uninstalling, and before a line change. The PVC is a chart object, so helm uninstall takes it with the release.
  • A line change is two values in one upgrade (image.tagOverride + minecraft.version). Changing only one fails the render — which is the improvement; before 5.0.0 it installed silently.
  • App major jumps go stepwise and through PRs, per Versioning — not through the change-app-version.yml dispatch workflow, which commits straight to main and bypasses both pipelines.
  • minecraft.resources.storageClassName is immutable on a bound volume, and shrinking storage fails the upgrade on the PVC instead of touching data.

Renovate

The chart's customManagers block tracks appVersion against itzg/minecraft-server with

versioning: regex:^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)-(?<compatibility>java\d+)$

The compatibility group is the point: the tags encode the JRE as a suffix, and without the group Renovate would happily offer a tag from another Java line — a silent runtime switch. image.tagOverride is not tracked by anything. See Renovate.

CI

ci/minecraft-server/ holds three files:

  • test-values.yaml — a real 2Gi world, a 1G heap and a 500m CPU request so a 4-vCPU runner copes. minecraft.version is deliberately not set: leaving it at the chart default means the install-test exercises exactly what a user gets, so a line published with a mismatched default version fails CI instead of passing on a CI-only override. opPlayers stays null because the image resolves every name against Playerdb and aborts the start on a miss. server.service.type stays ClusterIP, which is what the real deployments run and which keeps CI free of node-port allocation.
  • fixtures.yaml — a no-provisioner StorageClass named longhorn plus a pre-bound hostPath PV (the chart's storage class does not exist in k3d), a node-setup Job that pre-creates the directory world-writable (hostPath volumes do not support fsGroup), and the dummy Secret with both optional keys.
  • settings.env — INSTALL_TIMEOUT=600, because the image is large and the server downloads its vanilla jar on first start before the container settles.

The install-test runs on every PR that touches the chart — see CI/CD.

See also