> ## Content Index
> Fetch the complete content index at: https://blog.sudowl.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Project Zomboid Dedicated Server on Linux
- URL: https://blog.sudowl.com/project-zomboid-dedicated-server-linux/
- Published: 2026-05-25T10:00:00.000Z
- Updated: 2026-10-11T08:26:51.000Z
- Description: Run a Project Zomboid dedicated server on Ubuntu/Debian via SteamCMD. Configs, mods load order, memory, ports, systemd, and the gotchas that corrupt saves.
- Author: Lukas
- Tags: Gameserver, Linux

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:**

```bash
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](https://blog.sudowl.com/7-days-to-die-dedicated-server-linux/) and [Enshrouded](https://blog.sudowl.com/enshrouded-dedicated-server-linux/), 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.

```bash
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:

```bash
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 config
- `Zomboid/Server/servertest_SandboxVars.lua` — gameplay/world rules
- `Zomboid/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:

```ini
PublicName=My Zomboid Server
PublicDescription=Bring bandages.
Public=true
Password=joinpassword
MaxPlayers=16
DefaultPort=16261
UDPPort=16262
PauseEmpty=true
Mods=
WorkshopItems=

```

- `Password` is the join password (empty = open server). The admin password from first launch is separate.
- `Public=true` lists you in the in-game browser. `false` \= direct-connect only.
- `PauseEmpty=true` freezes the sim when nobody's on — saves CPU.
- `Mods=` and `WorkshopItems=` 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`:

```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`:

```json
"vmArgs": [
    "-Xmx8g",
    "-Xms8g",
    "-Dzomboid.steam=1",
    "-XX:+UseZGC"
]

```

Or override per-launch with `start-server.sh -Xmx8g`. Rules:

- `-Xmx8g` is 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 `-Xms` equal to `-Xmx` so 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**:

```ini
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's `mod.info` file). 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`:

```ini
[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/pz` and `Environment=HOME=/opt/pz` — PZ writes its `Zomboid/` 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 admin `quit`.
- `TimeoutStopSec=120` — give it time to save on shutdown.

```bash
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](https://blog.sudowl.com/systemd-service-unit-file/) 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:

```bash
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](https://blog.sudowl.com/ufw-firewall-rules-ubuntu/).

## Gotchas

- **`-adminpassword` only matters on first launch.** Once configs exist it's ignored; change the admin password from the console with `setaccesslevel` or 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 `-Xmx` can't be backed.
- **Wrong `$HOME` in 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:

```bash
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:

```bash
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.

---

### Related posts

- [Terraria Dedicated Server on Linux with TShock](https://blog.diengdoh.com/terraria-tshock-dedicated-server-linux/?ref=blog.sudowl.com)
- [Discord Alerts From Your Server Logs — A 100-Line Bot That Beats a Monitoring Stack](https://blog.diengdoh.com/discord-alerts-server-logs-systemd/?ref=blog.sudowl.com)
- [Monitoring a Game Server Over WireGuard — Prometheus Without Exposing a Single Port](https://blog.diengdoh.com/monitor-gameserver-wireguard-prometheus/?ref=blog.sudowl.com)