Skip to content

Vintage Story

Vintage Story 1.22 dedicated server running on a Proxmox LXC. Side-project host, separate from homelab core services.

Wiki: Guide:Dedicated Server

Container details

Property Value
Container ID 201
Hostname vintage
IP 192.168.1.235
OS Debian 13 (trixie), unprivileged, nesting=1
Memory 8192 MB
Cores 4
Storage 16 GB rootfs (local-zfs)
Managed by (host) Ansible — common, security, beszel_agent, fleet auto_updates
VS install Manual — wiki-blessed server.sh wrapper, no Ansible role
Default port 42420 (TCP+UDP)

Install layout

Path Purpose
/home/vintage/server/ Server binaries (extracted from the 1.22.0 tarball)
/home/vintage/server/server.sh Wiki-blessed wrapper script (start/stop/command)
/var/vintage/data/ Data path — serverconfig.json, Saves/, Mods/, Logs/, etc.
/var/vintage/data/Mods/ Drop .zip mods here, restart server
/var/vintage/data/Saves/ Savegame files (covered by pbs-daily snapshot)

The runtime user is vintage (created during manual setup). Server runs as that user via server.sh, which wraps dotnet VintagestoryServer.dll --dataPath /var/vintage/data inside a screen session named vintagestory_server.

Use bash, not /bin/sh, for the runtime user

Debian's default new-user shell is /bin/sh (dash) — minimal, no command history (no up-arrow recall), no tab completion, and confusing error messages on path issues. Switch to bash with usermod -s /bin/bash vintage (as root); takes effect on next login. Reversible with usermod -s /bin/sh vintage.

Runtime

.NET 10 runtime

VS 1.22 requires .NET 10 (the wiki page still references .NET 8 in places — out of date). Installed from the Microsoft Debian 13 package feed:

apt install dotnet-runtime-10.0

Operations (run as vintage user)

Action Command
Start server server/server.sh start
Stop server (graceful) server/server.sh stop
Send any command server/server.sh command "/whitelist add Foo"
Attach to live console /usr/bin/screen -r vintagestory_server
Detach without killing Ctrl-A then D
Tail server log tail -f /var/vintage/data/Logs/server-main.log

screen detach, not Ctrl-C

Inside an attached screen session, Ctrl-C sends SIGINT to the server and stops it. Always detach with Ctrl-A then D.

server.sh is CWD-sensitive

The wrapper resolves internal paths relative to your current directory. Always invoke from /home/vintage/ (cd ~ && server/server.sh ...) or use the absolute path (/home/vintage/server/server.sh ...). A typical symptom of CWD drift: -sh: 2: server/server.sh: not found even though the file exists.

Tracked-alias quirk if still on dash

If you haven't switched the runtime user to bash and screen -r <name> fails with screen: not found after a successful screen -ls, dash's tracked-alias cache is stale. Either hash -r to refresh, or use the full path /usr/bin/screen -r ....

Server config

Field Value Notes
ServerName Operator-set Visible in client server browser if advertised
Ip null or "192.168.1.235" Bind interface, not advertise field. Setting to your public WAN IP makes the server fail to bind because that IP doesn't exist on the CT. Leave null for "all interfaces."
Password Set via /serverconfig password <new> Per-connection password
MaxClients 16 (default) Bump via /serverconfig maxclients
Port 42420 TCP + UDP both required (UDP added in 1.20+)
AdvertiseServer false (default) Public server browser opt-in
WhitelistMode 0 = Default = on for dedicated servers 1.20+ default; legacy OnlyWhitelisted field is obsolete
DieAboveMemoryUsageMb Tune to ~7000 (CT has 8 GiB) Wiki default 50000 is well above the CT's RAM cap, so the kernel OOM-killer fires (SIGKILL = corrupt save) before this graceful-shutdown threshold ever trips. Set just below container memory for clean self-shutdown.
LoginFloodProtection true (recommended for public exposure) Rate-limits join attempts. Default is false.

Prefer runtime commands over direct JSON edits

Edit serverconfig.json directly only when the server is stopped. Even then, a single missing comma will hard-fail the next start with [Error] Failed to read serverconfig.json and a JSON parse exception in server-main.log. Runtime /serverconfig <field> <value> commands validate before persisting and survive next start, so they're safer for live changes.

Server commands reference

Sent via server/server.sh command "<cmd>" from a vintage shell, or typed directly into the live screen console. Full list at Wiki: List of server commands.

/serverconfig — runtime configuration

Command Effect
/serverconfig password <string> Set connection password (no spaces)
/serverconfig nopassword Remove connection password
/serverconfig maxclients <int> Adjust player cap
/serverconfig WhitelistMode [default\|off\|on] Toggle whitelist enforcement
/serverconfig advertise [on\|off] Toggle public server browser registration
/serverconfig allowpvp <bool> Toggle PvP
/serverconfig allowfirespread <bool> Toggle fire spread
/serverconfig allowfallingblocks <bool> Toggle gravity-affected blocks
/serverconfig antiabuse [Off\|Basic\|Pedantic] Block-interaction distance enforcement
/serverconfig defaultspawn [x y? z] Set spawn coordinates
/serverconfig setspawnhere Set spawn to your current position
/serverconfig tickrate <10-100> Server tick rate

Full subcommand list: Wiki: /serverconfig.

Whitelist & roles

Command Effect
/whitelist add <playername> Add by VS account name (resolves to UID on first connect)
/whitelist remove <playername> Remove from whitelist
/whitelist off Disable whitelist enforcement globally
/op <playername> Grant admin role
/deop <playername> Revoke admin role
/player <playername> role <rolename> Assign arbitrary role
/list role Show configured roles
/role <rolename> privilege [grant\|revoke] <privilegename> Edit role privileges

Player management

Command Effect
/kick <playername> [reason] Disconnect player
/ban <playername> [reason] Ban by player name
/banip <ip> [reason] Ban by IP — useless if a reverse proxy masks source IPs
/unban <playername> Reverse player-name ban
/list clients Online players
/list banned Banned players

Server lifecycle

Command Effect
/stop Graceful shutdown (saves world first)
/autosavenow Force a world save without stopping
/announce <message> Broadcast to all connected players
/stats Performance metrics
/info seed World seed

World & time

Command Effect
/time set <time> lunch, day, night, or HH:MM
/weather setprecip <-1..1> Force rain intensity
/nexttempstorm Show next temporal storm timing
/tp <coordinates> Teleport

Items (admin/cheat)

Command Effect
/giveitem <itemcode> <qty> [toplayer] Spawn items
/giveblock <blockcode> <qty> [toplayer] Spawn blocks
/player <playername> clearinv Clear player inventory

Mods

Manual workflow (not Ansible-managed at this stage):

  1. Download .zip from mods.vintagestory.at — verify version compatibility against 1.22.
  2. Drop into /var/vintage/data/Mods/ (do not unzip).
  3. Restart server: server/server.sh stop && server/server.sh start.
  4. Clients auto-fetch on next connect (1.16+ behaviour). Each server gets its own subfolder under ~/.config/VintagestoryData/ModsByServer/<servername>/ on the client side.

Same-filename update gotcha

If a mod updates while keeping the same filename, clients won't re-download. Either bump the filename when bumping the version, or have players manually delete the per-server subfolder client-side. Documented in Wiki: Adding mods.

The Mods/ folder inside the server binaries dir (/home/vintage/server/Mods/) holds base game files — do not delete it even though it has the same name as the data-path mods folder.

Current modlist

12 mods deployed at launch (2026-04-26). Side column reflects what the mod's modinfo.json declares; null defaults to Universal per VS convention. Client-only mods don't run server-side but auto-sync to clients on connect.

Mod Version Side Purpose Link
BetterRuins 0.6.0 Universal Adds many structures to worldgen, surface and underground mods.vintagestory.at
Beam Tweaks 2.1.0 Universal Tweaks structural beam behaviour mods.vintagestory.at
Click Up Torches 1.1.1 Universal Right-click to pick up torches mods.vintagestory.at
ClickToggle 1.1.1 Client Toggle auto-clicking mouse buttons (default N / Shift+N) mods.vintagestory.at
FirepitsShowFuel 2.0.0-dev.1 Universal Firepits display the fuel they're burning mods.vintagestory.at
Footprints 1.2.0 Universal Animals and players leave footprints when moving mods.vintagestory.at
Item Pickup Notifier 2.2.0 Client Small HUD overlay showing picked-up items mods.vintagestory.at
KsCartographyTable 1.0.4 Universal Cartography table for waypoint sharing between players mods.vintagestory.at
Real Smoke 1.2.0 Universal Physics-based smoke effects mods.vintagestory.at
Spyglass 0.6.0 Universal Spyglass tool for distance viewing mods.vintagestory.at
Stone Bake Oven 1.2.2 Universal Large stone oven for baking mods.vintagestory.at
VS Roofing 1.5.1 Universal Gridless roof crafting and building with dynamic slopes/corners mods.vintagestory.at

Verifying mod load state

There's no documented runtime "list active mods" command in 1.22. The server startup log is the authoritative source for what actually loaded.

grep -E "Mods, sorted by dependency|External Origins in load order|enabled mods|JsonPatch Loader" /var/vintage/data/Logs/server-main.log

That gives you four useful lines:

  • Mods, sorted by dependency: ... — every mod the server detected (including base game and client-only ones), in load order.
  • External Origins in load order: ... — every mod actually contributing assets to the server (excludes client-only mods).
  • Instantiated N mod systems from M enabled mods — how many server-side mods were instantiated. This count excludes client-only mods, which is why it's often lower than the number of zips on disk.
  • JsonPatch Loader: N patches total, ... — quick health signal. no errors is good; otherwise drill into the surrounding lines.

Client-only mods are not 'broken' — they're by design

Mods with "side": "Client" in their modinfo.json (UI tweaks, cosmetic effects, HUD additions like itempickupnotifier, ClickToggle) won't show up in the instantiated-server-side count. They're kept in /var/vintage/data/Mods/ so the server can auto-distribute them to clients on connect (1.16+ behaviour). Don't remove them — they're how players get the mod without each installing manually.

To inspect any mod's declared compatibility and side:

unzip -p /var/vintage/data/Mods/<modfile>.zip modinfo.json | jq '{name, version, side, dependencies, type}'

Common reasons a mod silently skips loading: incompatible game version in dependencies.game, missing dependency on another mod, duplicate mod ID conflict, or a server-side instantiation error.

Backup

Covered by the pbs-daily PVE backup job on proxfold (all 1 schedule, 02:00 daily) — picks up CT 201 automatically. Snapshots include the entire CT rootfs, which includes /var/vintage/data/ (savegames, mod files, server config). See Backup & Restore runbook for restore procedure.

Updates

The wiki has no documented update procedure for x64 Linux, and server.sh has no update subcommand. Updates are manual binary replacement. Data is separate from binaries (/var/vintage/data/ vs /home/vintage/server/), so swapping binaries doesn't risk savegames unless the new version performs an in-place world-format migration on first start (uncommon, called out in release notes when it happens).

Pre-flight

  1. Read release notes for breaking server-config changes or new runtime requirements (e.g. 1.21 → 1.22 jumped from .NET 8 → .NET 10).
  2. Check mod compatibility against the new version. Patch updates (1.22.0 → 1.22.1) almost always fine; major (1.22 → 1.23) often breaks mods:
    for z in /var/vintage/data/Mods/*.zip; do
      echo "=== $(basename "$z") ==="
      unzip -p "$z" modinfo.json 2>/dev/null | jq -r '"\(.name) -> game: \(.dependencies.game)"'
    done
    
  3. Take a manual PVE snapshot before major bumps (pbs-daily covers nightly):
    ssh root@192.168.1.250 'vzdump 201 --storage nas-primary --mode snapshot --notes-template "{{guestname}}-pre-update"'
    

Procedure (as vintage user)

# 1. Stop server gracefully
server/server.sh stop

# 2. Move old binaries aside as rollback path
mv /home/vintage/server /home/vintage/server.<old-version>
mkdir /home/vintage/server

# 3. Download + verify new tarball (substitute target version)
curl -fsSL -o /tmp/vs_server.tar.gz \
  "https://cdn.vintagestory.at/gamefiles/stable/vs_server_linux-x64_1.22.X.tar.gz"
curl -s https://api.vintagestory.at/stable.json | jq -r '."1.22.X".linuxserver.md5'
md5sum /tmp/vs_server.tar.gz   # compare against the printed md5

# 4. Extract into fresh dir
tar -xzf /tmp/vs_server.tar.gz -C /home/vintage/server
rm /tmp/vs_server.tar.gz

# 5. (CRITICAL) restore your server.sh customisations — see warning below
cp /home/vintage/server.<old-version>/server.sh /home/vintage/server/server.sh

# 6. As root, install new .NET runtime if release notes require it
#    (rare — only on big jumps like 1.21→1.22)

# 7. Start + watch the launch
server/server.sh start
tail -f /var/vintage/data/Logs/server-main.log

Look for the new version line (Game Version: v1.22.X), the Mods, sorted by dependency: block, and Dedicated Server now running on Port 42420. Re-run the mod-load grep to confirm everything still loads.

The new tarball overwrites your customised server.sh

The upstream server.sh defaults to USERNAME='vintagestory', VSPATH='/home/vintagestory/server', DATAPATH='/var/vintagestory/data' — none of which match this install (which uses vintage). If you don't restore your customisations after extracting, server.sh start errors with Username, Group or data path missing. Server environment setup needed (create user vintagestory ...). Two ways to handle: copy the old script over (step 5 above), or sed-patch the new one in place:

sed -i \
  -e 's|^USERNAME=.*|USERNAME="vintage"|' \
  -e 's|^VSPATH=.*|VSPATH="/home/vintage/server"|' \
  -e 's|^DATAPATH=.*|DATAPATH="/var/vintage/data"|' \
  /home/vintage/server/server.sh

Rollback

If the new version misbehaves or breaks mods you can't replace:

server/server.sh stop
rm -rf /home/vintage/server
mv /home/vintage/server.<old-version> /home/vintage/server
server/server.sh start

For savegame corruption from an in-place world-format migration, restore the CT from the pre-update PBS snapshot (backup-restore runbook).

Public access

Live as of 2026-04-26. Players connect to vintage.rampancy.cloud:42420.

Layer Configuration
DNS A record vintage.rampancy.cloud → <public WAN IP>. DNS-only / gray cloud on Cloudflare — orange-cloud proxying breaks game traffic.
UDM port-forward WAN 42420 TCP+UDP → 192.168.1.235:42420 (see firewall doc)
DDNS UDM Pro built-in (Settings → Internet → Dynamic DNS), if WAN IP isn't static
Reverse proxy None. VS doesn't speak PROXY protocol on either TCP or UDP, so an nginx-streams hop would mask source IPs and break /banip for the cost of zero protection. Direct UDM forward only.
Auth model Whitelist (default for dedicated servers in 1.20+) + connection password

Gotchas (worth knowing before you hit them)

  • Wiki lag. The Linux setup section on the dedicated-server wiki page references .NET 8 in places; 1.22 actually needs .NET 10. The server.sh script in the 1.22 tarball is the authoritative source for the runtime requirement (it runtime-checks dotnet 10.0).
  • server.sh data-path default. The wrapper script defaults --dataPath to /var/$USER/data (so /var/vintage/data). A non-root user can't mkdir directly under /var/, so the path must be pre-created and chowned (mkdir /var/vintage && chown vintage:vintage /var/vintage) before server.sh start succeeds. The wiki doesn't call this out.
  • Ip field is bind-interface, not advertise. A common mistake: putting your public WAN IP in serverconfig.json's Ip field. That IP doesn't exist on the CT (only 192.168.1.235 does), so the server fails to bind. Leave Ip: null (bind all) or set "192.168.1.235". The hostname clients connect to is purely DNS+port-forward, not anything VS knows about.
  • DieAboveMemoryUsageMb default defeats itself on small CTs. Default is 50000 (≈49 GiB). On an 8 GiB CT the kernel OOM-killer fires long before this trips, so the graceful shutdown protection never engages — SIGKILL leaves savefiles potentially corrupt. Tune to slightly under container RAM (~7000 here).
  • Whitelist on by default. From 1.20 onward, dedicated servers are whitelisted out of the box (WhitelistMode: 0 = "Default" = on for dedicated). Old guides referencing OnlyWhitelisted are stale — that field is obsolete.
  • Mod RAM scaling is non-linear. Vanilla 2–5 players ≈ 4 GB; modded 8+ players push 8–12 GB. The official "1 GB + 300 MB/player" formula is the "it boots" floor, not "stays smooth long-term." 8 GB allocated on this CT.
  • Client-only mods don't show in the instantiated count. Mods with "side": "Client" are in the dependency graph but skip server instantiation. The "13 enabled mods" line in the log won't include them, which can look like they failed to load — they didn't, they just don't run server-side. They still auto-sync to clients on connect.
  • Updates overwrite server.sh customisations. The upstream tarball ships server.sh with vintagestory-keyed defaults; extracting over your install replaces your vintage-keyed values. Always re-apply (cp from backup, or sed) as part of the update procedure.

Resources