| your Linux construction kit
Source
← Back to Plex on ZFS

Game Servers on ZFS — never lose a world again.

Every game server admin has a horror story. The Minecraft world that corrupted during a power outage. The Valheim save that vanished after a bad update. The griefer who joined the Rust server and leveled everything while you were asleep. The Palworld save that just... stopped loading one day.

The problem is always the same: game servers write important data to disk, and nothing protects it. ext4 doesn't snapshot. NTFS doesn't snapshot. Your backup script that runs at midnight doesn't help when the grief happened at 12:01 AM. You're always one crash away from losing everything.

ZFS changes that. Automatic snapshots every 15 minutes. Rollback to any point in time in under 2 seconds. Zero-cost clones for testing. Replication to a backup server. Your worlds become indestructible.

1. The problem

Game server data is uniquely fragile. Save files are written constantly, often by janky serialization code that was never designed for reliability. A crash mid-write means corruption. A bad mod update means your world loads into a void. A griefer with TNT means hours of building gone in seconds. And the "solutions" the game server community uses — manual backups, rsync cron jobs, copying the world folder to Dropbox — are all point-in-time copies that miss everything in between.

What goes wrong without ZFS

Save corruption: Server crashes mid-write. The world file is half-written. Game won't load it. Gone.
Griefing: Someone joins your server and destroys everything. Your last backup was 6 hours ago.
Bad updates: You update the server or a mod. The save format changed. No rollback path.
Disk failure: The SSD dies. Your backups are on the same disk. Everything's gone.
Operator error: You accidentally delete the world folder. rm -rf doesn't ask twice.

Think of ZFS snapshots like Git for your game worlds. Every 15 minutes, the filesystem silently records the exact state of everything. Unlike Git, it's automatic, instant, and takes almost no extra space. Unlike your backup script, it never forgets to run.

2. Quick start — Minecraft in 5 minutes

Let's start with the game everyone knows. You'll have a Minecraft server running on ZFS with auto-snapshots in about five minutes. Both Java Edition and Bedrock — pick your flavor.

Create the ZFS dataset for Minecraft

# Create a dedicated ZFS dataset for Minecraft world data
kdir /srv/minecraft

# Verify it — you'll see compression enabled, mountpoint set
zfs list -o name,mountpoint,compression,compressratio rpool/srv/minecraft
kdir doesn't just mkdir. It creates a ZFS dataset with compression, proper mountpoints, and its own space accounting. Every game gets its own dataset — its own little universe that can be snapshotted, cloned, or replicated independently.

Minecraft Java Edition

# Run Minecraft Java server in Docker
docker run -d --name minecraft-java \
  --restart unless-stopped \
  -p 25565:25565 \
  -e EULA=TRUE \
  -e MEMORY=4G \
  -e TYPE=PAPER \
  -e DIFFICULTY=normal \
  -e MAX_PLAYERS=20 \
  -e MOTD="ZFS-backed server — your builds are immortal" \
  -v /srv/minecraft/java:/data \
  itzg/minecraft-server

# Check the logs — wait for "Done" message
docker logs -f minecraft-java

Minecraft Bedrock Edition

# Run Minecraft Bedrock server in Docker
docker run -d --name minecraft-bedrock \
  --restart unless-stopped \
  -p 19132:19132/udp \
  -e EULA=TRUE \
  -e DIFFICULTY=normal \
  -e SERVER_NAME="ZFS Bedrock" \
  -e MAX_PLAYERS=10 \
  -v /srv/minecraft/bedrock:/data \
  itzg/minecraft-bedrock-server

# Both editions, one ZFS dataset, one snapshot schedule

Auto-snapshots with Sanoid — every 15 minutes

# Add to /etc/sanoid/sanoid.conf
cat >> /etc/sanoid/sanoid.conf <<'EOF'

[rpool/srv/minecraft]
  use_template = gameserver
  recursive = yes

[template_gameserver]
  frequently = 8
  hourly = 48
  daily = 30
  monthly = 3
  yearly = 0
  autosnap = yes
  autoprune = yes
  frequent_period = 15
EOF

# Restart sanoid timer
systemctl restart sanoid.timer

# Verify — snapshots start appearing every 15 minutes
ksnap list /srv/minecraft
Sanoid is the silent guardian of your game worlds. Every 15 minutes it takes a snapshot — a frozen-in-time copy of your entire world. It keeps 8 frequent snapshots (2 hours of 15-minute granularity), 48 hourly, 30 daily, and 3 monthly. Old ones get pruned automatically. You never have to think about it.

Grief happened? Roll it back.

# Someone blew up the spawn? Roll back to before it happened.
docker stop minecraft-java

# See every snapshot with timestamps
ksnap list /srv/minecraft
# NAME                                          USED  REFER  CREATION
# rpool/srv/minecraft@autosnap_2026-03-23_14:00  128K  2.1G   Mon Mar 23 14:00
# rpool/srv/minecraft@autosnap_2026-03-23_14:15  256K  2.1G   Mon Mar 23 14:15
# rpool/srv/minecraft@autosnap_2026-03-23_14:30  384K  2.1G   Mon Mar 23 14:30  <-- grief at 14:35
# rpool/srv/minecraft@autosnap_2026-03-23_14:45  512K  2.1G   Mon Mar 23 14:45

# Roll back to the 14:30 snapshot — before the grief
ksnap rollback /srv/minecraft

# Start the server again — world restored, 2 seconds flat
docker start minecraft-java
Imagine a time machine for your Minecraft world. Someone griefed at 14:35? Jump back to 14:30. At worst you lose 15 minutes of building. Without ZFS, you lose everything since your last backup — which was... when exactly?

3. Valheim

Valheim worlds are precious. Hundreds of hours building a Viking fortress, sailing to every biome, defeating every boss. One corrupted save and it's all gone. Let's make that impossible.

Valheim dedicated server on ZFS

# Create ZFS dataset for Valheim
kdir /srv/valheim

# Run Valheim server — lloesche's image handles updates automatically
docker run -d --name valheim \
  --restart unless-stopped \
  --cap-add=sys_nice \
  -p 2456-2458:2456-2458/udp \
  -e SERVER_NAME="Valhalla ZFS" \
  -e WORLD_NAME="Midgard" \
  -e SERVER_PASS="your-secret-password" \
  -e BACKUPS_MAX_AGE=7 \
  -e UPDATE_CRON="0 5 * * *" \
  -v /srv/valheim/config:/config \
  -v /srv/valheim/data:/opt/valheim \
  lloesche/valheim-server

# Add to Sanoid — same gameserver template
cat >> /etc/sanoid/sanoid.conf <<'EOF'

[rpool/srv/valheim]
  use_template = gameserver
  recursive = yes
EOF

systemctl restart sanoid.timer

Clone your world for testing

# Want to test a mod without risking your main world?
# Clone it — instant, takes zero extra disk space until you change things
kclone /srv/valheim /srv/valheim-test

# Run a second server on the clone with different ports
docker run -d --name valheim-test \
  --restart unless-stopped \
  -p 2459-2461:2456-2458/udp \
  -e SERVER_NAME="Valhalla TEST" \
  -e WORLD_NAME="Midgard" \
  -e SERVER_PASS="test-password" \
  -v /srv/valheim-test/config:/config \
  -v /srv/valheim-test/data:/opt/valheim \
  lloesche/valheim-server

# Test your mods. If it works, apply to production.
# If it doesn't, just destroy the clone — your real world never saw it.
docker rm -f valheim-test
zfs destroy -r rpool/srv/valheim-test
ZFS clones are like parallel universes. You branch off a perfect copy of your Valheim world, do whatever you want to it, and if things go wrong you just delete the branch. Your original world never knew the clone existed. This is how you test mods safely.

4. Palworld

Palworld dedicated servers are notoriously fragile. The save format has changed multiple times across updates, and corruption reports are common. ZFS snapshots before every server restart give you a safety net that no amount of manual backups can match.

Palworld dedicated server

# Create ZFS dataset
kdir /srv/palworld

# Run Palworld dedicated server
docker run -d --name palworld \
  --restart unless-stopped \
  -p 8211:8211/udp \
  -p 27015:27015/udp \
  -e PUID=1000 \
  -e PGID=1000 \
  -e MULTITHREADING=true \
  -e COMMUNITY=false \
  -e SERVER_NAME="Palworld ZFS" \
  -e SERVER_PASSWORD="your-secret" \
  -e MAX_PLAYERS=16 \
  -v /srv/palworld:/palworld \
  thijsvanloef/palworld-server-docker:latest

# Sanoid config
cat >> /etc/sanoid/sanoid.conf <<'EOF'

[rpool/srv/palworld]
  use_template = gameserver
  recursive = yes
EOF

systemctl restart sanoid.timer

Snapshot before every restart

# Create a wrapper script that always snapshots before restarting
cat > /usr/local/bin/palworld-restart <<'SCRIPT'
#!/bin/bash
echo "Snapshotting Palworld save data..."
ksnap /srv/palworld
echo "Stopping server..."
docker restart palworld
echo "Done. Snapshot taken, server restarting."
echo "If anything goes wrong: ksnap rollback /srv/palworld"
SCRIPT
chmod +x /usr/local/bin/palworld-restart

# Now use palworld-restart instead of docker restart palworld
# Every restart has a matching snapshot — you can always go back
palworld-restart
Palworld updates have broken saves more than once. This script is dead simple: snapshot first, restart second. If the update corrupts your save, roll back to the snapshot from 10 seconds ago. It's a 2-line safety net that would have saved thousands of Palworld servers.

5. Rust

Rust has forced wipes. It's part of the game. But with ZFS, wipe day becomes trivial — roll back to a "clean slate" snapshot instead of rebuilding the server from scratch. And between wipes, your players' progress is protected by the same 15-minute snapshots.

Rust dedicated server with wipe-day snapshots

# Create ZFS dataset
kdir /srv/rust

# Run Rust server
docker run -d --name rust-server \
  --restart unless-stopped \
  -p 28015:28015/udp \
  -p 28016:28016 \
  -p 28082:28082 \
  -e RUST_SERVER_NAME="ZFS Rust | Wipe Thursdays" \
  -e RUST_SERVER_SEED=12345 \
  -e RUST_SERVER_MAXPLAYERS=50 \
  -e RUST_SERVER_WORLDSIZE=3000 \
  -e RUST_RCON_PASSWORD="your-rcon-pass" \
  -v /srv/rust:/steamcmd/rust \
  didstopia/rust-server

# After first boot and map generation, create the "day 1" snapshot
ksnap /srv/rust
# Rename it for clarity
zfs rename rpool/srv/rust@$(zfs list -t snapshot -o name -H rpool/srv/rust | tail -1 | cut -d@ -f2) rpool/srv/rust@wipe-day-clean

# On wipe day — one command, fresh server
docker stop rust-server
ksnap rollback /srv/rust
docker start rust-server
# That's it. World reset to day 1. No reinstall. No config fiddling. 2 seconds.
Traditional Rust wipe: stop server, delete save files, pray you didn't delete the config, restart, hope it generates cleanly. ZFS Rust wipe: stop server, rollback, start server. Done. The day-1 snapshot is your permanent reset button.

Monthly wipe automation

# Automate wipe day — first Thursday of every month at 3 AM
cat > /etc/cron.d/rust-wipe <<'EOF'
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
0 3 1-7 * 4 root /usr/local/bin/rust-wipe
EOF

# The wipe script
cat > /usr/local/bin/rust-wipe <<'SCRIPT'
#!/bin/bash
echo "[$(date)] Rust wipe day — rolling back to clean state"
docker stop rust-server
ksnap rollback /srv/rust
docker start rust-server
echo "[$(date)] Wipe complete. Server is live."
SCRIPT
chmod +x /usr/local/bin/rust-wipe

6. Multi-server setup

One box. Five games. Each game gets its own ZFS dataset with its own snapshot schedule, its own disk quota, and its own resource limits. Docker Compose ties it all together. This is the setup for running a proper game server community.

ZFS datasets with quotas

# Each game gets its own dataset — isolated snapshots, isolated space
kdir /srv/minecraft
kdir /srv/valheim
kdir /srv/palworld
kdir /srv/rust
kdir /srv/terraria

# Set quotas so one game can't eat all the disk
zfs set quota=50G rpool/srv/minecraft
zfs set quota=20G rpool/srv/valheim
zfs set quota=30G rpool/srv/palworld
zfs set quota=40G rpool/srv/rust
zfs set quota=10G rpool/srv/terraria

# Verify
kdf

Docker Compose — all servers in one file

# /srv/docker-compose.yml
services:
  minecraft:
    image: itzg/minecraft-server
    restart: unless-stopped
    ports:
      - "25565:25565"
    environment:
      EULA: "TRUE"
      MEMORY: "4G"
      TYPE: "PAPER"
    volumes:
      - /srv/minecraft/java:/data
    deploy:
      resources:
        limits:
          cpus: "2.0"
          memory: 6G

  valheim:
    image: lloesche/valheim-server
    restart: unless-stopped
    ports:
      - "2456-2458:2456-2458/udp"
    environment:
      SERVER_NAME: "Valhalla"
      WORLD_NAME: "Midgard"
      SERVER_PASS: "your-secret"
    volumes:
      - /srv/valheim/config:/config
      - /srv/valheim/data:/opt/valheim
    deploy:
      resources:
        limits:
          cpus: "2.0"
          memory: 4G

  palworld:
    image: thijsvanloef/palworld-server-docker:latest
    restart: unless-stopped
    ports:
      - "8211:8211/udp"
    environment:
      SERVER_NAME: "Palworld ZFS"
      MAX_PLAYERS: "16"
    volumes:
      - /srv/palworld:/palworld
    deploy:
      resources:
        limits:
          cpus: "2.0"
          memory: 8G

  rust:
    image: didstopia/rust-server
    restart: unless-stopped
    ports:
      - "28015:28015/udp"
      - "28016:28016"
    environment:
      RUST_SERVER_NAME: "ZFS Rust"
      RUST_SERVER_MAXPLAYERS: "50"
    volumes:
      - /srv/rust:/steamcmd/rust
    deploy:
      resources:
        limits:
          cpus: "2.0"
          memory: 8G

  terraria:
    image: ryshe/terraria:latest
    restart: unless-stopped
    ports:
      - "7777:7777"
    volumes:
      - /srv/terraria:/root/.local/share/Terraria/Worlds
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 2G
    stdin_open: true
    tty: true
# Start everything
cd /srv && docker compose up -d

# Check status
docker compose ps
Five games, five datasets, five independent snapshot timelines, five separate disk quotas, five Docker containers with CPU and memory limits. If Rust eats 100% CPU, Minecraft doesn't lag. If Palworld fills its quota, Valheim still has space. Isolation is the whole game.

7. Anti-grief toolkit

This is the part that makes ZFS magic for game servers. You don't just have backups — you have a time machine. Every 15 minutes, automatically, forever. Here's how to use it when bad things happen.

See every snapshot

# List all snapshots for your Minecraft world
ksnap list /srv/minecraft

# Output — every 15 minutes, like clockwork
# NAME                                                    USED  REFER  CREATION
# rpool/srv/minecraft@autosnap_2026-03-23_12:00_frequent   64K  2.1G   Mon Mar 23 12:00
# rpool/srv/minecraft@autosnap_2026-03-23_12:15_frequent  128K  2.1G   Mon Mar 23 12:15
# rpool/srv/minecraft@autosnap_2026-03-23_12:30_frequent  192K  2.1G   Mon Mar 23 12:30
# rpool/srv/minecraft@autosnap_2026-03-23_12:45_frequent  256K  2.1G   Mon Mar 23 12:45
# rpool/srv/minecraft@autosnap_2026-03-23_13:00_hourly    384K  2.1G   Mon Mar 23 13:00
# ...

# Notice the USED column — snapshots are tiny.
# They only store what changed since the last snapshot.
# A 2 GB Minecraft world generates maybe 1-5 MB of snapshot data per hour.

Roll back to any point in time

# Grief discovered at 3:20 PM. Griefer joined at ~3:05 PM.
# Roll back to the 3:00 PM snapshot — 20 minutes of grief, undone in 2 seconds.

docker stop minecraft-java
ksnap rollback /srv/minecraft
# Interactive: pick the 3:00 PM snapshot from the list
docker start minecraft-java

# Or target a specific snapshot directly
docker stop minecraft-java
zfs rollback rpool/srv/minecraft@autosnap_2026-03-23_15:00_frequent
docker start minecraft-java

Clone for forensics

# Don't want to rollback yet? Clone the pre-grief state and investigate.
kclone /srv/minecraft /srv/minecraft-forensics

# Now you have two copies:
# /srv/minecraft           — current state (with grief)
# /srv/minecraft-forensics — clean state (before grief)

# Run a second server instance on the clone to compare
docker run -d --name mc-forensics \
  -p 25566:25565 \
  -e EULA=TRUE \
  -v /srv/minecraft-forensics/java:/data \
  itzg/minecraft-server

# Join both servers. Compare. See exactly what was destroyed.
# Document it. Ban the griefer. Then rollback the real server.
In crime shows they clone the hard drive before investigating. That's exactly what kclone does. You get a perfect copy of the world at any point in time, and you can run it as a live server for inspection — without touching the original. Zero extra disk space until you start changing things.

8. WireGuard for players

Public game servers get DDoS'd, port-scanned, and griefed by strangers. A private game server behind WireGuard is invisible to the internet. No port forwarding. No public IP exposure. No DDoS. Give each player a WireGuard config and they connect directly to your server over an encrypted tunnel.

Set up WireGuard for your game server

# Generate server keys
wg genkey | tee /etc/wireguard/server.key | wg pubkey > /etc/wireguard/server.pub

# Server config
cat > /etc/wireguard/wg-games.conf <<EOF
[Interface]
Address = 10.100.0.1/24
ListenPort = 51820
PrivateKey = $(cat /etc/wireguard/server.key)
PostUp = iptables -A FORWARD -i wg-games -j ACCEPT
PostDown = iptables -D FORWARD -i wg-games -j ACCEPT
EOF

# Generate a config for each player
for player in alice bob charlie; do
  wg genkey | tee /etc/wireguard/peers/${player}.key | wg pubkey > /etc/wireguard/peers/${player}.pub

  # Get next available IP
  NEXT_IP=$(($(wg show wg-games peers 2>/dev/null | wc -l) + 2))

  # Add peer to server config
  cat >> /etc/wireguard/wg-games.conf <<PEER

[Peer]
# ${player}
PublicKey = $(cat /etc/wireguard/peers/${player}.pub)
AllowedIPs = 10.100.0.${NEXT_IP}/32
PEER

  # Generate player config file — send this to them
  cat > /etc/wireguard/peers/${player}.conf <<CLIENT
[Interface]
PrivateKey = $(cat /etc/wireguard/peers/${player}.key)
Address = 10.100.0.${NEXT_IP}/32
DNS = 10.100.0.1

[Peer]
PublicKey = $(cat /etc/wireguard/server.pub)
Endpoint = YOUR_PUBLIC_IP:51820
AllowedIPs = 10.100.0.0/24
PersistentKeepalive = 25
CLIENT

  echo "Created config for ${player}: /etc/wireguard/peers/${player}.conf"
done

# Start WireGuard
systemctl enable --now wg-quick@wg-games

How players connect

# Send each player their .conf file
# They import it into the WireGuard app (Windows, Mac, Linux, iOS, Android)

# In-game server address becomes the WireGuard IP:
# Minecraft:  10.100.0.1:25565
# Valheim:    10.100.0.1:2456
# Palworld:   10.100.0.1:8211
# Rust:       10.100.0.1:28015

# No port forwarding on your router. No public IP exposure.
# The game server only listens on the WireGuard interface.
# To the internet, your server doesn't exist.
WireGuard is like a private VPN just for your game server. UDP-native (low latency for games), encrypted, and invisible. Your ISP sees encrypted UDP traffic on port 51820. They don't see Minecraft. Port scanners don't see your game server. DDoS bots don't see your game server. Only players with the config file can connect.

9. Performance tuning

Game servers have specific I/O patterns. Worlds are written frequently in big chunks. ZFS can be tuned for this. The goal: fast writes for game saves, enough RAM for both the game and ZFS, minimal overhead.

ZFS recordsize for game saves

Most game saves: recordsize=128K (the default). Minecraft worlds, Valheim saves, Palworld data — these are medium-sized files written in bursts. 128K is the sweet spot.

Large world files: recordsize=1M. If a game stores its entire world in one massive file (some games do), bump to 1M to reduce metadata overhead on sequential writes.

Many small files: recordsize=32K. If the game scatters data across hundreds of tiny files (mods, configs, chunk files), smaller recordsize reduces wasted space.

# Set recordsize per dataset
zfs set recordsize=128K rpool/srv/minecraft
zfs set recordsize=128K rpool/srv/valheim
zfs set recordsize=128K rpool/srv/palworld
zfs set recordsize=1M   rpool/srv/rust

ARC tuning for game servers

Game servers are write-heavy. The ZFS ARC (read cache) matters less than having enough RAM for the game processes themselves. On a dedicated game server box, reduce the ARC to leave more RAM for Docker containers.

# 32 GB RAM box running game servers
# Give ARC 4 GB, leave 28 GB for games + OS
echo "options zfs zfs_arc_max=4294967296" > /etc/modprobe.d/zfs.conf

# Apply without reboot
echo 4294967296 > /sys/module/zfs/parameters/zfs_arc_max

# Verify
arc_summary | head -20

RAM allocation guide

# For a 32 GB game server box:
#
#   OS + Docker overhead:  2 GB
#   ZFS ARC:               4 GB
#   Minecraft (Paper):     4-6 GB
#   Valheim:               4 GB
#   Palworld:              8 GB  (this game is hungry)
#   Rust:                  6-8 GB
#   ---
#   Total:               ~28-32 GB
#
# If you're running all 4 games: 32 GB minimum.
# If you're running 2-3 games: 16 GB works.
# If you're running just Minecraft: 8 GB is plenty.
#
# Rule of thumb: give ZFS ARC 10-15% of total RAM on game servers.
# On a storage server you'd give it 50-75%. Games are different.
Your RAM is a shared apartment. The game servers are loud roommates who need lots of space. ZFS ARC is the quiet roommate who's happy with a small room but uses it incredibly efficiently. Don't let the ARC hog the apartment — set a limit and let the games have what they need.

10. Replication — your world survives even if the server burns down

Snapshots protect you from software problems. Replication protects you from hardware problems. syncoid sends your game worlds to a second machine over the network. If your server dies — disk failure, power surge, coffee spill — your worlds are safe on the backup box.

Replicate game worlds to a backup server

# One command — replicate Minecraft to the backup server
syncoid rpool/srv/minecraft backup-server:tank/minecraft

# Replicate everything under /srv
syncoid -r rpool/srv backup-server:tank/gameservers

# Automate it — replicate every hour
cat > /etc/cron.d/game-replication <<'EOF'
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
0 * * * * root syncoid -r --no-sync-snap rpool/srv root@backup-server:tank/gameservers 2>&1 | logger -t syncoid
EOF

# First run sends the full dataset. Every run after that sends only changes.
# A Minecraft world that changes 50 MB/hour sends 50 MB — not the whole 2 GB world.
syncoid is like having a second hard drive in a different building that magically stays in sync with yours. The first copy takes a while. After that, it only sends what changed — a few megabytes every hour. If your server catches fire, SSH into the backup, import the pool, start Docker, and your players reconnect to a world that's at most 1 hour behind.

Disaster recovery — the backup server takes over

# Main server is dead. On the backup server:

# 1. The replicated datasets are already there
zfs list -r tank/gameservers

# 2. Set mountpoints
zfs set mountpoint=/srv/minecraft tank/gameservers/minecraft
zfs set mountpoint=/srv/valheim tank/gameservers/valheim

# 3. Start Docker containers (same compose file, same images)
cd /srv && docker compose up -d

# 4. Update WireGuard endpoints to point to backup server IP
# 5. Players reconnect — world is at most 1 hour behind

# Total recovery time: ~5 minutes of typing.
# Data loss: at most 1 hour (your replication interval).
# Without replication: total loss. Start over. Explain to 20 angry players.

Every game server admin learns the same lesson the hard way: your world is only as safe as your last backup, and your last backup is never recent enough. ZFS changes the math. Snapshots every 15 minutes cost almost nothing. Rollback takes 2 seconds. Clones are free. Replication is one command.

You don't need a game hosting company. You don't need a control panel with a monthly fee. You need a Linux box, ZFS, Docker, and 10 minutes of setup. Your friends connect over WireGuard. Your worlds are snapshotted, replicated, and indestructible.

Go build something worth protecting. ZFS will make sure you never lose it.