How to host a Valheim server on Apple Silicon

How to host a Valheim server on Apple Silicon

Valheim is a fun (and addictive) game about Vikings, building stuff, and killing bosses. Playing it alone is decent, but it’s most fun when playing with your friends. The best way to do this is to host your own server. There are many options, but using your own hardware is the best and least expensive option. I’ve found that in both performance-to-watt and price-to-performance, the M1 Mac mini makes for an incredible server. In fact, I’m using one to host this site as well as my own Valheim server. Trying to host it on Apple Silicon took a bit of research, however, since as of writing Valheim (and Valheim server) only natively supports x86 processors. Fortunately, Apple Silicon is extremely efficient at emulating x86, and you can host a Valheim server with no hiccups even on the base model 8GB M1 Mac mini. Let’s walk through how to do it!

Table of Contents

Necessary software

Configure your Valheim VM and container environment

Before starting your VM instance, you’ll need to configure your VM and container manifests. First, create a project folder:

mkdir -p $HOME/valheim-configs
cd $HOME/valheim-configs

Set up Lima VM

Lima VM is the best option I’ve found for using “slow” QEMU emulation, which fully emulates x86 architecture instead of partial “fast” emulation that you may find using something like Docker Desktop, Parallels, or multipass. Lima is also much more lightweight than VMs such as UTM or VirtualBox.

To install Lima, first you must have Homebrew. Follow the installation instructions on the Homebrew site to install brew, then run brew install lima. After installation finishes, install the included socket_vmnet service, which will automatically forward ports from the VM through the host, by running brew install socket_vmnet. Now you’re ready to configure the VM manifest file.

Lima config

Create the following .yaml manifest, titled valheim-server.yaml:

# full QEMU emulation
arch: 'x86_64'
# must be minimum 50GB
disk: 50GiB
# amount needed depends on player count
# generally half of what Intel needs
# recommend at least 4GiB
memory: 6GiB
# recommend at least 3 cpus
cpus: 4

# bridged creates separate IP on your LAN with the hostname "lima-<vm-name>"
networks:
- lima: bridged

# To run `docker` on the host (assumes docker-cli is installed):
# $ export DOCKER_HOST=$(limactl list docker --format 'unix://{{.Dir}}/sock/docker.sock')
# $ docker ...

# This example requires Lima v0.8.0 or later
images:
# Try to use release-yyyyMMdd image if available. Note that release-yyyyMMdd will be removed after several months.
- location: "https://cloud-images.ubuntu.com/releases/22.04/release-20221201/ubuntu-22.04-server-cloudimg-amd64.img"
  arch: "x86_64"
  digest: "sha256:8a814737df484d9e2f4cb2c04c91629aea2fced6799fc36f77376f0da91dba65"
- location: "https://cloud-images.ubuntu.com/releases/22.04/release-20221201/ubuntu-22.04-server-cloudimg-arm64.img"
  arch: "aarch64"
  digest: "sha256:8a0477adcbdadefd58ae5c0625b53bbe618aedfe69983b824da8d02be0a8c961"
# Fallback to the latest release image.
# Hint: run `limactl prune` to invalidate the cache
- location: "https://cloud-images.ubuntu.com/releases/22.04/release/ubuntu-22.04-server-cloudimg-amd64.img"
  arch: "x86_64"
- location: "https://cloud-images.ubuntu.com/releases/22.04/release/ubuntu-22.04-server-cloudimg-arm64.img"
  arch: "aarch64"

mounts:
- location: "~"
- location: "/tmp/lima"
  writable: true
# containerd is managed by Docker, not by Lima, so the values are set to false here.
containerd:
  system: false
  user: false
provision:
- mode: system
  # This script defines the host.docker.internal hostname when hostResolver is disabled.
  # It is also needed for lima 0.8.2 and earlier, which does not support hostResolver.hosts.
  # Names defined in /etc/hosts inside the VM are not resolved inside containers when
  # using the hostResolver; use hostResolver.hosts instead (requires lima 0.8.3 or later).
  script: |
    #!/bin/sh
    sed -i 's/host.lima.internal.*/host.lima.internal host.docker.internal/' /etc/hosts
- mode: system
  script: |
    #!/bin/bash
    set -eux -o pipefail
    command -v docker >/dev/null 2>&1 && exit 0
    export DEBIAN_FRONTEND=noninteractive
    curl -fsSL https://get.docker.com | sh
    # NOTE: you may remove the lines below, if you prefer to use rootful docker, not rootless
    systemctl disable --now docker
    apt-get install -y uidmap dbus-user-session
- mode: user
  script: |
    #!/bin/bash
    set -eux -o pipefail
    systemctl --user start dbus
    dockerd-rootless-setuptool.sh install
    docker context use rootless
probes:
- script: |
    #!/bin/bash
    set -eux -o pipefail
    if ! timeout 30s bash -c "until command -v docker >/dev/null 2>&1; do sleep 3; done"; then
      echo >&2 "docker is not installed yet"
      exit 1
    fi
    if ! timeout 30s bash -c "until pgrep rootlesskit; do sleep 3; done"; then
      echo >&2 "rootlesskit (used by rootless docker) is not running"
      exit 1
    fi
  hint: See "/var/log/cloud-init-output.log". in the guest
hostResolver:
  # hostResolver.hosts requires lima 0.8.3 or later. Names defined here will also
  # resolve inside containers, and not just inside the VM itself.
  hosts:
    host.docker.internal: host.lima.internal
portForwards:
- guestSocket: "/run/user/{{.UID}}/docker.sock"
  hostSocket: "{{.Dir}}/sock/docker.sock"
message: |
  To run `docker` on the host (assumes docker-cli is installed), run the following commands:
  ------
  docker context create lima-{{.Name}} --docker "host=unix://{{.Dir}}/sock/docker.sock"
  docker context use lima-{{.Name}}
  docker run hello-world
  ------

Container config

Now create a docker compose manifest titled docker-compose.yaml:

version: "3"


services: 
  valheim: 
    image: ghcr.io/lloesche/valheim-server
    cap_add:
      - sys_nice
    volumes: 
      - $HOME/valheim-server/config:/config
      - $HOME/valheim-server/data:/opt/valheim
      - /Users/$USER/.ssh/id_ecdsa_valheim:/root/.ssh/id_ecdsa:ro
      - /Users/$USER/.ssh/known_hosts:/root/.ssh/known_hosts
    ports: 
      - "2456-2457:2456-2457/udp"
    env_file:
      - ./valheim.env
    restart: always
    stop_grace_period: 2m

And to accompany, create a valheim.env file to reference:

SERVER_NAME=ServerName
WORLD_NAME=WorldName
SERVER_PASS=securepassword
SERVER_PUBLIC=true
TZ='US/Central'
POST_BACKUP_HOOK='timeout 300 cp @[email protected] $HOME/backups/$(basename @[email protected])'

Be sure to change the TZ var to your time zone. Things like auto-restart and auto-update rely on knowing the right time to perform certain actions so they’ll be done early in the morning rather than mid-day.

The POST_BACKUP_HOOK var can be used to copy your world backups to a different location after a local backup is made. I’d recommend copying them to either your host machine via scp or a remote file server to have for extra redundancy.

If you want to copy your backup back to your host machine, I’ve already added a volume reference to /Users/$USER/.ssh/id_ecdsa_valheim in the docker compose file. To create this file, run:

ssh-keygen -t ecdsa -f /Users/$USER/.ssh/id_ecdsa_valheim -N ''

Now you have an SSH key for securely communicating between your VM and host machine without needing a password.

Deploy your Valheim server container

Initialise your Lima VM with:

# you can add this export to your .zshrc file as well
# otherwise it reverts to 'default' on shell restart
export LIMA_INSTANCE='valheim-server' &&
limactl start --name=$LIMA_INSTANCE $HOME/valheim-server/valheim-server.yaml

Once your VM has finished initialising, enter the VM via:

# alias for `limactl shell $LIMA_INSTANCE`
lima 

Now you’ll need to copy over your docker-compose and env files.

mkdir -p ~/valheim-server &&
cd ~/valheim-server &&
cp /Users/$USER/valheim-configs/docker-compose.yaml . &&
cp /Users/$USER/valheim-configs/valheim.env . &&
# start docker container
docker compose up -d --remove-orphans

Starting the container will create two folders in your ~/valheim-server/ directory: config/ and data/. These are where all of your Valheim server and world info is stored, so don’t delete them!

Now you can view the server logs with:

docker logs -f valheim-server-valheim-1

Now you should see the server logs live, and will see the line supervisord: valheim-server DEBUG - [125] - Waiting for server to listen on UDP port 2456 throughout the logs for the first 2 minutes after starting the server. Once you see a message stating supervisord: valheim-server DEBUG - [125] - Server is now listening on UDP port 2456, your server will be ready to join. Before you’re able to join from the Internet, however, there’s one final step of forwarding your ports.

Access your server from the Internet

Forward ports

Now that your Lima instance is up and running, a new IP will show up on your LAN with a host name like lima-valheim-server. Forward the necessary UDP ports from that IP (default 2456-2457/udp) on your router. Since each router has a different way of doing this, I can’t post specific instructions here, so refer to the documentation for your router.

Check server status and logs

The easiest way to see if your ports are properly forwarded and the server is working is to use https://geekstrom.de/valheim/check/, but this does require that your server is public. Once you see a status like below, your server is ready to go!

Join your server

Now you and your friends are ready to start up your game and play!

After starting your game, select “Start Game,” create or select your character, then go to the “Join Game” tab and select “Add Server.”

Put in your sever name, press Return, then select “Connect” under the “Add server” button.

It should take a few seconds until an “Enter Password” prompt appears. This will be the password you set in valheim.env.

After you enter your password, your world will load and you’re ready to explore!

Conclusion

Now you can go have fun with your friends in your own Valheim server! Cheers to many buildings built and many bosses killed in the journey to Valhalla.

Apple Silicon offers an incredibly powerful and efficient architecture for running a server. The level of sustained performance I’ve been able to get out of just an 8GB M1 Mac mini is impressive, especially at such a small energy footprint. I’m looking forward for what’s next, especially for the use of scaling up my homelab.

Troubleshooting

Server says it’s listening on port but it’s still inaccessible

  • Be sure your ports are open on your router, and that you’re forwarding ports from the guest IP (lima-.lan) and not your host IP.
  • Stop the server, run brew reinstall socket_vmnet, and start the server again. Sometimes the socket_vmnet service seems to not be properly initialised.
  • Relatedly, make sure to not run socket_vmnet as a service on your host machine. The brew install gives directions to do this, but don’t do it. Lima starts its own socket_vmnet service when starting the VM.
  • Make sure you have at least 50GB free space for your VM. Not sure why this is a requirement, but anything under that seems to prevent the server from working properly.

Container error mentioning incorrect architecture

  • Be sure you’ve included arch: x86_64 in your lima config file. Otherwise it defaults to “fast” QEMU emulation, where we want “slow” (full) emulation.

Anything else

Open an issue/PR for lloesche/valheim-server or lima-vm/lima respectively.