Valheim Dedicated Servers in Docker on Linux

What better time to learn some Docker, and what excuse than to check out the mistlands?

Valheim Dedicated Servers in Docker on Linux

I've kinda been itching for a good excuse to play around with Docker and learn the ropes a little bit. I'm also crazy about Valheim, which just released a big new content update that has been in the works for the past year. I set up a dedicated server for some friends on a spare Windows machine I have lying around. But in my mind, this sort of thing (running servers) feels best in Linux. So I'll be wiping Windows clear off of it and running a few dedicated servers off of Linux.

I have a few criteria specific to my situation that I'd like to accomplish with this setup.

  • I want to be able to spin up multiple servers concurrently.
  • I want trusted friends to be able to VPN into the network segment to manage the servers as needed. (This machine will run within its own VLAN to suit that purpose.)
  • And I'd like to automate a few things to make common administrative tasks quick and painless, even for friends who aren't savvy with command line.

Obviously not all of these thing will be applicable to everyone wanting to run a Valheim dedicated server. So if you've come across this page searching for a straightforward tutorial of Valheim dedicated server Docker images in Linux, I will still try to provide some helpful bits. I'll split up the article into two parts, covering my experience with getting the dedicated server up and running in Docker, then later we'll get into the criteria mentioned above.

Making backups

I migrated this server from Windows to Linux, so I made a backup of all the server files since I'll be wiping the operating system completely. I won't be needing all of the files, since it's going to run as a Docker image, but backed up the entire dedicated server file tree regardless.

If you have an existing world you want to copy over, first determine whether it is stored locally or in the cloud. Since patch 0.209.8, there's an option to use Steam's cloud storage. More information about their cloud storage update can be found in this Steam community post.

Worlds stored locally will be in %UserProfile%\AppData\LocalLow\IronGate\Valheim\worlds_local in Windows or ~/.config/unity3d/IronGate/Valheim in Linux.

Worlds saved on the cloud will be in C:\Program Files(x86)\Steam\userdata\NUMERICSTEAMID\892970\remote in Windows, or ~/.steam/steam/userdata/NUMERICSTEAMID/892970/remote in Linux.

Worlds from a previous dedicated server will be in C:\Program Files(x86)\Steam\steamapps\common\Valheim dedicated server\ in Windows, or ~/.config/unity3d/IronGate/Valheim/worlds in Linux.

Installing Linux

Now that I've got backups of everything, I'll wipe the OS and get a fresh one up and running. I'm going to go with a Linux Mint build. I've put together a nice comfy look and feel for it that I've grown accustomed to. I wrote a tutorial on putting it together if you're curious about the details.

Linux Mint with a Kali Themed Twist
Some good ol fashioned customization to make Linux Mint just the way I like it... Like Kali.

I'll make one note here about swap space. I ran into issues having a large swap partition using ZFS. When spinning up servers beyond what could be supported by RAM, the system would freeze. This was likely due to ZFS compressing the swap space, and running into problems when large amounts of swap space were being utilized by Docker for the containers. There may be ways around this issue, but I opted to reinstall on LVM instead. If RAM is a limitation of your hardware and you foresee the necessity of using a large swap partition from time to time, I'd recommend just using LVM instead of ZFS unless your have a good reason otherwise.

I do recommend setting up a large swap space if your RAM is limited. Swap size can be adjusted after-the-fact, but the method changes depending on if you're using traditional partitioning, LVM, or ZFS. My 8GB of RAM were pretty much entirely used upon initializing the first instance, and about 2-3GB of additional space were needed by each additional server after that.

Surtling raids are purdy.

Installing Docker

With the new OS up and running the way I like, it's time to grab Docker.

Install Docker Engine on Ubuntu
Instructions for installing Docker Engine on Ubuntu

I followed the instructions of the official Docker installation guide, but quickly ran into some issues specific to Linux Mint. If you aren't running Mint, the instructions in the link above should work fine for you.

I ran the provided commands to add the Docker repository to apt.

 curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null


However, when I ran sudo apt-get update I received the following error.

Ign:7 https://download.docker.com/linux/ubuntu vanessa InRelease
Err:8 https://download.docker.com/linux/ubuntu vanessa Release
  404  Not Found [IP: 54.230.21.60 443]
Reading package lists ... Done
E: The repository 'https://download.docker.com/linux/ubuntu vanessa Release does not have a Release file.
N: Updating from such a repository can't be done securely, and is therefore disabled by default.
N: See apt-secure(8) manpage for repository creation and user configuration details.

This issue is specific to Linux Mint 21, as lsb_release -cs in the command to write docker.list results in the output vanessa instead of jammy. Editing the docker.list file to change vanessa to jammy fixes the issue and allows sudo apt-get update to proceed as normal. Docker installed without a hitch and ran the hello-world container.

sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo docker run hello-world

Running the hello-world container is just a simple way to verify that the installation worked correctly.

I added my user to the docker group.

sudo usermod -aG docker $USER

A logout is recommended for this to take effect, followed by running docker -v to test that the permissions are working correctly.

With that working, I ultimately want a separate account for managing all things Valheim because I may want to run other servers on this machine in the future. I want an account that friends can use to run server maintenance when necessary, but I want to keep it separate from other services I may be running in the future. So I created a new user account, set things up the way I like them, and made sure to add the new user to the same docker group.

Setting up directories

I created folders in my new user's home directory for each of the Valheim servers I'd like to run. Each server needs a config and a data directory, and since I'll be using pre-existing worlds, a config/worlds/ folder. From the user's home directory, I ran:

mkdir -p valheim-server1/config/worlds valheim-server1/data
mkdir -p valheim-server2/config/worlds valheim-server2/data
mkdir -p valheim-server3/config/worlds valheim-server3/data

The -p argument tells mkdir to create parent directories if needed. Also, I didn't actually use this naming scheme, and opted for using the names of the worlds instead. Either way works fine. Just be sure to adjust any references as needed.

Migrating worlds

I copied the world data over from another computer with a USB stick. If you're starting a server with a fresh new world, you won't have to worry about this step. Otherwise, scoop up the backups you made earlier and find the world data you want to copy over.

You're going to want the .db and the .fwl files that coincide with the world name you want to copy over. Place the files for each separate world in the corresponding ~/valheim-server#/config/worlds directory.

We needed a lot of portals for our world.

Creating the initialization script

This can be done with a single command, but depending on what options you want to use for your server, it can get pretty unwieldy to write out. I wrote up a .sh script for each server so that I can edit the options and re-initialize as necessary.

#! /bin/sh
docker run -d \
    --name Docker_Server_Name \
    --cap-add=sys_nice \
    --stop-timeout 120 \
    -p '2456-2457:2456-2457/udp' \
    -p '9001:9001' \
    -v $HOME/Server_Folder/config:/config \
    -v $HOME/Server_Folder/data:/opt/valheim \
    -e SERVER_PORT="2456" \
    -e SERVER_NAME="Game_Server_Name" \
    -e WORLD_NAME="worldfilename" \
    -e SERVER_PASS="BestPasswordEver" \
    -e SERVER_PUBLIC="false" \
    -e RESTART_CRON="0 6 * * *" \
    -e TZ="America/Los_Angeles" \
    -e BEPINEX="true" \
    -e SUPERVISOR_HTTP="true" \
    -e SUPERVISOR_PASS="BestAdminPasswordEver" \
    -e ADMINLIST_IDS="00000000000000000" \
    -e BACKUPS_MAX_AGE="10000" \
    -e BACKUPS_MAX_COUNT="50" \
    -e BACKUPS_IF_IDLE="false" \
    -e BACKUPS_CRON="0 0 * * *" \
    -e BACKSUPS_IDLE_GRACE_PERIOD="86400" \
    lloesche/valheim-server

This is just an example configuration with a lot of details that will need to be altered in order to function properly. Here's basic rundown of why these settings are what they are.

The --name argument provides the reference name for the Docker container. This name can be used with Docker commands such as docker restart name or docker stop name, for example.

With cap-add, we add Linux capabilities to the container. The sys-nice value was provided by lloesche's documentation. More information about privileges and capabilities can be found here.

And stop-timeout sets the stop buffer time to give the container time to shut down properly. A maximum time is set to avoid indefinite hangs, but the default time of 10 seconds is insufficient for gracefully closing out all the components of the container.

Next up, we have port configurations with -p. The left hand side of the : is the outside, and right hand side is internal to the container. If running multiple instances, the internal port can remain default values with only the external port altered. For instance, -p '2456-2457:2456-2457/udp' for one instance and -p '2458-2459:2456:2457/udp' for another. The quotes surrounding the values are recommended by Docker's documentation, but not strictly necessary.

There is also a port mapping for 9001, which is the supervisor HTTP interface (enabled further down). HTTP is a TCP protocol, and TCP is Docker's default value in port mappings unless otherwise specified, so -p '9001:9001' suffices.

Then we'll also want to pass volume arguments to Docker to let it know where it can map certain internal data to external directories. This gives us little windows into the container's filesystem where we can place things like configuration files, world data, plugins, and have persistent storage of the build data.

Setting  the -v $HOME/Server_Folder/config:/config volume enables the container to find our world data (which should be located in ~/Server_Folder/config/worlds) and create any necessary configuration files. Backup data will also be stored in a folder at this location.

Setting the -v $HOME/Server_Folder/data:/opt/valheim volume comes in handy in the case of shutting an instance down, removing it from docker, and re-initializing it with a different configuration. Instead of having to pull down updates from scratch to initialize, it can pull directly from the mounted volume.

Now if you scroll back up and take a look at the initialization script again, you'll see that we have a whole bunch of -e arguments in a row. These are the environment variables that will be used to set configurations of the server.

The first few environment variables are fairly self explanatory. Others, not as much.

  • SERVER_PORT doesn't ever really need to change, as it can be adjusted through the container's port settings with -p.
  • SERVER_NAME is the name that can show up in-game.
  • WORLD_NAME is the name of the files.
  • SERVER_PASS needs to be a minimum of 5 characters.
  • SERVER_PUBLIC allows for server discovery through Steam. I recommend setting this false unless you genuinely want a public server, as it seems that populating the server list in-game checks connection against each potential server on the list resulting in excessive amounts of connections. Think of it as getting pinged a few dozen times a minute. It's not a huge load, but it's unnecessary unless you actually want to be on the list.
  • RESTART_CRON sets a restart timer using cron format. Daily restarts are useful for avoiding server errors and memory leaks that can build up over time. There is another environment variable RESTART_IF_IDLE which has a default value of true. So if players are currently connected during the scheduled restart time, it will skip the restart. I have my server set to restart at a time when people are not usually logged in, but allow the server to skip the restart (by leaving RESTART_IF_IDLE at default true) because missing a daily restart isn't as immediately awful as being caught in the middle of a build and losing progress. If you're unfamiliar with cron's format, there's a handy web tool for entering different values to see the results.
  • TZ sets the server's time zone using tz database names.
  • BEPINEX is a mod framework. If you aren't using any mods, this can just be omitted completely as the default value is false. If you are using mods, either BEPINEX or VALHEIM_PLUS should be set to true, but not both at the same time. On the first initialization, plugin folders will be created where you can drop in whatever mods you want to install. (Either config/bepinex/plugins or config/valheimplus/plugins depending on which you use.) After copying your mods to the location, you can restart the server to load them with docker restart container_name.
  • SUPERVISOR_HTTP and SUPERVISOR_PASS creates a simple web server and creates a password for it that allows you to manage certain functions and check log tails. I would not recommend opening this port to outside your LAN (for instance if you want friends to be able to restart the server) for security reasons. HTTP does not use encryption, and the password does not appear to actually be used even for restarting the server. Allowing external access to this feature will enable anyone on the wide open interwebs to check your logs (including snagging player IDs if present) and stop your server at will. I have actually removed this function from my own servers, but left it in this list as an excuse to mention the vulnerability. I will recommend other means of performing these same tasks instead of using this interface in the second part of this.
  • ADMINLIST_IDS passes space separated user IDs to the adminlist.txt file to give admin permissions to users on your server. User IDs can be pulled from in-game using the F2 overlay which displays all currently connected users and their corresponding ID. These IDs can also be pulled from the logs by grepping lines with "Got connection SteamID". Note that the adminlist uses SteamIDs and not in-game player IDs (ZDOID in log entries). Multiple IDs should be presented in the same "" block separated by a space. For example: "01234567890123456 12345678901234567".

Backup scheduling can be a bit of trick depending on your needs and your resources. In my case, I wanted it to mainly update when people are connected, and I have a fair amount of storage to throw at storing backups. Since we often go for months at a time without playing much, the BACKUPS_MAX_AGE is set to "10000" to effectively disable removal of older backups. Instead, I set BACKUPS_MAX_COUNT to store a total of 50. BACKUPS_IF_IDLE="false" makes the backup only run if players are connected, and BACKUPS_IDLE_GRACE_PERIOD="86400" (which is 24 hours in seconds) provides some wiggle room. So if a player has connected within the past 24 hours, the backup will run. BACKUPS_CRON defaults to every hour, but in my case I just want it to back up once a day at midnight so setting the value to "0 0 * * *" works fine.

It's also worth noting that these backups are a compressed archive of the entire world folder. The dedicated server itself makes autobackups on its own schedule, and this separate backup process makes a backup of all of those autosaves as well. For extra integrity, backups can be set to save to a custom location with BACKUPS_DIRECTORY. So backups can be saved directly to a separate drive, for instance. In my case, I will eventually be setting up my server with a RAID1 array to mirror the drive in case of failure.

Finally, the argument lloesche/valheim-server gives Docker the build we're going to use to create the container.

For a full list of environment variables, check lloesche's documentation. Configurations can get even more intricate and use things like log filters, event hooks, Discord webhooks, and configuring plugin variables, just to name a few.

Spin it up!

Once you have a configuration you want to try out saved, make sure to give executable permissions with chmod 744 server_init.sh. It can then be run with ./server_init.sh.

On the first run, the container will be downloaded and it will automatically run updates. Logs of the progress can be run with docker logs Docker_Server_Name. You also may have to open ports in UFW, but at this point you can test to make sure you can connect from another computer on your LAN.

If you find settings you need to change in the initialization, the container can be stopped, removed, and reinitialized.

docker stop container_name
docker rm container_name
./server_init.sh

This is where having the data/ volume mount comes in handy because reinitialization will require a whole lot less.

If your server is set up the way you like it and connections are working locally, it's time to set up port forwarding and firewall rules and have your friends try it out!


In the next part, I'll be going over my own network configuration to describe the other features I'd like to have on my server. While some of them are very specific to my own network configuration, there may be some useful things in there for anyone who'd like to set up remote administration in a more secure way using an OpenVPN server for remote tunnels, FTPS for file access, and SSH for shell access. Link will be added here once it's available.

As always, if you have any suggestions or corrections for me to take into account, please let me know!

Mastodon