Basic Server Setup and Server Installation

Install steamcmd and unzip.

root@server # add-apt-repository multiverse root@server # dpkg --add-architecture i386 root@server # apt-get update root@server # apt-get install steamcmd unzip

Create a user under which the server will run.

root@server # adduser valheimuser

Create a directory for the server.

root@server # mkdir /opt/valheimserver

Create the install script for the server /opt/valheimserver/install_update.sh

#!/bin/bash cd "$(dirname "$0")"; dir=$(pwd); /usr/games/steamcmd +@sSteamCmdForcePlatformType linux +force_install_dir ${dir} +login anonymous +app_update 896660 -beta public validate +quit;

Change /opt/valheimserver/install_update.sh to be executable.

root@server # chmod +x /opt/valheimserver/install_update.sh

Change the owner of /opt/valheimserver and its contents to valheimuser

root@server # chown -R valheimuser:valheimuser /opt/valheimserver

Execute the script as valheimuser

root@server # su - valheimuser -c '/opt/valheimserver/install_update.sh'

Configuring the firewall

Open ports 2456, 2457, and 2458 in ufw.

root@server # ufw allow 2456:2458/udp

Installing BepInEx

Download BepInEx for Valheim from Thunderstore.io on to your local computer. The Thunderstore package page does not expose a direct download URL, so wget will not work here — use your browser to download the zip, then scp it to the server. The mod update script later in this guide uses the Thunderstore API which does provide direct download URLs.

Use scp to copy the file e.g. denikson-BepInExPack_Valheim-5.4.2333.zip to the server.

you@your_computer $ scp /home/you/Downloads/denikson-BepInExPack_Valheim-5.4.2333.zip you@remote:/tmp/denikson-BepInExPack_Valheim-5.4.2333.zip

Extract denikson-BepInExPack_Valheim-5.4.2333.zip on the server.

you@server $ cd /tmp && unzip denikson-BepInExPack_Valheim-5.4.2333.zip -d BepInExPack_Valheim-5.4.2333

Copy the files in BepInExPack_Valheim to /opt/valheimserver.

you@server $ sudo cp -r /tmp/BepInExPack_Valheim-5.4.2333/BepInExPack_Valheim/. /opt/valheimserver/

Replace the contents of /opt/valheimserver/start_server_bepinex.sh with the following. Change the name, world, and password variables to suit your server. The password must be five or more characters in length or the server will not start.

#!/bin/sh # BepInEx-specific settings # NOTE: Do not edit unless you know what you are doing! #### export DOORSTOP_ENABLE=TRUE export DOORSTOP_INVOKE_DLL_PATH=./BepInEx/core/BepInEx.Preloader.dll export DOORSTOP_CORLIB_OVERRIDE_PATH=./unstripped_corlib export LD_LIBRARY_PATH="./doorstop_libs:$LD_LIBRARY_PATH" export LD_PRELOAD="libdoorstop_x64.so:$LD_PRELOAD" #### export templdpath=$LD_LIBRARY_PATH export LD_LIBRARY_PATH=./linux64:$LD_LIBRARY_PATH export SteamAppID=892970 echo "Starting server PRESS CTRL-C to exit" # Tip: Make a local copy of this script to avoid it being overwritten by steam. # NOTE: Minimum password length is 5 characters & Password cant be in the server name. # NOTE: You need to make sure the ports 2456-2458 is being forwarded to your server through your local router & firewall. name="change_to_your_server_name"; world="change_to_your_world_name"; password="change_to_your_password"; ./valheim_server.x86_64 -name "${name}" -port 2456 -nographics -batchmode -world "${world}" -password "${password}" -public 1 export LD_LIBRARY_PATH=$templdpath

Change /opt/valheimserver/start_server_bepinex.sh to be executable.

root@server # chmod +x /opt/valheimserver/start_server_bepinex.sh

Change the owner of /opt/valheimserver and its contents to valheimuser.

root@server # chown -R valheimuser:valheimuser /opt/valheimserver

Configuring systemd

Create a systemd service file /etc/systemd/system/valheim.service with the following contents.

[Unit] Description=Start the Valheim Server Requires=network-online.target After=network-online.target [Service] Type=simple Restart=always RestartSec=10 User=valheimuser Group=valheimuser WorkingDirectory=/opt/valheimserver/ ExecStartPre=/opt/valheimserver/install_update.sh ExecStart=/opt/valheimserver/start_server_bepinex.sh [Install] WantedBy=multi-user.target

Start the server and enable the service so the Valheim server starts at boot.

root@server # systemctl enable --now valheim.service

Installing and Updating Mods on the Server

Mods are managed through two files kept together in the same directory:

  • mods.json — the list of mods to install, each referencing a Thunderstore author, package name, and version
  • update_mods.sh — the script that stops the server, downloads every mod fresh, reconciles the plugins directory, and restarts the server

mods.json

Each entry references a package on Thunderstore.io. Set "version" to "latest" to always pull the newest release, or pin a specific version number to prevent unintended updates.

[ { "author": "Advize", "package": "PlantEverything", "version": "latest" }, { "author": "RandyKnapp", "package": "EquipmentAndQuickSlots", "version": "latest" }, { "author": "k942", "package": "MassFarming", "version": "latest" } ]

To find the correct author and package values for a mod, open its Thunderstore page — they appear in the URL: https://thunderstore.io/c/valheim/p/<author>/<package>/.

Mods not available on Thunderstore must still be copied manually via scp into /opt/valheimserver/BepInEx/plugins.

update_mods.sh

#!/bin/bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" MODS_JSON="${SCRIPT_DIR}/mods.json" PLUGINS_DIR="/opt/valheimserver/BepInEx/plugins" BACKUP_DIR="/opt/valheimserver/BepInEx/plugins.bak" VALHEIM_OWNER="valheimuser:valheimuser" SERVICE="valheim.service" TMP_DIR=$(mktemp -d /tmp/valheim_mods.XXXXXX) cleanup() { rm -rf "$TMP_DIR"; } trap cleanup EXIT echo "==> Stopping ${SERVICE}..." sudo systemctl stop "$SERVICE" echo "==> Backing up current plugins to ${BACKUP_DIR}..." sudo rm -rf "$BACKUP_DIR" sudo cp -r "$PLUGINS_DIR" "$BACKUP_DIR" echo "==> Downloading and extracting mods..." STAGING_DIR="${TMP_DIR}/staging" mkdir -p "$STAGING_DIR" python3 << PYEOF import json, urllib.request, zipfile, shutil, os HEADERS = {"User-Agent": "valheim-update-mods/1.0"} def fetch_json(url): req = urllib.request.Request(url, headers=HEADERS) with urllib.request.urlopen(req) as r: return json.load(r) def download_file(url, dest): req = urllib.request.Request(url, headers=HEADERS) with urllib.request.urlopen(req) as r, open(dest, "wb") as f: shutil.copyfileobj(r, f) with open("${MODS_JSON}") as f: mods = json.load(f) for mod in mods: author = mod["author"] package = mod["package"] version = mod.get("version", "latest") if version == "latest": api_url = f"https://thunderstore.io/api/experimental/package/{author}/{package}/" data = fetch_json(api_url) version = data["latest"]["version_number"] download_url = data["latest"]["download_url"] else: download_url = f"https://thunderstore.io/package/download/{author}/{package}/{version}/" print(f" {author}-{package} -> {version}") zip_path = f"${TMP_DIR}/{author}-{package}.zip" download_file(download_url, zip_path) with zipfile.ZipFile(zip_path) as z: names = z.namelist() plugin_files = [n for n in names if "/plugins/" in n.lower() and not n.endswith("/")] if not plugin_files: plugin_files = [n for n in names if n.endswith(".dll") or n.endswith(".json")] if not plugin_files: print(f" WARNING: no plugin files found, skipping") continue for name in plugin_files: filename = os.path.basename(name) if not filename: continue target = os.path.join("${STAGING_DIR}", filename) with z.open(name) as src, open(target, "wb") as dst: shutil.copyfileobj(src, dst) print(f" + {filename}") PYEOF echo "==> Reconciling plugins directory..." sudo find "$PLUGINS_DIR" -mindepth 1 -delete sudo cp -r "$STAGING_DIR/." "$PLUGINS_DIR/" echo "==> Fixing ownership..." sudo chown -R "$VALHEIM_OWNER" "$PLUGINS_DIR" echo "==> Starting ${SERVICE}..." sudo systemctl start "$SERVICE" sleep 3 echo "==> Status:" sudo systemctl status "$SERVICE" --no-pager | head -12 echo "" echo "Done. Previous plugins backed up to ${BACKUP_DIR}"

Place both files in the same directory on the server — the home directory works well.

you@your_computer $ scp mods.json update_mods.sh you@remote:~/

Make the script executable.

you@server $ chmod +x ~/update_mods.sh

Run the script whenever mods need to be added, removed, or updated. The script requires sudo to stop and start the service and write to /opt/valheimserver. It wipes plugins/ completely and replaces it with exactly what is in mods.json, keeping a plugins.bak copy as a safety net. User configs in BepInEx/config/ and world save data are never touched.

you@server $ ~/update_mods.sh