Files
helm-charts/minecraft-server/templates/minecraft-server.sfs.yaml
T
jklein 9c0039685b minecraft-server: an overridable image tag and a Java-line guard (5.0.0+up2026.8.2.java25)
Only the newest Java line is maintained from now on. Publishing the chart once
per line out of one directory is what made the lines drift apart (#158): a
change reached whichever line happened to lie in the directory. java8 and java17
stay published as frozen artifacts at 4.0.0, java21 at 4.1.0, and none of them
gets another chart change.

Running an older JVM is now a decision at rollout time instead of a chart
version: image.tagOverride replaces the tag that otherwise comes from
appVersion. It is not called image.tag because it decouples the running image
from the chart version, and it is deliberately outside Renovate's reach -
renovate.json5 keeps tracking appVersion only.

The pairing of tag and minecraft.version is now checked at render time and fails
with both values named, the line each one belongs to, and where the tag came
from. Until now nothing checked it: a mismatch installed silently and only the
container failed afterwards. The boundaries are Mojang's, swept over all 102
releases of the piston-meta manifest (javaVersion.majorVersion): up to 1.16.5
Java 8, 1.17-1.17.1 Java 16, 1.18-1.20.4 Java 17, 1.20.5-1.21.11 Java 21, 26.1
and later Java 25. The 1.17 line was the one gap the chart's own notes did not
cover.

Two cases pass unchecked on purpose, documented in values.yaml and NOTES.txt: a
tag without a recognizable java<N> part, and a minecraft.version that is not a
plain numeric x[.y[.z]]. A guard that rejects what it does not understand gets
worked around instead of respected.

Major, because an installation that already runs a mismatched pair stops
rendering. It does not run today either - it now fails earlier and visibly.
2026-09-02 13:56:25 +02:00

244 lines
11 KiB
YAML

{{- include "minecraft-server.validateModpackSources" . }}
{{- include "minecraft-server.validateJavaLine" . }}
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
labels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
spec:
replicas: {{ include "minecraft-server.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.server.replicas) }}
selector:
matchLabels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
serviceName: {{ .Release.Name }}-{{ .Chart.Name }}-service
template:
metadata:
labels:
app: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
annotations:
# Rolls the pod whenever the rendered ConfigMap changes. init.sh is
# mounted with subPath (kubelet never refreshes those in a running
# container) and the init container only reads it at pod start, so
# without this a changed download URL or working directory would stay
# inert while the release still reported success.
checksum/config: {{ include (print $.Template.BasePath "/minecraft-server.configmap.yaml") . | sha256sum }}
# Deliberately a SEPARATE annotation, and deliberately NOT a hash of the
# secret's content: the Kubernetes Secret is materialized by the
# 1Password operator, so Helm never sees what is in it. This hash covers
# the rendered OnePasswordItem, i.e. the op:// item path - repointing
# secrets.minecraftServerRef at a different item now takes effect
# instead of silently leaving the old credentials in the running pod.
# A change of the item's CONTENT inside the vault is not covered; see
# the comment on secrets in values.yaml for why that gap cannot be
# closed here and what it means operationally.
checksum/secret-ref: {{ include (print $.Template.BasePath "/minecraft-server.secret.yaml") . | sha256sum }}
spec:
automountServiceAccountToken: false
securityContext:
{{- include "minecraft-server.podSecurityContext" .Values.securityContext | nindent 8 }}
{{- if .Values.curseforgeServerpack.enabled }}
initContainers:
- name: download-serverpack
image: {{ .Values.server.initImage }}
command: [ 'sh', '/init/init.sh' ]
securityContext:
{{- include "minecraft-server.containerSecurityContext" .Values.securityContext | nindent 12 }}
volumeMounts:
- name: "mc-volume"
mountPath: /modpacks
subPath: mc-modpacks/
- name: "mc-volume"
mountPath: /data
subPath: mc-data/
- name: init-script
mountPath: /init/init.sh
subPath: init.sh
- name: tmp
mountPath: /tmp
{{- end }}
containers:
- name: {{ .Release.Name }}-{{ .Chart.Name }}-sfs
{{- /* Tag from .Chart.AppVersion unless image.tagOverride replaces
it; validateJavaLine above rejects a tag whose Java line does
not fit minecraft.version. Template comment on purpose - a
YAML comment here would change the rendered manifest. */}}
image: "itzg/minecraft-server:{{ include "minecraft-server.imageTag" . }}"
imagePullPolicy: IfNotPresent
securityContext:
{{- include "minecraft-server.containerSecurityContext" .Values.securityContext | nindent 12 }}
ports:
- name: mc-port
containerPort: {{ .Values.server.port }}
protocol: TCP
{{- if .Values.server.rcon.enabled }}
- name: minecraft-rcon
containerPort: {{ .Values.server.rcon.port }}
protocol: TCP
{{- end }}
- name: metrics
containerPort: 19565
protocol: TCP
env:
- name: EULA
value: "true"
- name: DUMP_SERVER_PROPERTIES
value: "true"
- name: MOTD
value: "{{ .Values.server.motd }}"
- name: DIFFICULTY
value: {{ .Values.minecraft.difficulty }}
- name: SERVER_PORT
value: "{{ .Values.server.port }}"
# ENABLE_WHITELIST is set in both positions on purpose - it is the
# only variable that can switch the whitelist OFF again. The image
# enables the whitelist when WHITELIST/WHITELIST_FILE is non-empty
# OR ENABLE_WHITELIST is true, and disables it (white-list=false)
# when ENABLE_WHITELIST is false, rewriting an existing
# server.properties on the volume. Because a non-empty WHITELIST
# wins over ENABLE_WHITELIST=false, players must stay unset while
# disabled - hence the guard below rather than emitting both.
- name: ENABLE_WHITELIST
value: "{{ .Values.minecraft.whitelist.enabled }}"
# enforce-whitelist is mapped from ENFORCE_WHITELIST in the image's
# property-definitions.json. The image only ever sets it to true (in
# its enable branch) and never resets it, so without this line a
# server that once had the whitelist on keeps enforce-whitelist=true
# in its persisted server.properties after switching off. It is
# inert while white-list=false, but leaving stale state behind is
# exactly what this chart version fixes, so it follows the switch.
- name: ENFORCE_WHITELIST
value: "{{ .Values.minecraft.whitelist.enabled }}"
{{- if .Values.minecraft.whitelist.enabled }}
- name: EXISTING_WHITELIST_FILE
value: {{ .Values.minecraft.whitelist.overridePolicy }}
{{- with .Values.minecraft.whitelist.players }}
- name: WHITELIST
value: "{{ . }}"
{{- end }}
{{- end }}
{{- with .Values.minecraft.management.opPlayers }}
- name: OPS
value: "{{ . }}"
{{- end }}
- name: EXISTING_OPS_FILE
value: "SYNCHRONIZE"
- name: MAX_PLAYERS
value: "{{ .Values.minecraft.management.maxPlayers }}"
- name: FORCE_GAMEMODE
value: "{{ .Values.minecraft.management.forceGameMode }}"
- name: SNOOPER_ENABLED
value: "false"
- name: MODE
value: "{{ .Values.minecraft.management.gameMode }}"
- name: LEVEL
value: "{{ .Values.minecraft.management.worldSaveName }}"
- name: SERVER_NAME
value: "{{ .Values.minecraft.management.serverName }}"
- name: VERSION
value: "{{ .Values.minecraft.version }}"
- name: ENABLE_RCON
value: "{{ .Values.server.rcon.enabled }}"
{{- if .Values.server.rcon.enabled }}
- name: RCON_PORT
value: "{{ .Values.server.rcon.port }}"
- name: RCON_PASSWORD
valueFrom:
secretKeyRef:
key: rcon-password
name: {{ .Release.Name }}-{{ .Chart.Name }}-secret
{{- end }}
- name: ENABLE_AUTOPAUSE
value: "false"
- name: MEMORY
value: "{{ .Values.minecraft.resources.memory }}G"
{{- if .Values.curseforge.enabled }}
- name: TYPE
value: "AUTO_CURSEFORGE"
- name: CF_API_KEY
valueFrom:
secretKeyRef:
key: curseforge-api-key
name: {{ .Release.Name }}-{{ .Chart.Name }}-secret
- name: CF_SLUG
value: "{{ .Values.curseforge.modpack.slug }}"
- name: CF_FILE_ID
value: "{{ .Values.curseforge.modpack.versionId }}"
- name: CF_EXCLUDE_MODS
value: "{{ .Values.curseforge.modpack.exclude }}"
- name: CF_OVERRIDES_EXCLUSIONS
value: "{{ .Values.curseforge.modpack.excludeOverride }}"
- name: CF_FORCE_INCLUDE_MODS
value: "{{ .Values.curseforge.modpack.include }}"
- name: CF_FORCE_SYNCHRONIZE
value: "{{ .Values.curseforge.forceReload }}"
{{- end }}
{{- if .Values.curseforgeServerpack.enabled }}
- name: TYPE
value: "CURSEFORGE"
- name: CF_SERVER_MOD
value: "/modpacks/serverpack.zip"
- name: CF_BASE_DIR
value: "{{ .Values.curseforgeServerpack.workingDirectory }}"
{{- end }}
{{- if .Values.ftb.enabled }}
- name: TYPE
value: FTBA
- name: FTB_MODPACK_ID
value: "{{ .Values.ftb.modpackId }}"
- name: FTB_MODPACK_VERSION_ID
value: "{{ .Values.ftb.versionId }}"
{{- end }}
{{- range $value := .Values.env }}
{{- if $value.value }}
- name: {{ $value.name }}
value: {{ tpl $value.value $ }}
{{- else if $value.valueFrom }}
{{- if $value.valueFrom.secretKeyRef }}
- name: {{ $value.name }}
valueFrom:
secretKeyRef:
name: {{ tpl $value.valueFrom.secretKeyRef.name $ }}
key: {{ tpl $value.valueFrom.secretKeyRef.key $ }}
{{- else if $value.valueFrom.configMapKeyRef }}
- name: {{ $value.name }}
valueFrom:
configMapKeyRef:
name: {{ tpl $value.valueFrom.configMapKeyRef.name $ }}
key: {{ tpl $value.valueFrom.configMapKeyRef.key $ }}
{{- end }}
{{- end }}
{{- end }}
livenessProbe:
{{- include "minecraft-server.livenessProbe" .Values.livenessProbe | nindent 12 }}
readinessProbe:
{{- include "minecraft-server.readinessProbe" .Values.readinessProbe | nindent 12 }}
startupProbe:
{{- include "minecraft-server.startupProbe" .Values.startupProbe | nindent 12 }}
resources:
{{- include "minecraft-server.serverResources" . | nindent 12 }}
volumeMounts:
- name: "mc-volume"
mountPath: /data
subPath: mc-data/
- name: "mc-volume"
mountPath: /modpacks
subPath: mc-modpacks/
# Writable path required by readOnlyRootFilesystem: the image writes
# its server-icon scratch file and mc-server-runner's console named
# pipe to /tmp. Everything else goes to the PVC at /data.
- name: tmp
mountPath: /tmp
volumes:
- name: "mc-volume"
persistentVolumeClaim:
# Deliberate exception from the naming scheme: renaming the PVC
# would make Kubernetes provision a new, empty volume and orphan
# the existing world (see README, naming exceptions).
claimName: {{ .Release.Name }}-mc-volume-claim
- name: init-script
configMap:
name: {{ .Release.Name }}-{{ .Chart.Name }}-configmap
- name: tmp
emptyDir: {}