8.4 KiB
Valheim Server StackScript
A Linode StackScript that turns a freshly created Debian 13 Linode into a Valheim dedicated server. Running it as a StackScript means everything happens as root during deployment, so no SSH session (and therefore no SSH key) is needed to configure the machine.
It does four things, in order:
- Installs Docker from Debian's own
docker.iopackage. - Mounts the pre-existing Valheim volume (this mirrors
voldirections.txt;mkfs.ext4is not run because the filesystem already exists). - Creates the persistent
config/anddata/directories on the volume. - Starts the Valheim server container with those directories bind-mounted, so worlds, configuration and backups survive destroying and recreating the Linode.
Instructions
- Log in to the Linode Cloud Manager and open StackScripts.
- Click Create StackScript.
- Set Label to something like
valheim-serverand choose Debian 13 (or "Any") as the Target Images. - Paste the code block below into the Script editor.
- Edit the server settings at the top of the script (name, world, password) before saving.
- Leave the UDF (User Defined Fields) section empty — the values are hard-coded, so nothing needs to be entered at deploy time.
- Click Save. The numeric ID of the StackScript appears in the URL and in the StackScript list; copy it.
- Put that ID in
create-linode.shas theSTACKSCRIPT_IDvalue near the top of the file.
After that, create-linode.sh c <label> passes the StackScript on the create
call and the Linode comes up as a running Valheim server with no further steps.
Where things live on the volume
Everything the container needs to keep is under /mnt/ValVol202609/valheim:
| Host path | Container path | Contents |
|---|---|---|
/mnt/ValVol202609/valheim/config |
/config |
World saves, adminlist.txt, bannedlist.txt, permittedlist.txt, backups/ |
/mnt/ValVol202609/valheim/data |
/opt/valheim |
The ~1 GB downloaded server plus SteamCMD's depot cache |
Mounting /opt/valheim as well means a newly created Linode does not have to
re-download the server from Steam, which saves several minutes per instance.
The depot cache lives inside that same directory so it stays in sync with the
installation it belongs to.
Worlds are stored in config/worlds_local/<WORLD_NAME>/ (older releases used a
single <WORLD_NAME>.db/.fwl pair directly in config/worlds/). To bring an
existing world along, copy it into the worlds_local directory on the volume
and set WORLD_NAME to match.
Notes
- Ports. The container publishes
2456-2457/udp(game and query). Linodes have no inbound firewall by default, so nothing else is required. If you add a Cloud Firewall, allow those two UDP ports. - Sizing. The upstream project recommends a high-clocked 4 core / 8 GB
machine, which is exactly
g6-standard-4— the typecreate-linode.shuses. - First start. The container downloads ~1 GB from Steam on first boot and takes several minutes to become joinable.
- Admin. Once the server has started,
config/adminlist.txtexists on the volume. Put your SteamID64 in it to get in-game admin commands. - Backups. The container writes hourly world backups to
config/backups/on the volume and prunes them after 3 days. - Updates. The container checks for Valheim updates every 15 minutes and restarts the server when one is found, but only while nobody is connected.
- Re-running. The script is safe to run again: the mount and the
fstabentry are only created if missing, and any previous container is removed before the new one is started.
Sensitive data
- There is a secrets file in gitignore
- The IDs for all linode API calls are linked to my account, but non-sensitive.
- The passwords are placeholders, edited when setting up StackScripts
- Repo can be made public, as no git snapshot had sensitive data.
StackScript
#!/bin/bash
# Linode StackScript: turn a fresh Debian 13 Linode into a Valheim dedicated
# server whose worlds, configuration and backups live on the pre-existing
# Valheim volume. Runs as root during deployment; no SSH access required.
set -euo pipefail
# ---------------------------------------------------------------------------
# Server settings - edit these before saving the StackScript.
# ---------------------------------------------------------------------------
SERVER_NAME="Valheim Server"
WORLD_NAME="Dedicated"
# Must be at least 5 characters, otherwise valheim_server.x86_64 refuses to
# start. Anything saved here is readable by anyone with access to the
# StackScript, so change it.
SERVER_PASS="changeme123"
# true lists the server in the in-game community browser.
SERVER_PUBLIC="true"
# ---------------------------------------------------------------------------
# Volume and container settings.
# ---------------------------------------------------------------------------
VOLUME_NAME="ValVol202609"
VOLUME_DEVICE="/dev/disk/by-id/scsi-0Linode_Volume_${VOLUME_NAME}"
VOLUME_MOUNT="/mnt/${VOLUME_NAME}"
# Everything the container has to keep across Linode rebuilds lives here.
VALHEIM_DIR="${VOLUME_MOUNT}/valheim"
CONFIG_DIR="${VALHEIM_DIR}/config"
DATA_DIR="${VALHEIM_DIR}/data"
CONTAINER_NAME="valheim-server"
# The upstream project moved to ghcr.io/community-valheim-tools/valheim-server.
# The Docker Hub image below is still published and can be swapped for it.
IMAGE="lloesche/valheim-server"
# ---------------------------------------------------------------------------
# 1. Install Docker.
# Debian's docker.io package is enough for a single `docker run` and avoids
# adding a third-party apt repository. ca-certificates is listed explicitly
# because --no-install-recommends would otherwise skip it and image pulls
# would fail TLS verification.
# ---------------------------------------------------------------------------
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y --no-install-recommends docker.io ca-certificates docker-cli
systemctl enable --now docker
# ---------------------------------------------------------------------------
# 2. Mount the Valheim volume.
# The volume is attached over the Linode API once the instance is running, so
# wait for the device to appear. 60 attempts x 5s = 5 minute ceiling.
# ---------------------------------------------------------------------------
for _ in $(seq 1 60); do
if [ -e "$VOLUME_DEVICE" ]; then
break
fi
echo "Waiting for ${VOLUME_DEVICE} to appear..."
sleep 5
done
if [ ! -e "$VOLUME_DEVICE" ]; then
echo "ERROR: ${VOLUME_DEVICE} never appeared. Aborting so that the world is not written to the instance's ephemeral disk." >&2
exit 1
fi
mkdir -p "$VOLUME_MOUNT"
if ! mountpoint -q "$VOLUME_MOUNT"; then
mount "$VOLUME_DEVICE" "$VOLUME_MOUNT"
fi
# Mount on every boot, without duplicating the line on re-runs. nofail keeps a
# missing volume from blocking boot; if the volume is not there the mountpoint
# silently becomes a directory on the instance's own disk, which is why the
# check above aborts rather than continuing.
if ! grep -q "$VOLUME_DEVICE" /etc/fstab; then
echo "${VOLUME_DEVICE} ${VOLUME_MOUNT} ext4 defaults,noatime,nofail 0 2" >> /etc/fstab
fi
# ---------------------------------------------------------------------------
# 3. Create the persistent directories on the volume.
# ---------------------------------------------------------------------------
mkdir -p "$CONFIG_DIR" "$DATA_DIR"
# ---------------------------------------------------------------------------
# 4. Start the Valheim server.
# --restart unless-stopped brings the server back after a reboot.
# --stop-timeout 120 gives the server time to save the world on shutdown.
# --cap-add=sys_nice lets the Steam library raise its own thread priority.
# ---------------------------------------------------------------------------
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
docker run -d \
--name "$CONTAINER_NAME" \
--cap-add=sys_nice \
--stop-timeout 120 \
--restart unless-stopped \
-p 2456-2457:2456-2457/udp \
-v "${CONFIG_DIR}:/config" \
-v "${DATA_DIR}:/opt/valheim" \
-e SERVER_NAME="$SERVER_NAME" \
-e WORLD_NAME="$WORLD_NAME" \
-e SERVER_PASS="$SERVER_PASS" \
-e SERVER_PUBLIC="$SERVER_PUBLIC" \
-e SERVER_ARGS="-modifier resources more" \
"$IMAGE"
echo "Valheim server started."
echo "The first start downloads ~1GB from Steam and takes several minutes."
echo "Follow progress with: docker logs -f $CONTAINER_NAME"