Valheim Dedicated Server — Proxmox LXC¶
Valheim dedicated server inside an unprivileged Proxmox LXC container using SteamCMD, based on this r/valheim writeup with two changes:
- No root, and no single combined account. The original post runs everything as root. Instead, two accounts are used:
jake— asudoadmin account you SSH into the box as — andsteam— a non-sudo service account that owns and runs SteamCMD and the Valheim server, and nothing else. - No port forwarding. Instead of opening inbound ports on your router/firewall, the LXC joins your Tailscale tailnet, so only devices already on your tailnet can reach the server.
Follows the same overall structure as the Minecraft-on-Proxmox-LXC notes: provision the container → base OS hardening → admin user + SSH hardening → dedicated service user → install the game server → wrap it in systemd → put it on the private network.
1. Prerequisites¶
- A working Proxmox VE host with an available CT template (Debian 12 or Ubuntu 22.04/24.04 recommended — this guide uses Debian 12).
- A Tailscale account (free tier is fine) and an auth key or ability to authenticate interactively.
- At least 2 vCPU / 4 GB RAM / 10 GB disk allocated to the container (Valheim itself is light, but world generation and save files grow over time — 20 GB+ gives headroom for backups).
- No static IP config needed on the container itself — DHCP is fine. Reserve the assigned address as a static lease on your router/DHCP server if you want it to stay put.
2. Generate an SSH key (on your workstation)¶
ssh-keygen -t ed25519 -C "your-email@example.com"
Accept the default path (~/.ssh/id_ed25519). Print the public key so you can copy it:
cat ~/.ssh/id_ed25519.pub
This key gets seeded into the container's root account at creation time (either pasted into the Proxmox "Create CT" GUI wizard, or passed via --ssh-public-keys on the CLI below) and then copied over to jake once the admin user exists.
3. Create the LXC container¶
From the Proxmox shell (or the "Create CT" wizard, pasting your public key into the SSH key field on the General tab):
pveam update
pveam available --section system | grep debian-12
pveam download local debian-12-standard_12.7-1_amd64.tar.zst
pct create 200 local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst \
--hostname valheim \
--cores 2 \
--memory 4096 \
--swap 512 \
--rootfs local-lvm:20 \
--net0 name=eth0,bridge=vmbr0,ip=dhcp \
--ssh-public-keys ~/.ssh/id_ed25519.pub \
--unprivileged 1 \
--features nesting=1 \
--onboot 1
Notes on these flags
--ssh-public-keyspoints at the.pubfile's path on the Proxmox host — copy it there first (e.g.scpit up, or paste the key text into a file withnano) if you're running this from an SSH session on the host rather than your workstation directly.--unprivileged 1keeps the container mapped to unprivileged UIDs on the host — good practice, and it works fine with SteamCMD and Tailscale (see the Tailscale section for the one extra device permission it needs).--features nesting=1isn't strictly required for Valheim, but it avoids a class of cgroup/namespace headaches with some Tailscale/systemd interactions in LXC and costs nothing to enable.ip=dhcpis intentional — go reserve the lease on your router once you see what address it picks up, rather than configuring a static IP in the container.
Start the container and open a console:
pct start 200
pct enter 200
4. Base OS setup (as root, one-time)¶
apt update && apt full-upgrade -y
apt install -y curl wget sudo gnupg lib32gcc-s1 lib32stdc++6 unzip ufw ca-certificates
lib32gcc-s1 / lib32stdc++6 are required
SteamCMD and the Valheim server binaries are 32-bit executables even on a 64-bit host. These 32-bit runtime libraries are the single most common install failure if skipped.
On Debian, also enable the non-free component and add i386 architecture:
dpkg --add-architecture i386
sed -i 's/main$/main contrib non-free non-free-firmware/' /etc/apt/sources.list
apt update
5. Create the admin user (jake) and lock down SSH¶
# Create your admin user
useradd -m -s /bin/bash -G sudo jake
passwd jake
# Copy root's SSH key so jake can log in
mkdir -p /home/jake/.ssh
cp /root/.ssh/authorized_keys /home/jake/.ssh/authorized_keys
chown -R jake:jake /home/jake/.ssh
chmod 700 /home/jake/.ssh
chmod 600 /home/jake/.ssh/authorized_keys
Harden SSH:
nano /etc/ssh/sshd_config
Set these lines:
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
systemctl restart sshd
Test before closing your session
Open a second terminal and confirm you can SSH in as jake before closing your current session:
ssh jake@<container-ip>
All further steps are run as jake using sudo.
6. Create the steam service user (non-sudo)¶
This account owns SteamCMD, the Valheim install, and the running server process. Unlike jake, it gets no sudo group membership, no password, and no SSH key — the only way onto this account is by escalating from jake. If a bug in SteamCMD or the game server itself were ever exploited, steam has no path to root.
sudo adduser --disabled-password --gecos "" steam
To act as steam (installing SteamCMD, downloading the server, editing the start script), drop into a login shell from your jake session:
sudo -iu steam
This works without ever setting a password for steam, because sudo is authenticating you (as jake), not steam. Use exit to return to your jake shell. Anywhere below that says "as steam," this is how you get there.
7. Install SteamCMD as the steam user¶
sudo -iu steam
mkdir -p ~/steamcmd && cd ~/steamcmd
curl -sqL "https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz" -o steamcmd_linux.tar.gz
tar -xvzf steamcmd_linux.tar.gz
rm steamcmd_linux.tar.gz
Run it once so it self-updates and confirm it works:
./steamcmd.sh +quit
8. Install the Valheim dedicated server¶
Still as steam. Valheim's dedicated server is Steam app ID 896660, and it's free to download anonymously (you don't need to own the game):
mkdir -p ~/valheim-server
~/steamcmd/steamcmd.sh \
+force_install_dir /home/steam/valheim-server \
+login anonymous \
+app_update 896660 validate \
+quit
This drops valheim_server.x86_64, start_server.sh, and the game's shared libraries into /home/steam/valheim-server.
9. The start script¶
SteamCMD ships a default start_server.sh. Rather than editing the stock file (it can be overwritten by future app_update runs), copy it:
cp start_server.sh start_valheim.sh
chmod +x start_valheim.sh
Then edit start_valheim.sh with your own values. Flag reference:
| Line/Flag | Purpose |
|---|---|
export LD_LIBRARY_PATH=./linux64:$LD_LIBRARY_PATH |
Tells the dynamic linker to look in the bundled linux64/ folder for the game's shared libraries (Steamworks networking, etc.) before falling back to system libraries. Without this the binary won't find libsteam_api.so and will fail to start. |
export SteamAppId=892970 |
Sets the Steam App ID the binary checks at launch. 892970 is the client app ID (Valheim the game); this is not the same as 896660, which is the dedicated server app used only for the SteamCMD download step. |
-name "My server" |
The server name shown in the in-game server browser and join screen. Keep it under ~64 characters; avoid special characters that can break log parsing. |
-port 2456 |
The base UDP port the server listens on. Valheim uses two consecutive ports (2456 and 2457 by default). If you run multiple servers on one host, space them at least 2 ports apart. |
-world "Dedicated" |
The world/save name. Determines the filenames for the .db and .fwl save files. Changing this after the fact starts a new world — it does not rename an existing one. |
-password "secret" |
Join password. Valheim enforces a minimum of 5 characters, and the password cannot be a substring of the server name or vice versa — the server will refuse to start otherwise. |
-public 1 |
1 lists the server in Valheim's public browser; 0 hides it. Since we're not port-forwarding, set this to 0. |
-crossplay |
Enables the PlayFab-based crossplay backend so Xbox/Game Pass and Steam players can join the same server. When enabled, players join via an in-game invite code shown in the server log rather than a raw IP:port. Omit if you only ever connect via Tailscale IP from Steam clients. |
-savedir "/home/steam/valheim-server/saves" |
Overrides where world saves are written. By default Valheim writes to ~/.config/unity3d/IronGate/Valheim/; pointing this at an explicit path makes backups and permissions easier to manage under systemd. |
-logFile "/home/steam/valheim-server/logs/valheim_server.log" |
Redirects server log output to a file — required once the process is daemonized under systemd. |
-saveinterval <seconds> (optional) |
How often the world autosaves. Default is 1800 (30 min). |
-backups <count> / -backupshort <seconds> / -backuplong <seconds> (optional) |
Iron Gate's built-in rolling backup system. Useful as a second layer on top of Proxmox-level backups. |
A filled-in example:
#!/bin/bash
export LD_LIBRARY_PATH=./linux64:$LD_LIBRARY_PATH
export SteamAppId=892970
./valheim_server.x86_64 \
-name "Bifrost" \
-port 2456 \
-world "Midgard" \
-password "vikingsonly" \
-public 0 \
-savedir "/home/steam/valheim-server/saves" \
-logFile "/home/steam/valheim-server/logs/valheim_server.log" \
-saveinterval 1800 \
-backups 4 \
-backupshort 7200 \
-backuplong 43200
Create the folders and do a manual test run before wiring up systemd:
mkdir -p ~/valheim-server/saves ~/valheim-server/logs
cd ~/valheim-server
./start_valheim.sh
Watch the log for Game server connected and confirm no crash loop, then Ctrl+C to stop it. Once confirmed, exit out of the steam shell back to jake.
10. World modifiers (resource drop rate, difficulty, raids, etc.)¶
The dedicated server accepts world modifiers directly as launch arguments — you don't need to create the world locally in the Valheim client. They take effect on restart against the existing world. Add these to start_valheim.sh (as steam, via sudo -iu steam).
Three flags handle it:
| Flag | Format | What it does |
|---|---|---|
-preset <name> |
one value | Applies a full tuned profile: normal, casual, easy, hard, hardcore, immersive, hammer |
-modifier <name> <value> |
repeatable | Adjusts one dial at a time |
-setkey <key> |
repeatable, no value | Pure on/off toggles |
-modifier reference:
| Modifier | Values | Controls |
|---|---|---|
combat |
veryeasy, easy, hard, veryhard |
Fight difficulty |
deathpenalty |
casual, veryeasy, easy, hard, hardcore |
What you lose on death |
resources |
muchless, less, more, muchmore, most |
Resource drop rate |
raids |
none, muchless, less, more, muchmore |
Base-attack frequency |
portals |
casual, hard, veryhard |
Portal item-transfer rules |
-setkey reference: nobuildcost, playerevents, passivemobs, nomap — each is a bare flag with no value.
Ordering matters
Always put -preset first, then -modifier, then -setkey. The server reads arguments left to right — a later -preset will silently flatten the modifiers you set before it.
./valheim_server.x86_64 \
-name "Bifrost" \
-port 2456 \
-world "Midgard" \
-password "vikingsonly" \
-public 0 \
-preset casual \
-modifier resources most \
-modifier raids less \
-setkey nobuildcost \
-savedir "/home/steam/valheim-server/saves" \
-logFile "/home/steam/valheim-server/logs/valheim_server.log"
To apply a change: edit start_valheim.sh as steam, then restart the service as jake:
sudo systemctl restart valheim
Modifiers are read at server startup rather than baked into the world file, so you can retune resource rates or difficulty at any point in an existing world's life without touching save files.
11. Run it as a systemd service¶
As jake (via sudo), create the unit file:
sudo tee /etc/systemd/system/valheim.service > /dev/null <<'EOF'
[Unit]
Description=Valheim Dedicated Server
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=steam
Group=steam
WorkingDirectory=/home/steam/valheim-server
ExecStart=/home/steam/valheim-server/start_valheim.sh
KillSignal=SIGINT
TimeoutStopSec=90
Restart=on-failure
RestartSec=15s
LimitNOFILE=100000
[Install]
WantedBy=multi-user.target
EOF
Why KillSignal=SIGINT and TimeoutStopSec=90
Valheim needs a clean SIGINT (not SIGKILL) to flush the world save on shutdown — an abrupt kill can corrupt or roll back the save. User=steam / Group=steam enforces the privilege drop at the process level; steam never needs a password, sudo rights, or an interactive login for the service to work.
Enable and start it:
sudo systemctl daemon-reload
sudo systemctl enable --now valheim
sudo systemctl status valheim
journalctl -u valheim -f
12. Networking: Tailscale instead of port forwarding¶
Rather than forwarding UDP 2456-2457 on your router, join the LXC to your tailnet so it's reachable only from your other tailnet devices.
12.1 Allow the TUN device on an unprivileged container¶
Unprivileged LXCs don't have access to /dev/net/tun by default, which Tailscale needs. On the Proxmox host, edit the container config:
nano /etc/pve/lxc/200.conf
Add:
lxc.cgroup2.devices.allow: c 10:200 rwm
lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file
Restart the container from the host:
pct stop 200 && pct start 200
Privileged alternative
If you'd rather avoid this step, mark the container privileged at creation time instead — simpler, but gives up some of the isolation unprivileged containers provide.
12.2 Install and connect Tailscale inside the container¶
Back inside the container, as jake:
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --hostname=valheim-lxc
If you have an auth key, authenticate non-interactively instead:
sudo tailscale up --authkey=tskey-auth-xxxxxxxxxxxx --hostname=valheim-lxc
Otherwise, the command prints a login URL — open it in a browser to approve the device against your tailnet.
Set jake as the Tailscale operator so day-to-day commands don't need sudo every time:
sudo tailscale set --operator=jake
Confirm it's connected and note its Tailscale IP:
tailscale status
tailscale ip -4
12.3 Is a local firewall necessary?¶
Since nothing is port-forwarded on your router, the Valheim ports (2456/2457) are already unreachable from the public internet with or without ufw. What ufw actually protects against here is your LAN: without it, anything else on your home network can still hit those ports directly over eth0. ufw lets you say "the game only exists on tailscale0."
SSH stays reachable from the LAN in this setup so jake can administer the box without needing to be on the tailnet. If you'd rather manage the container over Tailscale exclusively, see the commented line below.
sudo ufw default deny incoming
sudo ufw allow in on tailscale0
sudo ufw allow ssh # keeps SSH reachable from your LAN
# sudo ufw allow in on tailscale0 to any port 22 # use this instead + remove the line above to require Tailscale for SSH too
sudo ufw enable
12.4 Connecting from a client¶
In Valheim, use Join IP and enter <tailscale-ip>:2456 (the port you set with -port). Anyone connecting needs:
- To be a member of your tailnet (or an approved shared/guest node), and
- The in-game server password from
-password.
No router configuration, DDNS, or public port exposure required.
13. Day-2 operations¶
Updating the server — stop the service as jake, re-run the SteamCMD update as steam, restart:
sudo systemctl stop valheim
sudo -iu steam bash -c '~/steamcmd/steamcmd.sh +force_install_dir /home/steam/valheim-server +login anonymous +app_update 896660 validate +quit'
sudo systemctl start valheim
Or use the update script below, which wraps those same steps with error handling and a single sudo prompt.
Update script (~/update-valheim.sh)¶
Create it directly on the container as jake:
nano ~/update-valheim.sh
#!/bin/bash
set -euo pipefail
# Cache sudo credentials up front so you're only prompted once,
# not separately for each sudo/sudo -iu call below.
sudo -v
echo "Stopping valheim service..."
sudo systemctl stop valheim
echo "Updating server via SteamCMD (as steam)..."
sudo -iu steam bash -c '~/steamcmd/steamcmd.sh +force_install_dir /home/steam/valheim-server +login anonymous +app_update 896660 validate +quit'
echo "Starting valheim service..."
sudo systemctl start valheim
echo "Done. Current status:"
sudo systemctl status valheim --no-pager
Make it executable:
chmod +x ~/update-valheim.sh
Run it with:
~/update-valheim.sh
A few notes on what's doing the work:
set -euo pipefail— stops the script immediately if any command fails, instead of plowing ahead and, say, restarting the service on top of a half-finished update. Without this, a failedapp_updatewouldn't stop the script from trying to start the service anyway.sudo -v— refreshes/caches your sudo timestamp right at the start. Since the default sudo timeout is probably 5–15 minutes, the two latersudocalls (and thesudo -iu steam, which is itself just another sudo invocation) reuse that cached auth instead of prompting you three separate times mid-script.- The
sudo -iu steam bash -c '...'line is unchanged from the manual steps above — single quotes are important there so~and variables expand inside thesteamshell, not in yourjakeshell beforesudoeven runs.
Backups. Iron Gate's -backups/-backupshort/-backuplong flags handle rolling backups inside saves/, but that's not a substitute for backing up off the container. At minimum, snapshot the LXC itself from Proxmox (vzdump), and/or periodically copy ~/valheim-server/saves/ somewhere else.
Logs: journalctl -u valheim -f for the systemd wrapper, or tail the file set by -logFile directly for game-level output.
Checking it's really off the public internet: from a device not on your tailnet, confirm nmap -sU -p2456-2457 <public-ip> fails — that's the whole point of skipping port forwarding.