A persistent Minecraft Java Edition server running on Kubernetes with a Paper server, persistent world storage, whitelist enforcement, and public access through a Playit tunnel.
No inbound router port forwarding is required.
- Minecraft Java Edition server
- Paper server implementation
- Kubernetes
StatefulSet - Persistent world storage using a
PersistentVolumeClaim - Internal Kubernetes Services
- Public access through Playit
- No router port forwarding
- Mojang online authentication
- Whitelist enforcement
- RCON disabled
- CPU and memory resource controls
- Startup and readiness probes
- Automatic Pod recovery
- Separate Kubernetes Secret for the Playit credential
Minecraft Clients
│
│ Public Minecraft connection
▼
Playit Public Endpoint
│
│ Outbound tunnel
▼
Playit Agent Deployment
│
│ TCP 25565
▼
Minecraft ClusterIP Service
│
▼
Paper StatefulSet
│
▼
PersistentVolumeClaim
│
▼
Worlds, configuration, player data, and plugins
.
├── 00-namespace.yaml
├── 01-minecraft-pvc.yaml
├── 02-minecraft-services.yaml
├── 03-minecraft-statefulset.yaml
├── 04-playit-secret.yaml.example
├── 05-playit-deployment.yaml
├── .gitignore
└── README.md
- A working Kubernetes cluster
kubectlconfigured for the cluster- A compatible
local-pathStorageClass - At least 4 CPU cores available to the Kubernetes node
- At least 8 GiB of memory available to the Kubernetes node
- Minecraft Java Edition
- A Playit account
- A Playit agent secret
The default resource configuration is intended for a small private server with approximately 4–8 active players.
Minecraft version: 26.2
Server implementation: Paper
Java version: Java 25
Minecraft JVM heap: 3 GiB
Container memory limit: 4 GiB
CPU request: 1 core
Persistent storage: 20 GiB
Maximum players: 10
View distance: 8
Simulation distance: 6
Game mode: Survival
Difficulty: Normal
Adjust these values according to your cluster capacity and expected player count.
git clone git@github.com:calico88x/minecraft-kubernetes.git
cd minecraft-kubernetesOpen:
vim 03-minecraft-statefulset.yamlFind:
- name: WHITELIST
value: ""Add the permitted Minecraft Java Edition usernames as a comma-separated list:
- name: WHITELIST
value: "PlayerOne,PlayerTwo,PlayerThree"Whitelist enforcement is enabled by default:
- name: ENABLE_WHITELIST
value: "TRUE"
- name: ENFORCE_WHITELIST
value: "TRUE"The empty default whitelist intentionally causes the server to fail closed. Add at least one username before attempting to join.
Copy the example file:
cp 04-playit-secret.yaml.example 04-playit-secret.yamlRestrict its permissions:
chmod 600 04-playit-secret.yamlEdit it:
vim 04-playit-secret.yamlReplace the placeholder:
apiVersion: v1
kind: Secret
metadata:
name: playit-secret
namespace: minecraft
type: Opaque
stringData:
SECRET_KEY: "YOUR_PLAYIT_SECRET_HERE"The real 04-playit-secret.yaml file is excluded by .gitignore and must never be committed.
Apply the manifests in order:
kubectl apply -f 00-namespace.yaml
kubectl apply -f 01-minecraft-pvc.yaml
kubectl apply -f 02-minecraft-services.yaml
kubectl apply -f 03-minecraft-statefulset.yaml
kubectl apply -f 04-playit-secret.yaml
kubectl apply -f 05-playit-deployment.yamlCheck the Pods, Services, StatefulSet, Deployment, and PVC:
kubectl get statefulset,deployment,pods,services,pvc \
--namespace minecraftExpected Pod state:
NAME READY STATUS RESTARTS
minecraft-0 1/1 Running 0
playit-xxxxxxxxxx-xxxxx 1/1 Running 0
The PVC should show:
STATUS CAPACITY ACCESS MODES
Bound 20Gi RWO
Read the Minecraft logs:
kubectl logs minecraft-0 \
--namespace minecraft \
--tail=100A successful startup includes:
Starting Minecraft server on *:25565
Done (...)! For help, type "help"
Follow the logs continuously:
kubectl logs minecraft-0 \
--namespace minecraft \
--followRun a temporary test Pod:
kubectl run minecraft-test \
--namespace minecraft \
--rm \
--stdin \
--tty \
--restart=Never \
--image=nicolaka/netshoot \
-- nc -vz minecraft.minecraft.svc.cluster.local 25565Expected result:
Connection to minecraft.minecraft.svc.cluster.local 25565 port [tcp/*] succeeded!
Check the Playit logs:
kubectl logs deployment/playit \
--namespace minecraft \
--tail=50A connected agent should report:
playit connected
An IPv6 message such as this may appear when the Kubernetes environment does not provide outbound IPv6 connectivity:
Network unreachable
This is not necessarily fatal if the agent connects successfully over IPv4.
In the Playit dashboard:
- Confirm that the Kubernetes Playit agent is online.
- Create a new tunnel.
- Select Minecraft Java.
- Set the local port to:
25565
- Set the local destination to the Kubernetes
minecraftService.
Find the current Service IP:
kubectl get service minecraft \
--namespace minecraftExample:
NAME TYPE CLUSTER-IP PORT(S)
minecraft ClusterIP 10.43.224.98 25565/TCP
Use that ClusterIP as the Playit local IP and use port 25565.
Do not use:
127.0.0.1
The Playit agent and Minecraft server run in separate Pods, so 127.0.0.1 would point back to the Playit container itself.
Playit will provide a public hostname after the tunnel is created.
In Minecraft Java Edition:
- Open Multiplayer.
- Select Add Server.
- Enter any server name.
- Enter the public hostname provided by Playit.
- Join the server.
Do not rely on Scanning for games on your local network. LAN discovery does not cross the Kubernetes and Rancher Desktop virtual networks.
The Minecraft client version must be compatible with the server version configured in:
- name: VERSION
value: "26.2"kubectl get pods \
--namespace minecraftkubectl top pod minecraft-0 \
--namespace minecraftkubectl top nodeskubectl rollout restart statefulset/minecraft \
--namespace minecraftkubectl rollout status statefulset/minecraft \
--namespace minecraftkubectl rollout restart deployment/playit \
--namespace minecraftkubectl logs minecraft-0 \
--namespace minecraft \
--followkubectl logs deployment/playit \
--namespace minecraft \
--followEdit:
vim 03-minecraft-statefulset.yamlUpdate:
- name: WHITELIST
value: "PlayerOne,PlayerTwo,PlayerThree"Apply the updated StatefulSet:
kubectl apply -f 03-minecraft-statefulset.yamlWait for the rollout:
kubectl rollout status statefulset/minecraft \
--namespace minecraftThe manifest uses:
- name: EXISTING_WHITELIST_FILE
value: "SYNCHRONIZE"This keeps the persisted Minecraft whitelist synchronized with the usernames declared in the StatefulSet.
Minecraft data is stored in the minecraft-data PVC and mounted inside the container at:
/data
This includes:
- Overworld data
- Nether data
- End data
- Player data
- Whitelist
- Server properties
- Paper configuration
- Plugins
- Logs
Restarting or replacing the Minecraft Pod does not delete the world.
Do not delete the PVC unless you intentionally want to delete the persistent server data.
Check the PVC:
kubectl get pvc \
--namespace minecraftScale Minecraft to zero:
kubectl scale statefulset minecraft \
--namespace minecraft \
--replicas=0Stop the Playit agent:
kubectl scale deployment playit \
--namespace minecraft \
--replicas=0The Kubernetes resources and Minecraft PVC remain intact.
Start Minecraft again:
kubectl scale statefulset minecraft \
--namespace minecraft \
--replicas=1Start Playit again:
kubectl scale deployment playit \
--namespace minecraft \
--replicas=1The default configuration includes the following controls:
- Mojang online authentication enabled
- Whitelist enabled
- Whitelist enforcement enabled
- RCON disabled
- Playit credential stored in a Kubernetes Secret
- No inbound router port forwarding
- Separate Playit and Minecraft Pods
- Restricted Playit container resources
- Kubernetes Service used for internal routing
- Persistent data separated from the Pod lifecycle
Never commit:
04-playit-secret.yaml- Playit agent credentials
- Personal access tokens
- Private keys
- Real
.envfiles - World backups containing sensitive player information
The default Minecraft container requests:
resources:
requests:
cpu: "1"
memory: "3Gi"
limits:
memory: "4Gi"No CPU limit is configured for Minecraft. This allows the game server to use additional CPU during chunk generation without being throttled by a restrictive Kubernetes CPU quota.
The JVM is configured with:
- name: INIT_MEMORY
value: "1G"
- name: MEMORY
value: "3G"Monitor real usage before increasing the heap:
kubectl top pod minecraft-0 \
--namespace minecraftAvoid assigning the entire Kubernetes node memory capacity to Minecraft. Kubernetes, the container runtime, DNS, networking components, and the node operating system require memory headroom.
The PVC currently uses:
storageClassName: local-pathVerify your cluster provides this StorageClass:
kubectl get storageclassChange storageClassName in 01-minecraft-pvc.yaml when deploying to a cluster that uses a different storage provisioner.
Remove the workloads and Services while retaining the PVC:
kubectl delete -f 05-playit-deployment.yaml
kubectl delete -f 03-minecraft-statefulset.yaml
kubectl delete -f 02-minecraft-services.yamlDeleting the namespace will also delete namespace-scoped resources, including the PVC:
kubectl delete namespace minecraftDo not delete the namespace unless you have backed up the Minecraft world or intentionally want to remove it.
This repository contains Kubernetes deployment manifests and documentation only.
Minecraft, Paper, Playit, Kubernetes, and the referenced container images remain subject to their respective licenses, terms, and usage policies.