valheim-server-docker: three download mirrors, one checksum, and an idle check before updating
Valheim dedicated gameserver with automatic update, World backup, BepInEx and ValheimPlus mod support
At a glance
- What is it?
- This is a container image that runs a dedicated game server and updates it automatically. The engineering worth reading is in the image build, where an external tool is fetched from one of three mirrors and then verified against a pinned hash, where the project's own shell scripts are linted and its own unit tests run during the build, and where an idle check stops the updater from restarting a game that people are in the middle of.
- Who is it for?
- valheim-server-docker is a good model if you are containerising something that a vendor distributes as a downloadable binary rather than as a package, because the patterns for mirroring, verifying, updating and backing up are all here and all documented with the failure modes attached.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 3 days ago.
- What is it written in?
- Mainly Shell, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 30, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Three mirrors and one checksum, which is the best thing in the repository
There is a loop in the image build that fetches an external tool, and it is worth reading line by line because it solves a problem most container builds do not.
The tool in question is a small multi-call binary that the runtime image needs. The build declares a version and a checksum as arguments, so both are visible in the build file and both are reviewable. Then it enters a loop over three base addresses: a build-system mirror, a distribution mirror, and the tool's own site. For each one it downloads with a retry count, a retry on any error rather than only on specific ones, an explicit connection timeout and an explicit overall time limit, and it breaks out of the loop as soon as one succeeds.
A minimal deployment is three volumes and three variables:
services:
valheim:
image: ghcr.io/community-valheim-tools/valheim-server
cap_add:
- sys_nice
restart: always
stop_grace_period: 2mThe reason for three addresses is availability, and it is a real problem. A single download URL in a container build is a single point of failure, and if that one host is slow, rate limited, or blocked on your network, your build fails for a reason that has nothing to do with your code. Having a fallback turns a build outage into a slower build.
The reason for the loop's shape is that a naive mirror fallback is a supply chain problem, and this one is not. After the loop, and outside it, there is a single checksum verification step against the pinned hash. Whatever mirror served the file, the hash has to match or the build stops. That is the whole point, and it is the part that is easy to get wrong. If the checksum were inside the loop, or if the fallback trusted the mirror, then whoever controls any one of three hosts could substitute a different binary and the other two would never be consulted.
Doing it the way this project does it means availability improves and integrity does not degrade. That is a small piece of build engineering and it is the sort of thing that is invisible until the day you need it, at which point you are extremely glad somebody wrote the loop.
The same pattern appears elsewhere in the build. A language runtime is installed by downloading an archive, extracting it and deleting the archive, with the version in a build argument. A supervisor is versioned in a build argument. A Python protocol library for querying server status is versioned in a build argument. Everything external is pinned at the point of use, which is the whole discipline in four lines.
A shell-heavy project that lints its shell and runs its own tests during the build
The primary language of this repository is a shell script, and the build environment installs a shell linter alongside the compiler toolchain. That is a small line in a package install and it is the correct one.
Shell is not a language with a type checker, so the substitutes a shell project can use are a linter that knows about the quoting and expansion mistakes shell is prone to, and tests. This project has both, and it puts them where they are hard to skip.
The tests are conditional. There is a build argument that decides whether a test run happens, and when it does, the build creates a separate virtual environment, installs a pinned test runner into it, and runs it against one specific component: the program that converts environment variables into the game's own configuration file. That component is small, it is the trust boundary between your deployment configuration and what the server actually reads, and it is exactly the kind of string manipulation that is easy to get subtly wrong. So it gets a test suite, it gets a pinned runner version, and it gets its own environment so its dependencies cannot collide with anything else in the build.
The same boundary logic explains the top-level layout. There is a directory for that converter, a directory for a log filter that is built as its own unit with its own build stage, a directory for hooks, and a script that appears to be a stub supervisor. Each of those is a component with a seam, which is what makes them testable and what makes the one component that is tested the one that most needed it.
Running tests during the image build rather than only in continuous integration is a deliberate choice with a real trade. It means a broken test cannot produce a published image, which is the point. It also means the build takes longer and needs the test dependency, which is why the switch exists, so that a release build can skip it once the tests have run in the pipeline. The dangerous version of this decision is skipping the tests and never running them elsewhere, and the existence of a separate test script at the top of the repository suggests that is not what happens.
There is also a separate script for pushing the development branch to the main one, which tells you the default branch is not where the work happens. For a project that publishes an image, that means the image is built from a branch, and a script that does the promotion is exactly what you need to keep it from being done by hand.
Two tools built from source, one of them with a checked-in applet list
The runtime image does not install a process supervisor or a shell utility collection from a package repository. It builds both from source, and the reason is visible in how the build is written.
The multi-call binary is configured before it is compiled, from a configuration file in the repository. That file decides which applets get compiled in. So the set of commands available in the runtime image is not whatever the tool's defaults happen to be; it is a list in a file that a reviewer can read, and a change to that list shows up as a diff in a pull request. For a container image, that is a meaningful attack surface reduction, because the default build of such a tool enables more than a minimal image needs, and every enabled applet is a program somebody can invoke.
The reason to build it at all rather than use a distribution package is the same. A base image changes under you. If you install a utility from a package repository, the version you get depends on when the base image was built and on that repository's current state. If you build it from a pinned source archive with a verified checksum, the binary in your image is the binary you reviewed.
The supervisor is versioned in a build argument the same way, and its presence is a design statement. Running a game server that must download an update, restart, take a backup and stay reachable involves several processes with ordering requirements and a shutdown grace period, and the readme's example sets a two-minute stop timeout in both the command line and the compose file. Doing that with a shell script and a trap handler is possible and unpleasant. A supervisor is the boring correct answer.
The two-minute stop timeout deserves a note of its own, because it is the kind of detail that only appears when somebody lost a world. A game server that is killed rather than asked to stop can leave a world in an inconsistent state, and the shutdown grace period is what gives it time to write. Its presence in both examples, and the environment variable documentation that distinguishes it from other options, suggests it was added in response to a problem.
The idle check, which is a separate script, is the other half of that concern. An automatic updater that restarts a game server every night will, eventually, restart it at two in the morning while six people are building a base. A script whose only job is to answer whether the server is currently busy is the minimum viable answer to that, and its presence at the top level next to the updater says the problem was considered.
Three separate updaters, and why the cache has to travel with the installation
The top level contains three updater components, and the reason there are three rather than one is the shape of the dependency chain.
One updates the game server itself, which is a downloadable binary distributed by a platform whose download tool the container has to invoke. One updates the mod framework, which is a third-party modification loader distributed separately from the game. One updates a specific mod. Each has its own release cadence and its own failure mode, so each gets its own updater and its own configuration. A single updater would be simpler and would break in three different ways.
The mod framework and the mod are the interesting pair, because they have a compatibility relationship with the game that neither has with the other. A mod framework that does not support the current game version will refuse to load, and a mod written for an older framework version will refuse to load. So the two updaters have to be ordered relative to the game updater, and a container that updates the game on a schedule and the framework on the same schedule will eventually put them out of step. The readme's separate sections for the two mod components, each with its own update and configuration documentation, are the visible consequence of treating them as independently versioned things.
Then there is the detail in the basic usage section that explains a real class of confusing failure. The directory holding the downloaded server also holds the download tool's manifest cache, and the readme explains that the cache is what allows the tool to update an existing installation after the game has been updated upstream. Without that cache the tool has no record of what is currently installed, and it cannot compute a delta. It re-downloads everything, or it fails.
The consequence for anyone deploying this is that the cache has to live alongside the installation it describes. If you mount the installation directory somewhere that is not persisted, you get a fresh download on every container start, which the readme mentions as an option to avoid, and if you persist it you keep the cache with it. Two directories, one for configuration and one for the server and its cache, mounted separately, and the readme says why each exists. That is the difference between a container that starts in ten seconds and one that takes several minutes and a gigabyte of bandwidth.
The readme also warns that a fresh start downloads about a gigabyte, and states the first-start cost plainly rather than leaving it to be discovered.
Configuration is all environment variables, converted by a program that has tests
The configuration model is worth understanding because it explains why there is a directory in the repository whose name is a converter.
Everything is an environment variable. The table of them is long, and the header says in capitals that all names and values are case sensitive. There is no configuration file to edit inside the container, no command line flags to remember, and no interactive setup. You set variables, the container translates them into whatever format the game server reads, and the server starts.
That translation is a program, and it is the component the build tests. Which is the right priority. An environment variable is a string; a game configuration file has types, sections and defaults; the conversion between them is where a trailing space, a missing default or a changed key name becomes a bug that shows up as a server that will not start. Testing the converter is testing the seam.
The case sensitivity warning is the kind of thing that belongs at the top of such a table and usually does not, and it is there because a container runtime treats variable names as opaque strings and will not tell you that a name is wrong. You get a default value instead of your value, and you spend an hour wondering why.
Two other details in the same area are worth naming. First, there is a documented minimum length for the server password, with the consequence spelled out: below it, the server binary refuses to start. That is a real gotcha that a user will hit on their first run, and it is documented rather than left as a crash.
Second, there is a second world-naming convention, one for each era of the game. For an existing world the name is the directory name inside the worlds folder, and for a world from before the first major release it is the file name with the database and world-file extensions stripped. So a container that has to work with a user's existing save has to support both conventions, and the readme explains both. That is the kind of requirement that only exists because somebody's world would not load.
The example compose file keeps the environment file outside the repository, pointing at a path in the user's home directory, and the repository ships an example file to copy from. So secrets do not live in the repository by default, which is the outcome you want from a project that stores a server password and a world you care about.
Five deployment targets and three NAS platforms documented from support tickets
The table of contents is generated by an editor plugin rather than written by hand, and it is a reasonable index of what the project actually supports. Reading it as a list of use cases is more informative than the feature summary.
There are five deployment targets. A service unit for a systemd host, a compose file, Kubernetes manifests, a task definition for a managed container service, and a job specification for a scheduler. Three of the five are checked into the repository as working examples, which is the right ratio: the three that most people use, and the two that are documented rather than shipped because their schemas change more often.
Then there are three NAS platforms with their own sections, and these are the most valuable documentation in the readme because of how they are written. Each has a heading that is the actual error message. One platform has a section for a permission-denied error, which in practice means the container runs as a user that the NAS's shared-folder permissions do not map, and the fix is a mapping change on the host rather than anything inside the container. Another has a section on a filesystem-specific issue, which is the kind of problem that appears on one platform because of how that platform's storage is layered, and is unfixable from inside a container. The third has a section for an error after downloading a new image, which is almost always stale cached layers on a NAS that does not prune aggressively.
Every one of those is a support ticket transcribed into a heading. That is the most honest documentation there is: it is written by the person who answered the question, it is searchable by the exact string the user saw, and it cannot drift from reality because it came from reality.
The same pattern shows up in the container usage section, where an optional capability is documented together with the warning message you get when you do not grant it. So a user who sees that line in their log can look it up rather than filing a question. And there is a section on changing the startup command in a graphical management interface, which exists because the most popular deployment path for this image is a NAS vendor's container app, and that interface does not let you pass flags.
The monitoring section, which covers a status page and integration with a self-hosted uptime service, is a smaller version of the same thinking. A game server that is down is a support question, and giving people a URL they can check turns most of those into non-questions.
Editorial conclusion
valheim-server-docker is a good model if you are containerising something that a vendor distributes as a downloadable binary rather than as a package, because the patterns for mirroring, verifying, updating and backing up are all here and all documented with the failure modes attached. It is a poor fit if you want a minimal container, since the image builds its own shell utilities and a process supervisor from source and carries a Go toolchain's worth of build steps, and a poor fit if you run multiple instances, since the port and world naming conventions assume one. Read the update section and the idle check before enabling automatic updates on a server people actually play on, keep your environment file outside the repository as the example compose file does, and pin the image by digest rather than by tag once you are past the first week.
Frequently asked questions
What does the valheim-server-docker image do?
It runs a dedicated server for a survival game in a container, downloading the server from the game's platform on first start, and it can update the game, a modification framework and a modification automatically. It also supports backups, a status page, and configuration entirely through environment variables.
How does the image build handle download mirrors without weakening integrity?
It loops over three base addresses with retries and timeouts, breaking on the first success, and then verifies the downloaded archive against a checksum pinned in the build file. The verification happens once, outside the loop, so any mirror can serve the file but a substituted file fails the hash check regardless of which host provided it.
What does the idle check in valheim-server-docker do?
It is a separate script that determines whether the server is currently busy, and the updater consults it. The purpose is to stop an automatic update from restarting the server while people are playing, which is the failure mode an unattended updater will eventually cause.
How is the container configured?
Entirely through environment variables, which are case sensitive. A program inside the image converts them into the game's own configuration file, and that converter is the component the build's test suite exercises, since it is the seam between your deployment configuration and what the server reads.
Why does the server directory need to be mounted if I want fast starts?
It holds both the downloaded server and the download tool's manifest cache, and the cache is what lets the tool update an existing installation after the game is updated upstream. Without it the tool has no record of the current installation and re-downloads roughly a gigabyte on every fresh start.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/community-valheim-tools-valheim-server-docker)