Project Zomboid ships a real native Linux server, no Wine. SteamCMD pulls it, app id 380870, anonymous login. The pain isn't the install — it's mod load order and giving the JVM enough RAM.
TL;DR:
sudo adduser --system --group --home /opt/pz steam
sudo -u steam -H steamcmd \
+force_install_dir /opt/pz \
+login anonymous \
+app_update 380870 validate \
+quit
sudo ufw allow 16261:16262/udp
sudo -u steam -H /opt/pz/start-server.sh -adminpassword "changeme"
Then edit the configs in ~/Zomboid/Server/, wire up a systemd unit, set memory. Details below. Same SteamCMD pattern as 7 Days to Die and Enshrouded, but PZ runs on the JVM so the memory story is different.
Install via SteamCMD
App id is 380870 — that's the dedicated server, not the game. Anonymous login works; the server is free. Run everything as a non-root steam user. Never run game servers as root.
sudo dpkg --add-architecture i386
sudo apt update
sudo apt install steamcmd openjdk-17-jre-headless
sudo adduser --system --group --home /opt/pz steam
sudo -u steam -H steamcmd \
+force_install_dir /opt/pz \
+login anonymous \
+app_update 380870 validate \
+quit
PZ bundles its own JRE under /opt/pz/jre64/, so the apt openjdk line is optional — but having a system JRE saves headaches when the bundled one breaks after an update.
First launch creates the configs
The config files don't exist until the server runs once. First launch with an admin password:
sudo -u steam -H /opt/pz/start-server.sh -adminpassword "SomeStrongPassword"
It boots, generates everything under /opt/pz/Zomboid/Server/ (the home dir is the install dir here because of --home /opt/pz), then sits at a > console prompt. Type quit and Enter to shut down cleanly. Do not Ctrl-C — that risks a half-written save. Always quit from the console.
After this you have:
Zomboid/Server/servertest.ini— the main server configZomboid/Server/servertest_SandboxVars.lua— gameplay/world rulesZomboid/Server/servertest_spawnregions.lua— spawn zones
servertest is the default server name. If you pass -servername myworld, the files become myworld.ini etc.
servertest.ini — the main config
Stop the server, then edit ~/Zomboid/Server/servertest.ini. The lines that matter:
PublicName=My Zomboid Server
PublicDescription=Bring bandages.
Public=true
Password=joinpassword
MaxPlayers=16
DefaultPort=16261
UDPPort=16262
PauseEmpty=true
Mods=
WorkshopItems=
Passwordis the join password (empty = open server). The admin password from first launch is separate.Public=truelists you in the in-game browser.false= direct-connect only.PauseEmpty=truefreezes the sim when nobody's on — saves CPU.Mods=andWorkshopItems=are where the mod fight happens. See below.
servertest_SandboxVars.lua — the world rules
This is the Lua file that controls zombies, loot, and time. Edit ~/Zomboid/Server/servertest_SandboxVars.lua:
SandboxVars = {
Zombies = 3, -- 1=insane, 3=normal, 4=few
Distribution = 1,
DayLength = 4, -- hours of real time per in-game day
StartMonth = 7,
WaterShutModifier = 14, -- days until water cuts
ElecShutModifier = 14,
LootRespawn = 1, -- 1=none, 2=every day, ...
ZombieConfig = {
Speed = 2, -- 1=sprinters, 2=fast shamblers
Strength = 3,
PopulationMultiplier = 1.0,
},
}
DayLength is the one people get wrong — it's an enum-ish hours value, not minutes. LootRespawn=1 (none) is the right call for a small persistent group; respawning loot trivializes the mid-game.
Memory — give the JVM real RAM
PZ is a Java process. Default heap is -Xmx8g (it used to be 16g in older builds — check yours). Edit /opt/pz/ProjectZomboid64.json:
"vmArgs": [
"-Xmx8g",
"-Xms8g",
"-Dzomboid.steam=1",
"-XX:+UseZGC"
]
Or override per-launch with start-server.sh -Xmx8g. Rules:
-Xmx8gis a hard cap and it needs that RAM to actually exist. Set 8g on a 4 GB box and the JVM fails to start, full stop.- Set
-Xmsequal to-Xmxso the heap doesn't grow-and-fragment under load. - 16 players with a few mods sits around 6-7 GB resident. An empty 4-player server is fine at 4g, but don't go lower.
Mods and load order — the actual gotcha
This is where servers break. Two lists in servertest.ini, both semicolon-separated, and load order matters:
WorkshopItems=2392709985;2169435993;2860874280
Mods=tsarslib;BetterSortCC;Brita
WorkshopItems=is the numeric Steam Workshop ID (the number in the workshop URL). This tells the server what to download.Mods=is the internal mod ID (a string, found in the mod'smod.infofile). This tells the server what to load and in what order.
The two lists are different identifiers for the same mods, and people constantly mix them up — putting workshop IDs in Mods= (server loads nothing) or mod IDs in WorkshopItems= (server downloads nothing).
Load order: dependencies first. If Brita needs tsarslib, tsarslib must come earlier in the Mods= line. Get it wrong and the server either won't start or silently drops the mod, and every connecting client gets a mismatch kick. When a mod misbehaves, the server log under ~/Zomboid/server-console.txt names the offending mod ID — read it.
Always restart the server after changing mods, and make sure every player has the exact same mod set, or they can't join.
systemd unit
/etc/systemd/system/pzserver.service:
[Unit]
Description=Project Zomboid Dedicated Server
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=steam
Group=steam
WorkingDirectory=/opt/pz
Environment=HOME=/opt/pz
ExecStart=/opt/pz/start-server.sh -adminpassword "SomeStrongPassword"
Restart=on-failure
RestartSec=15
TimeoutStopSec=120
KillSignal=SIGTERM
[Install]
WantedBy=multi-user.target
What matters here:
WorkingDirectory=/opt/pzandEnvironment=HOME=/opt/pz— PZ writes itsZomboid/data relative to$HOME. Get this wrong and you get a second config tree in the wrong place and wonder why your edits do nothing.User=steam— non-root, again.Restart=on-failure— restart on crash, not on a clean adminquit.TimeoutStopSec=120— give it time to save on shutdown.
sudo systemctl daemon-reload
sudo systemctl enable --now pzserver
sudo journalctl -u pzserver -f
If you'd rather not use systemd, a screen -dmS pz /opt/pz/start-server.sh or tmux new -d gets you a detachable console you can reattach to with screen -r pz and type admin commands. Systemd is cleaner for autostart and restarts — read systemd service unit files if the unit above is unfamiliar.
Admin console / RCON
The server console (where you ran start-server.sh, or via screen -r) takes admin commands directly:
players
kick "username"
save
quit
save forces a world write; quit saves and shuts down. There's also RCON if you set RCONPassword and RCONPort in servertest.ini — handy for remote admin via mcrcon, but for a small server the console is enough.
Always save then quit to stop the server. A SIGKILL or power loss mid-write corrupts the map chunks, and PZ's corruption symptom is ugly: the server starts, then players spawn into black voids or fall through the world. There's no clean repair — you restore a backup.
Ports
PZ uses two UDP ports by default: 16261 (default/Steam) and 16262 (direct connect). Open both:
sudo ufw allow 16261:16262/udp
If you raise MaxPlayers past 16, PZ opens additional sequential UDP ports above 16262 (one per extra player slot for direct connect). For a 32-slot server, open 16261:16272/udp or wider. UDP only — opening TCP does nothing. UFW rules reference.
Gotchas
-adminpasswordonly matters on first launch. Once configs exist it's ignored; change the admin password from the console withsetaccesslevelor by editing the player DB, not by re-passing the flag.- Mod ID vs Workshop ID. Covered above. The single most common reason a server won't start.
- Heap larger than physical RAM = no boot. The JVM won't start if
-Xmxcan't be backed. - Wrong
$HOMEin systemd. Configs you edit must be the ones the service reads. Confirm the path. - Updates can break mods. After a
app_update 380870, mods built for the old build may be incompatible until their authors update. Pin your players to the same branch.
Verify
Server should be listening on UDP 16261:
sudo ss -tulnp | grep 1626
You want udp UNCONN 0 0 0.0.0.0:16261. No listener means the server didn't bind — check ~/Zomboid/server-console.txt for a startup error (usually a bad mod or a heap it couldn't allocate).
From inside the console, players should respond. From outside the box, prove reachability:
nc -uvz <publicip> 16261
If ss shows the listener but external clients still can't join, the gap is your firewall or home-router NAT, not the server. Same logic as every other game server — bind first, route second.