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:
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):
- Download
.zipfrom mods.vintagestory.at — verify version compatibility against 1.22. - Drop into
/var/vintage/data/Mods/(do not unzip). - Restart server:
server/server.sh stop && server/server.sh start. - 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 errorsis 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¶
- Read release notes for breaking server-config changes or new runtime requirements (e.g. 1.21 → 1.22 jumped from .NET 8 → .NET 10).
- 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:
- Take a manual PVE snapshot before major bumps (pbs-daily covers nightly):
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:
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 8in places; 1.22 actually needs.NET 10. Theserver.shscript in the 1.22 tarball is the authoritative source for the runtime requirement (it runtime-checksdotnet 10.0). server.shdata-path default. The wrapper script defaults--dataPathto/var/$USER/data(so/var/vintage/data). A non-root user can'tmkdirdirectly under/var/, so the path must be pre-created and chowned (mkdir /var/vintage && chown vintage:vintage /var/vintage) beforeserver.sh startsucceeds. The wiki doesn't call this out.Ipfield is bind-interface, not advertise. A common mistake: putting your public WAN IP inserverconfig.json'sIpfield. That IP doesn't exist on the CT (only192.168.1.235does), so the server fails to bind. LeaveIp: null(bind all) or set"192.168.1.235". The hostname clients connect to is purely DNS+port-forward, not anything VS knows about.DieAboveMemoryUsageMbdefault defeats itself on small CTs. Default is50000(≈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 referencingOnlyWhitelistedare 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.shcustomisations. The upstream tarball shipsserver.shwithvintagestory-keyed defaults; extracting over your install replaces yourvintage-keyed values. Always re-apply (cp from backup, or sed) as part of the update procedure.
Resources¶
- Vintage Story Mod DB (search + JSON API at
/api/mod/<id>) - Wiki: Dedicated Server
- Wiki: List of server commands
- Wiki: Adding mods
- Wiki: Server Config (file format reference)