FreeBSD on Hetzner Cloud

Posted on Apr 17, 2026

So I like weird, Linux is cool and everything, but I’ve always gravitated to FreeBSD for personal projects. I like the fact the FreeBSD team build the entire OS from the kernel to the userland tools. It’s cohesive as a system and the handbook is unlike any other resource for configuring an OS. (Ok maybe the ArchWiki comes close)

I’m also rather fond of NixOS so the similarity of chucking config settings into /etc/rc.conf is also cool.

Anyway, rambling. FreeBSD provide a VM image with ZFS for root and BASIC-CLOUDINIT which I’ve yet to figure out. That’s for next time.

I adapted a borrowed Packer build for Talos that I was messing with on Hetzner for k8s a while ago. This build spins up a little instance, reboots it into the Hetzner rescue system then wget’s the FreeBSD arm/x86 image and dd’s it to the instances /dev/sda disk. It’ll then take a snapshot you can use to spin up Hetzner Cloud servers.

Setup & Build

You’ll need to add a valid HCLOUD_TOKEN to your env

export HCLOUD_TOKEN="blah"

Then init the packer build to pull down the hcloud plugin

packer init freebsd-hcloud.pkr.hcl 

To build both the ARM and x86 images simultaneously run

packer build freebsd-hcloud.pkr.hcl

To build just the ARM snapshot:

packer build -only hcloud.freebsd-arm freebsd-hcloud.pkr.hcl

freebsd-hcloud.pkr.hcl

packer {
  required_plugins {
    hcloud = {
      version = "~> v1.6.0"
      source  = "github.com/hetznercloud/hcloud"
    }
  }
}

variable "freebsd_version" {
  type    = string
  default = "15.0-RELEASE"
}

variable "image_url_arm" {
  type    = string
  default = null
}

variable "image_url_x86" {
  type    = string
  default = null
}

variable "server_location" {
  type    = string
  default = "hel1"
}

locals {
  timestamp = formatdate("YYYY-MM-DD-hh-mm", timestamp())

  image_arm = var.image_url_arm != null ? var.image_url_arm : "https://download.freebsd.org/releases/VM-IMAGES/${var.freebsd_version}/aarch64/Latest/FreeBSD-${var.freebsd_version}-arm64-aarch64-BASIC-CLOUDINIT-zfs.raw.xz"
  image_x86 = var.image_url_x86 != null ? var.image_url_x86 : "https://download.freebsd.org/releases/VM-IMAGES/${var.freebsd_version}/amd64/Latest/FreeBSD-${var.freebsd_version}-arm64-aarch64-BASIC-CLOUDINIT-zfs.raw.xz"

  # Add local variables for inline shell commands
  download_image = "wget --timeout=5 --waitretry=5 --tries=5 --retry-connrefused --inet4-only -O /tmp/freebsd.raw.xz "

  write_image = <<-EOT
    set -ex
    echo 'FreeBSD image loaded, writing to disk... '
    xz -d -c /tmp/freebsd.raw.xz | dd of=/dev/sda && sync
    echo 'done.'
  EOT

  clean_up = <<-EOT
    set -ex
    echo "Cleaning-up..."
    rm -rf /etc/ssh/ssh_host_*
  EOT
}

source "hcloud" "freebsd-arm" {

  rescue       = "linux64"
  image        = "debian-13"
  location     = "hel1"
  server_type  = "cax11"
  ssh_username = "root"

  snapshot_name = "FreeBSD ${var.freebsd_version} ARM - ${local.timestamp}"
  snapshot_labels = {
    os      = "freebsd",
    version = var.freebsd_version,
    arch    = "arm",
  }
}

source "hcloud" "freebsd-x86" {

  rescue       = "linux64"
  image        = "debian-13"
  location     = "hel1"
  server_type  = "cx22"
  ssh_username = "root"

  snapshot_name = "FreeBSD ${var.freebsd_version} x86 - ${local.timestamp}"
  snapshot_labels = {
    os      = "freebsd",
    version = var.freebsd_version,
    arch    = "x86",
  }
}

build {
  sources = [
    "source.hcloud.freebsd-arm",
    "source.hcloud.freebsd-x86",
  ]

  provisioner "shell" {
    inline = ["${local.download_image}${local.image_arm}"]
    only   = ["source.hcloud.freebsd-arm"]
  }

  provisioner "shell" {
    inline = ["${local.download_image}${local.image_x86}"]
    only   = ["source.hcloud.freebsd-x86"]
  }

  provisioner "shell" {
    inline = [local.write_image]
  }

  provisioner "shell" {
    inline = [local.clean_up]
  }
}

Terraform

To then use the snapshot created with packer here’s some example terraform

data "hcloud_image" "freebsd" {
  with_selector     = "os=freebsd"
  with_architecture = "arm"
  most_recent       = true
}

resource "hcloud_server" "this" {
  name = "freebsd"

  image       = data.hcloud_image.freebsd.id
  server_type = "cax11"
  location    = "hel1"

  public_net {
    ipv4_enabled = true
    ipv6_enabled = true
  }
  
  <snip>
}