Hetzner

Hetzner Cloud is a cloud hosting provider. Flatcar Container Linux is not installable as one of the default operating system options, but you can deploy it by installing it through the rescue OS relatively easily.

Quickstart

With Docker installed, run the below hcloud-upload-image command with your Hetzner Cloud API Token for the simplest possible Flatcar OS image on Hetzner.

docker run --rm -e HCLOUD_TOKEN="<Hetzner Cloud API Token>" \
  ghcr.io/apricote/hcloud-upload-image:latest upload \
  --architecture=x86 \
  --compression=bz2 \
  --description flatcar-stable-x86 \
  --image-url=https://stable.release.flatcar-linux.net/amd64-usr/current/flatcar_production_hetzner_image.bin.bz2

If you don’t want to use an Ignition file and just use the Hetzner SSH key, go to Servers > Snapshots > Create Server from snapshot in the Hetzner Console, choose the SSH key you want to use and enter the server with core@<Hetzner Server IP>.

Creating servers

Requirements

Make sure that your SSH Key is available in the current Hetzner Cloud project:

hcloud ssh-key list

# If not, you can upload the public key:
hcloud ssh-key create --public-key-from-file ~/.ssh/<your-ssh-key>.pub --name my-ssh-key

Server configuration

Flatcar allows you to configure machine parameters, launch systemd units on startup and more via Butane Configs . These configs are then transpiled into Ignition JSON configs and given to booting machines. We’re going to provide our Butane Config to Hetzner via the user-data flag.

The coreos-metadata.service saves metadata variables to /run/metadata/flatcar. Systemd units can use them with EnvironmentFile=/run/metadata/flatcar in the [Service] section when setting Requires=coreos-metadata.service and After=coreos-metadata.service in the [Unit] section.

As an example, this Butane YAML config will start an nginx Docker container and display the instance hostname:

variant: flatcar
version: 1.0.0

storage:
  directories:
    - path: /var/www
systemd:
  units:
    - name: nginx.service
      enabled: true
      contents: |
        [Unit]
        Description=NGINX example
        After=docker.service coreos-metadata.service
        Requires=docker.service coreos-metadata.service
        [Service]
        EnvironmentFile=/run/metadata/flatcar
        TimeoutStartSec=0
        ExecStartPre=-/usr/bin/docker rm --force nginx1
        ExecStartPre=-/usr/bin/bash -c "echo \"Hello from ${COREOS_HETZNER_HOSTNAME}\" > /var/www/index.html"
        ExecStart=/usr/bin/docker run --name nginx1 --volume "/var/www:/usr/share/nginx/html:ro" --pull always --log-driver=journald --net host docker.io/nginx:1
        ExecStop=/usr/bin/docker stop nginx1
        Restart=always
        RestartSec=5s
        [Install]
        WantedBy=multi-user.target

Before we can create the server, we need to transpile this Butane configuration to the Ignition format:

docker run --rm -i quay.io/coreos/butane:latest < nginx-example.yaml > nginx-example.json

Now that we have the snapshots, SSH Key and our Ignition config, we can finally create the first server:

# Get ID of the most recent flatcar snapshot for x86
SNAPSHOT_ID=$(hcloud image list --type=snapshot --selector=os=flatcar --architecture=x86 -o=columns=id -o noheader --sort=created:desc | head -n1)

hcloud server create \
  --name flatcar-test \
  --type cpx11 \
  --image ${SNAPSHOT_ID} \
  --ssh-key <your ssh key name or id> \
  --user-data-from-file nginx-example.json

This will also take a minute or two to load the snapshot. After the process is finished, you will see the following output:

Server 48081481 created
IPv4: 37.27.83.94
IPv6: 2a01:4f9:c012:52f1::1
IPv6 Network: 2a01:4f9:c012:52f1::/64

To verify that nginx was properly started, run curl $(hcloud server ip flatcar-test).

You can log in via ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null core@$(hcloud server ip flatcar-test).

Creating snapshots with hcloud-upload-image

Requirements

Building the snapshots

hcloud-upload-image does not know anything about Flatcar. We need to construct the URL for the image ourselves.

export HCLOUD_TOKEN=<your-token>
export CHANNEL=beta
# "current" is the latest version, you can specify alternative version here (e.g 3941.1.0)
export VERSION=current

# For x86 (cx, cpx & ccx Server Types)
hcloud-upload-image upload \
  --architecture=x86 \
  --compression=bz2 \
  --image-url=https://${CHANNEL}.release.flatcar-linux.net/amd64-usr/${VERSION}/flatcar_production_hetzner_image.bin.bz2 \
  --labels os=flatcar,flatcar-channel=${CHANNEL} \
  --description flatcar-${CHANNEL}-x86
 
# For Arm (cax Server Types)
hcloud-upload-image upload \
  --architecture=arm \
  --compression=bz2 \
  --image-url=https://${CHANNEL}.release.flatcar-linux.net/arm64-usr/${VERSION}/flatcar_production_hetzner_image.bin.bz2 \
  --labels os=flatcar,flatcar-channel=${CHANNEL} \
  --description flatcar-${CHANNEL}-arm

Running hcloud-upload-image upload will take a few minutes to complete. If you need x86 and Arm snapshots, you can run both in parallel.

After it completes, you should see the following output:

Successfully uploaded the image! image=167673693

You can verify this through the hcloud CLI:

$ hcloud image list --type=snapshot --selector=os=flatcar
ID          TYPE       NAME   DESCRIPTION        ARCHITECTURE   IMAGE SIZE
167673693   snapshot   -      flatcar-beta-x86   x86            0.47 GB
167673694   snapshot   -      flatcar-beta-arm   arm            0.41 GB

Creating snapshots with Packer

Requirements

Template

Packer requires a template that describes how the snapshots should be built. Create a new file flatcar.pkr.hcl and paste in the following content:

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

variable "channel" {
  type    = string
  default = "beta"
}

variable "hcloud_token" {
  type      = string
  default   = env("HCLOUD_TOKEN")
  sensitive = true
}

source "hcloud" "flatcar" {
  token = var.hcloud_token

  image    = "ubuntu-24.04"
  location = "fsn1"
  rescue   = "linux64"

  snapshot_labels = {
    os      = "flatcar"
    channel = var.channel
  }

  ssh_username = "root"
}

build {
  source "hcloud.flatcar" {
    name          = "x86"
    server_type   = "cx23"
    snapshot_name = "flatcar-${var.channel}-x86"
  }

  source "hcloud.flatcar" {
    name          = "arm"
    server_type   = "cax11"
    snapshot_name = "flatcar-${var.channel}-arm"
  }

  provisioner "shell" {
    inline = [
      # Download script and dependencies
      "apt-get -y install gawk",
      "curl -fsSLO --retry-delay 1 --retry 60 --retry-connrefused --retry-max-time 60 --connect-timeout 20 https://raw.githubusercontent.com/flatcar/init/flatcar-master/bin/flatcar-install",
      "chmod +x flatcar-install",

      # Install flatcar
      "./flatcar-install -s -o hetzner -C ${var.channel}",
    ]
  }
}

Building the snapshots

export HCLOUD_TOKEN=<your-token>
packer init .

# This will build the snapshot for x86 (amd64-usr) and Arm (arm64-usr).
packer build .

The packer build . command takes a few minutes to complete. Afterward you can see the snapshot names and IDs:

==> Builds finished. The artifacts of successful builds are:
--> hcloud.x86: A snapshot was created: 'flatcar-beta-x86' (ID: 157132241)
--> hcloud.arm: A snapshot was created: 'flatcar-beta-arm' (ID: 157132242)

You can verify these through the hcloud CLI:

$ hcloud image list --type=snapshot --selector=os=flatcar
ID          TYPE       NAME   DESCRIPTION        ARCHITECTURE   IMAGE SIZE
167650172   snapshot   -      flatcar-beta-arm   arm            0.41 GB
167650577   snapshot   -      flatcar-beta-x86   x86            0.47 GB

Extended template

If you are looking for an extended Packer template that allows some more customization, check out github.com/apricote/flatcar-packer-hcloud .

Known limitations

These Hetzner Cloud feature do not work with Flatcar:

  • Volume Automount: You need to mount volumes manually.
  • Setting & Resetting Root Passwords: You need to configure an SSH Key through the API or Ignition User Data.