Set Up Minecraft Server

tags: CraftLab

0. (Optional) Create dedicated namespace

Run:

kubectl create namespace crafty

From now on, add -n crafty to each kubectl command (or include namespace: crafty in your YAML).

1. Provisioning Persistent Storage

Create directory on the node so Kubernetes can mount it into your Pod

# create the directories (if doesn’t exist)
sudo mkdir -p /mnt/data/crafty-config
sudo mkdir -p /mnt/data/crafty-servers

# make sure it’s writable by root (default is fine) or adjust ownership
sudo chown root:root /mnt/data/crafty-config /mnt/data/crafty-servers

# optionally tighten permissions if you like
sudo chmod 755 /mnt/data/crafty-config /mnt/data/crafty-servers

Once that’s in place, your PersistentVolume will bind to /mnt/data/crafty-config and /mnt/data/crafty-servers.Crafty will have a place to store its config, world data, backups, etc. After creating the folders we need to create PV and PVC YAML to define persistent volumes.

PersistentVolume (hostPath for now):

Crafty needs disk space for its config, database, backups and any Minecraft server files you create. Create the file crafty-pv.yaml

macc@craftlab:~$ vi crafty-pvs.yaml

File editor:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: crafty-config-pv
spec:
  capacity:
    storage: 5Gi   # adjust size to your needs
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  hostPath:
    path: /mnt/data/crafty-config  # adjust to the directories created in the previous step
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: crafty-servers-pv
spec:
  capacity:
    storage: 20Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  hostPath:
    path: /mnt/data/crafty-servers
~
~
~
~
:
PersistentVolumeClaim:

Create the file crafty-pvc.yaml

macc@craftlab:~$ vi crafty-pvcs.yaml

File editor:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: crafty-config-pvc
  namespace: crafty
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
  volumeName: crafty-config-pv
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: crafty-servers-pvc
  namespace: crafty
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 20Gi
  volumeName: crafty-servers-pv
~
~
~
~
:
Apply files

Run the following commands:

kubectl apply -f crafty-pvs.yaml
kubectl apply -f crafty-pvcs.yaml

Output:

persistentvolume/crafty-pv created
persistentvolumeclaim/crafty-pvc created

Verify pvcs where created

kubectl get pvc -n crafty

Output:

NAME                 STATUS   VOLUME              CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
crafty-config-pvc    Bound    crafty-config-pv    5Gi        RWO                           <unset>                 47m
crafty-servers-pvc   Bound    crafty-servers-pv   20Gi       RWO                           <unset>                 47m

2. Deploy Crafty Controller

This will run the Crafty web panel inside a Pod, mounting both pvs for config and world data (defining a Deployment manifest).

Create the file crafty-deployment.yaml

macc@craftlab:~$ vi crafty-deployment.yaml

File editor:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: crafty
  namespace: crafty
spec:
  replicas: 1
  selector:
    matchLabels:
      app: crafty
  template:
    metadata:
      labels:
        app: crafty
    spec:
      securityContext:  # Ensuring we run the pod as admin
        runAsUser: 0
        runAsGroup: 0
        fsGroup:    0
      containers:
      - name: crafty
        image: registry.gitlab.com/crafty-controller/crafty-4:latest
        ports:
        - name: http
          containerPort: 8000
        - name: https
          containerPort: 8443
        - name: dynmap
          containerPort: 8123
        - name: bedrock
          protocol: UDP
          containerPort: 19132
        volumeMounts:
        - name: crafty-config  # For config data
          mountPath: /crafty/app/config
        - name: crafty-servers # For servers and world data
          mountPath: /crafty/servers
      volumes:
      - name: crafty-config
        persistentVolumeClaim:
          claimName: crafty-config-pvc
      - name: crafty-servers
        persistentVolumeClaim:
          claimName: crafty-servers-pvc

~
~
~
~
:

Apply manifest, run:

kubectl apply -f crafty-deployment.yaml

3. Expose the Crafty-Controller UI by creating a Kubernetes Service

Create the file crafty-controller.yaml

macc@craftlab:~$ vi crafty-ui-service.yaml

File editor:

apiVersion: v1
kind: Service
metadata:
  name: crafty-ui
  namespace: crafty
spec:
  type: NodePort
  selector:
    app: crafty
  ports:
  - port: 8000
    targetPort: 8000
    nodePort: 32000
  - port: 8443
    targetPort: 8443
    name: https
    protocol: TCP
  - port: 19132
    targetPort: 19132
    name: bedrock
    protocol: UDP
~
~
~
~
:

Apply manifest, run:

kubectl apply -f crafty-ui-service.yaml

4. Verify & test

Verify pod is running:

kubectl get pods -n crafty --watch

Output:

NAME                      READY   STATUS    RESTARTS   AGE
crafty-76f768fc68-dw4k6   1/1     Running   0          119s

See config files are present inside

POD=$(kubectl get pods -n crafty -l app=crafty -o jsonpath='{.items[0].metadata.name}')
kubectl exec -n crafty $POD -- ls -R /crafty/app/config

List /crafty/servers directory (empty at first)

kubectl exec -n crafty $POD -- ls -R /crafty/servers

Verify pod:

# kubectl logs <new-crafty-pod> -n crafty
kubectl logs crafty-76f768fc68-dw4k6 -n crafty

Output:

Wrapper | 🏗️  Config not found, pulling defaults...
Wrapper | 📋 Looking for problem bind mount permissions globally...
Wrapper | 📋 (1/3) Ensuring root group ownership...
Wrapper | 📋 (2/3) Ensuring group read-write is present on files...
Wrapper | 📋 (3/3) Ensuring sticky bit is present on directories...
Wrapper | ✅ Initialization complete!
find: './import': No such file or directory
Wrapper | 🚀 Launching crafty with [-d -i]
Logging set to: 0

    ///////////////////////////////////////////////////////////////////////////
    #                  Welcome to Crafty Controller - v.4.4.7                 #
    ///////////////////////////////////////////////////////////////////////////
    #          Server Manager / Web Portal for your Minecraft server          #
    #                     Homepage: www.craftycontrol.com                     #
    ///////////////////////////////////////////////////////////////////////////

[+] Crafty: 05/14/25 23:18:46 - INFO:   Starting migrations
[+] Crafty: 05/14/25 23:18:46 - INFO:   Starting Backups migrations
[+] Crafty: 05/14/25 23:18:46 - INFO:   Migrations: Adding columns [backup_id, backup_name, backup_location, enabled, default, action_id, backup_status]
[+] Crafty: 05/14/25 23:18:46 - INFO:   Cleaning up orphan backups for all servers
[+] Crafty: 05/14/25 23:18:46 - INFO:   Cleaning up orphan schedules for all servers
[+] Crafty: 05/14/25 23:18:46 - WARNING:        We have detected a fresh install. Please be sure to forward Crafty's port, 8443, through your router/firewall if you would like to be able to access Crafty remotely.
[+] Crafty: 05/14/25 23:18:46 - INFO:   Fresh Install Detected - Creating Default Settings
[+] Crafty: 05/14/25 23:18:46 - CRITICAL:       Default password too short using Crafty's created default. Find it in app/config/default-creds.txt
[+] Crafty: 05/14/25 23:18:47 - INFO:   Checking for reset secret flag
[+] Crafty: 05/14/25 23:18:47 - INFO:   No flag found. Secrets are staying
[+] Crafty: 05/14/25 23:18:47 - INFO:   Checking for remote changes to config.json
[+] Crafty: 05/14/25 23:18:47 - INFO:   Remote change complete.
[+] Crafty: 05/14/25 23:18:47 - INFO:   Initializing all servers defined
[+] Crafty: 05/14/25 23:18:47 - INFO:   Crafty started in daemon mode, no shell will be printed

[+] Crafty: 05/14/25 23:18:47 - INFO:   Generating a self signed SSL
[+] Crafty: 05/14/25 23:18:47 - INFO:   Generating a key pair. This might take a moment.
[+] Crafty: 05/14/25 23:18:47 - INFO:   Setting up Crafty's internal components...
[+] Crafty: 05/14/25 23:18:47 - INFO:   https://10.244.0.5:8443 is up and ready for connections.
[+] Crafty: 05/14/25 23:18:47 - INFO:   Server Init Complete: Listening For Connections!
[+] Crafty: 05/14/25 23:18:49 - INFO:   Stats collection frequency set to 30 seconds
[+] Crafty: 05/14/25 23:18:49 - INFO:   Launching Scheduler Thread...
[+] Crafty: 05/14/25 23:18:49 - INFO:   Launching command thread...
[+] Crafty: 05/14/25 23:18:49 - INFO:   Launching log watcher...
[+] Crafty: 05/14/25 23:18:49 - INFO:   Launching realtime thread...

[+] Crafty: 05/14/25 23:18:51 - INFO:   Checking Internet. This may take a minute.
[+] Crafty: 05/14/25 23:18:53 - INFO:   Execution Mode: Non-interactive (e.g. 'python main.py')
[+] Crafty: 05/14/25 23:18:53 - INFO:   Application path: '/crafty'
[+] Crafty: 05/14/25 23:18:54 - INFO:   Crafty has fully started and is now ready for use!

Check Endpoints & UI: Once the Pod is Running, your crafty-ui Service will pick up its endpoints:

kubectl describe svc crafty-ui -n crafty

Output:

Name:                     crafty-ui
Namespace:                crafty
Labels:                   <none>
Annotations:              <none>
Selector:                 app=crafty
Type:                     NodePort
IP Family Policy:         SingleStack
IP Families:              IPv4
IP:                       10.103.200.86
IPs:                      10.103.200.86
Port:                     http-ui  8000/TCP
TargetPort:               8000/TCP
NodePort:                 http-ui  32000/TCP
Endpoints:                10.244.0.5:8000
Port:                     https-ui  8443/TCP
TargetPort:               8443/TCP
NodePort:                 https-ui  32001/TCP
Endpoints:                10.244.0.5:8443
Port:                     bedrock  19132/UDP
TargetPort:               19132/UDP
NodePort:                 bedrock  32002/UDP
Endpoints:                10.244.0.5:19132
Session Affinity:         None
External Traffic Policy:  Cluster
Events:                   <none>

Now you should be able to browse to http://<NODE_IP>:32000

5. Access Crafty Controller UI

Open a browser and go to https://192.168.101.186:32001

This page will open up, sometimes you need to bypass google's security warning.
Pasted image 20250514185544.png|600

6. Get default username and password for crafty-controller

Cat credentials from pod

Find your Crafty Pod name:

kubectl get pods -n crafty -l app=crafty

Output:

NAME                      READY   STATUS    RESTARTS   AGE
crafty-76f768fc68-dw4k6   1/1     Running   0          96m

Exec in and cat the creds file:

# kubectl exec -n crafty <crafty-pod-name> -- \ cat /crafty/app/config/default-creds.txt
kubectl exec -n crafty crafty-76f768fc68-dw4k6 -- \cat /crafty/app/config/default-creds.txt

Output:

{
    "username": "admin",
    "password": "8gM*Am553zmcYFG6wQCBR7&3VTWa8vRu8sgK7CCzywTsrp4H9KmfwnAhI8#lA0mG",
    "info": "This is NOT where you change your password. This file is only a means to give you a default password."
}

Use those to log in, then immediately change the password in the UI.

Tip: if you had mounted /crafty directly to a hostPath like /mnt/data/crafty, you can also read the file on the host at /mnt/data/crafty/app/config/default-creds.txt

Change username and password

Use default credentials to log in, then go to configuration
Screenshot 2025-05-14 at 7.06.16 PM.png|600

Click on "New Password"
Pasted image 20250514190820.png|600

7. Create a Kubernetes Minecraft Server Service

You need a Kubernetes Service for your Java server port:

Create a minecraft-service.yaml

vi minecraft-service.yaml

File editor:

# minecraft-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: minecraft-server
  namespace: crafty
spec:
  type: NodePort
  selector:
    app: crafty
  ports:
    - name: java-edition
      protocol: TCP
      port: 25565
      targetPort: 25565
      nodePort: 30065   # or any free 30000–32767 port

Apply file:

kubectl apply -f minecraft-service.yaml

Verify the Service & Endpoints

kubectl get svc minecraft-server -n crafty

Output:

NAME               TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)           AGE
minecraft-server   NodePort   10.106.122.60   <none>        25565:30065/TCP   49m

Verify Endpoints field is populated

kubectl describe svc minecraft-server -n crafty

Output:

Name:                     minecraft-server
Namespace:                crafty
Labels:                   <none>
Annotations:              <none>
Selector:                 app=crafty
Type:                     NodePort
IP Family Policy:         SingleStack
IP Families:              IPv4
IP:                       10.106.122.60
IPs:                      10.106.122.60
Port:                     java-edition  25565/TCP
TargetPort:               25565/TCP
NodePort:                 java-edition  30065/TCP
Endpoints:                10.244.0.5:25565
Session Affinity:         None
External Traffic Policy:  Cluster
Events:                   <none>

8. Create our first Minecraft server

Click on "Create New Server"
Pasted image 20250514191148.png|600

Follow these steps:

  1. Select server type
    • Minecraft Server, Minecraft Proxy
  2. Select server
    • Vanilla, forge, paper, etc...
  3. Select a Server Version
    • 1.18.2, ....
  4. Set a server name
    • Test MC
  5. Select Maximum Memory for this server and set port if necessary
    • I put 4 GB for testing
  6. Click "Build Server"

After you click "Build Server" you will see your new server listed in the dashboard
Pasted image 20250514192021.png|600

Click on the server name and start the server for the very first time
Pasted image 20250514192204.png|600

9. Open Minecraft and connect to the server

Open minecraft
Pasted image 20250514204010.png|600
Server Name: Test MC
Server Address: 192.168.101.186:30065

  1. Go to playit.gg
  2. Log in/Create account
  3. Go to "Downloads" and select "Plugins"
  4. Download "java bukkit plugin"
    Pasted image 20250514215356.png|600
  5. Go to Crafty Controller, Click on the server name, stop the server and click on "Files"
    Pasted image 20250515115212.png|600
  6. Right-click on the "plugins" folder and click on Upload, then select the playit.gg java plugin.
  7. Go to "Terminal" and start the server
  8. View the logs and open the link under a message from [gg.playit.minecraft.PlayitTcpTunnel]
  9. On the link, go trough the steps to claim the tunnel. It can take some minutes, at the end of this process the tunnel link to the server will be displayed
    name-surrounded.joinmc.link
    
  10. Start Minecraft, go to Multiplayer and Add Server
    • Server Name: You can put whatever the name you want to the server
    • Server Address: use the link provided by playit.gg: name-surrounded.joinmc.link
  11. Now You and all of your friends can play in your server using this link!

11. Install Forge Mods on your Minecraft Server

To add Forge mods to your Minecraft server running through Crafty Controller, you'll need to follow these steps:

1. Install Forge on Your Server

Before you can add mods, your server needs to be set up with Forge, which is a modding platform for Minecraft.

2. Add Mods to Your Server

Once Forge is installed, you can begin adding mods:

3. Configure Crafty Controller to Use Forge

Now that you have Forge installed and mods in place, you'll need to ensure that Crafty Controller is configured to use the Forge server:

4. Restart Your Server

After configuring the Crafty Controller, restart your server through the Crafty UI. Your server should now be running with Forge and the mods you've added.

5. Client-Side Mods (for Players)

Players joining your server will need to install the same mods on their own Minecraft client. They can do this by installing the Forge client version and placing the same mods into their mods folder.

1. Grab your Service's internal address
kubectl get svc minecraft-server -n crafty \
  -o jsonpath='{.spec.clusterIP}'

Suppose it prints:

10.96.72.34

On the Playit.gg dashboard under the Agents tab click on "setup a docker based agent", copy your SECRET_KEY (the long alphanumeric value).

example docker-compose.yml

version: '3'

services:
  playit:
    image: ghcr.io/playit-cloud/playit-agent:0.15
    network_mode: host
    environment:
    - SECRET_KEY=165dd89d7d43aca18a78bafaeec1528b567676e70ccf04169d76db928d979c8a
3. Create a Deployment manifest

Make a file called playit-agent.yaml with this content. Replace the placeholders exactly as shown:

vi playit-agent.yaml

File editor:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: playit-agent
  namespace: crafty
spec:
  replicas: 1
  selector:
    matchLabels:
      app: playit-agent
  template:
    metadata:
      labels:
        app: playit-agent
    spec:
      containers:
      - name: playit-agent
        image: ghcr.io/playit-cloud/playit-agent:0.15
        env:
        - name: SECRET_KEY
          value: "3f18720969048db5f5bcdbcd34f5ee5b0073159f7accd7dbc504a13a9a2923ee"
        - name: LOCAL_ADDR
          value: "10.106.122.60"    # <-- your ClusterIP from step 1
        - name: LOCAL_PORT
          value: "25565"          # <-- Minecraft’s listen port
~
~
~
~
:
4. Apply the deployment file
kubectl apply -f playit-agent.yaml

Watch it come up

kubectl get pods -n crafty -l app=playit-agent

Output:

NAME                           READY   STATUS    RESTARTS   AGE
playit-agent-6667d94bf-8kvzj   1/1     Running   0          37s

Verify the logs

kubectl logs -f deployment/playit-agent -n crafty

Output:

2025-05-19T02:16:49.657903Z  INFO playit_cli::ui: playit (v0.15.26): 1747621009657 tunnel running, 1 tunnels registered

Now the playit.gg pod is running and the tunnel is setup properly. You can share the address provided by playit.gg with your friends and start playing at this point.

To restart the playit.gg service:

kubectl rollout restart deployment/playit-agent -n crafty

14. (Optional) Configure backups using chronyd

Create a CronJob manifest file

vi crafty-backup-cronjob.yam

CronJob manifest

apiVersion: batch/v1
kind: CronJob
metadata:
  name: crafty-backup
  namespace: crafty
spec:
  schedule: "0 3 * * *"         # daily at 03:00
  concurrencyPolicy: Forbid
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
          - name: backup
            image: busybox
            command:
            - sh
            - -c
            args:
            - |
              TIMESTAMP=$(date +"%Y-%m-%d_%H%M")
              echo "Backing up world data…"
              tar czf /backups/world-${TIMESTAMP}.tgz -C /crafty/data .
              echo "Backing up Crafty config…"
              tar czf /backups/config-${TIMESTAMP}.tgz -C /crafty/app/config .
            volumeMounts:             # ← here, under the container
            - name: crafty-data
              mountPath: /crafty/data
            - name: crafty-config
              mountPath: /crafty/app/config
            - name: backup-dest
              mountPath: /backups
          volumes:
          - name: crafty-data
            persistentVolumeClaim:
              claimName: crafty-pvc
          - name: crafty-config
            hostPath:
              path: /mnt/data/crafty/config
              type: Directory
          - name: backup-dest
            hostPath:
              path: /mnt/data/crafty-backups
              type: DirectoryOrCreate

Apply it with your normal user (no sudo):

kubectl apply -f crafty-backup-cronjob.yaml
kubectl get cronjob -n crafty

(Optional) Trigger a test run:

kubectl create job --from=cronjob/crafty-backup crafty-backup-test -n crafty
kubectl get jobs -n crafty

Check your host /mnt/data/crafty-backups for the newly created .tgz files.

macc@craftlab:/mnt/data/crafty-backups$ ls
config-2025-05-26_0519.tgz  world-2025-05-26_0519.tgz

With this fix, volume mounts are legal in the schema and your backups directory will fill with timestamped archives without ever being overwritten.

15. (Optional) In case of forgetting admin password

Grab the default admin creds

Crafty generates a random password on first-run and writes it into default-creds.txt. To fetch them:

# get your Crafty pod name
POD=$(kubectl get pods -n crafty -l app=crafty -o jsonpath='{.items[0].metadata.name}')

# print the initial admin/user and password
kubectl exec -n crafty $POD -- cat /crafty/app/config/default-creds.txt

You will see something like:

{
    "username": "admin",
    "password": "#NElFL##gTk5$Fg169IUjq%7vqAYsf0vc2tpo%wACtJIo22*&&c%skPYQTs^*b1y",
    "info": "This is NOT where you change your password. This file is only a means to give you a default password."

Now you will be able to log in to the Crafty UI using these credentials.

16. (Optional) Checking the server files in the terminal

First use the following command to get the name of the POD

POD=$(kubectl get pods -n crafty -l app=crafty -o jsonpath='{.items[0].metadata.name}')

Run the following command to get your server UUID (You can also see this in the Crafty UI)

kubectl exec -n crafty $POD -- ls /crafty/servers

You will get something like:

b6fd9057-71e1-4d1e-ae9e-247162b781c1

Then assign a variable to store this ID (optional)

SERVER_UUID=$(kubectl exec -n crafty $POD -- ls /crafty/servers)

To list the server files you just need to do the following:

kubectl exec -n crafty $POD -- ls -ltR /crafty/servers/$SERVER_UUID | head -n 20

Output:

/crafty/servers/b6fd9057-71e1-4d1e-ae9e-247162b781c1:
total 1048
drwxr-sr-x  2 crafty root   4096 May 26 18:52 db_stats
drwxr-sr-x 10 crafty root   4096 May 26 18:48 world
-rw-r--r--  1 crafty root      2 May 26 18:48 ops.json
-rw-r--r--  1 crafty root      2 May 26 18:48 whitelist.json
-rw-r--r--  1 crafty root      2 May 26 18:48 banned-ips.json
-rw-r--r--  1 crafty root      2 May 26 18:48 banned-players.json
-rw-r--r--  1 crafty root      2 May 26 18:48 usercache.json
-rw-r--r--  1 crafty root   1143 May 26 18:48 server.properties
drwxr-sr-x  2 crafty root   4096 May 26 18:48 config
drwxr-sr-x  2 crafty root   4096 May 26 18:48 defaultconfigs
drwxr-sr-x  2 crafty root   4096 May 26 18:48 mods
drwxr-sr-x  2 crafty root   4096 May 26 18:48 logs
-rw-r--r--  1 crafty root      9 May 26 18:48 eula.txt
-rw-r--r--  1 crafty root 997937 May 26 18:43 forge-installer-1.18.2.jar.log
drwxr-sr-x 11 crafty root   4096 May 26 18:43 libraries
-rw-r--r--  1 crafty root    363 May 26 18:43 run.bat
-rwxr--r--  1 crafty root    366 May 26 18:43 run.sh
-rw-r--r--  1 crafty root    339 May 26 18:43 user_jvm_args.txt

17. (Optional) Testing persistence (ensuring no overwrite on restart)

Verifying that that your world and server data truly survive any Pod restart:

1. Note your server’s UUID and world contents

Get the Pod name and your server’s UUID directory:

POD=$(kubectl get pods -n crafty -l app=crafty -o jsonpath='{.items[0].metadata.name}')
SERVER_UUID=$(kubectl exec -n crafty $POD -- ls /crafty/servers)

List the server files inside the pod:

kubectl exec -n crafty $POD -- ls -ltR /crafty/servers/$SERVER_UUID/ | head -n 20
2. Delete the Crafty Pod

Since the server is managed by a Deployment with replicas: 1, deleting the Pod will immediately cause the Deployment controller to spin up a brand-new Pod in its place.

Delete the current Crafty Pod

kubectl delete pod -n crafty $POD
kubectl get pods -n crafty --watch

Wait until the new crafty-xxxxx Pod is Running.

Output:

NAME                            READY   STATUS      RESTARTS       AGE
crafty-664d488db-6cz4z          1/1     Running     0              31m
crafty-backup-test-9rx9n        0/1     Completed   0              14h
playit-agent-6456d7b889-lvfxc   1/1     Running     1 (2d4h ago)   7d14h
3. Re-verify inside the new Pod

List the same server files again in the new Pod

NEW_POD=$(kubectl get pods -n crafty -l app=crafty -o jsonpath='{.items[0].metadata.name}')
kubectl exec -n crafty $NEW_POD -- ls -ltR /crafty/servers/$SERVER_UUID/world | head -n 20

Compare that output to what you saw before deleting the Pod. They should be identical (same total number of files).

4. Check on the host

On your Ubuntu host, you can also inspect the PV’s data directly:

ls -R /mnt/data/crafty-servers/$SERVER_UUID/ | head -n 20

Again, it should match exactly.

Output:

/mnt/data/crafty-servers/b6fd9057-71e1-4d1e-ae9e-247162b781c1:
banned-ips.json
banned-players.json
config
crafty_managed.txt
db_stats
defaultconfigs
eula.txt
forge-installer-1.18.2.jar.log
journeymap
libraries
logs
modernfix
mods
ops.json
patchouli_books
run.bat
run.sh
server.properties
structurize
Conclusion

Once you see that the world files persist through a Pod deletion, you can be confident no future restarts—whether manual, automatic, or cluster upgrades—will erase your server or world.