Get Rewarded! We will reward you with up to €50 credit on your account for every tutorial that you write and we publish!

Installing Single-Node OpenShift on a Hetzner Dedicated Server

profile picture
Author
Tomaž Borštnar
Published
2026-11-05
Time to read
18 minutes reading time

About the author- Tomaž Borštnar is an Expert IT Architect and Trainer specializing in Containers, OpenShift, and Kubernetes, focused on driving innovation in software, cloud, and network engineering. Based in Slovenia, his technical coverage spans Containers, OpenShift, Kubernetes, Linux, Networking, DevOps, RHEL, Fedora, Ubuntu and many others.

Introduction

Hetzner dedicated servers are a good place to run a single-node OpenShift (SNO) lab. You get plenty of CPU, memory, and fast local NVMe storage, and the monthly cost is predictable. The awkward part is the install. On a dedicated server in Robot, you do not get the same easy ISO boot workflow you have with a VM or with Hetzner Cloud. OpenShift normally expects either a discovery ISO or PXE boot, and neither is especially convenient here.

The approach in this tutorial is to use the Hetzner Rescue System as a temporary working environment. Once the server is in rescue mode, you can build the OpenShift Agent-based Installer PXE artifacts there and then use kexec to jump straight into the Red Hat CoreOS (RHCOS) installer kernel. That avoids extra manual boot steps and works well for this kind of one-node setup.

I cover the manual path first and keep every command in the article so you can see exactly what is happening. There is also an optional shortcut later if you want to automate the preparation steps. What you get out of this:

  • A running single-node OpenShift cluster on one Hetzner dedicated server.
  • A kubeconfig and kubeadmin password on your workstation so that you can manage it remotely.
  • A Robot firewall in front of the node so it isn't wide open to the internet the moment it boots.

It is best suited for labs, testing, or edge use where you understand the risks. A single-node OpenShift is a single point of failure. Also, kexec does not always behave the same way as a full reboot on every hardware model. If the server does not boot cleanly into RHCOS, use a KVM console to see what is happening. If you need a supported installation path, use the Agent-based Installer with standard boot media.

Prerequisites

  • A Hetzner dedicated server you can put into the Rescue System. Servers like EX44-1 and AX41-1 are good choices for OpenShift SNO because they have enough CPU, memory, and local storage. This method was developed and tested on an EX44. Red Hat documents SNO minimums of 8 vCPUs, 16 GB RAM, and 120 GB disk, but in practice you will want more memory and disk capacity. Any modern Hetzner dedicated server should clear those requirements easily. SNO on bare metal uses the platform: none profile and OVN-Kubernetes by default. (Source: Red Hat — Preparing to install on a single node, 4.20.)
  • A Red Hat Account. You can register for free at redhat.com/wapps/ugc/register.html.
  • A Red Hat pull secret from the Red Hat console. A free Red Hat Developer subscription is enough for a lab.
  • An SSH key pair. The public key is baked into the install so you can later log in to the node as core.
  • OpenShift 4.14.0 or newer (recommended: latest GA version of 4.21 or 4.22). This workflow relies on openshift-install agent create pxe-files, which Red Hat documents from 4.14 onward. While any version from 4.14.0 works, using the latest GA release ensures you get recent fixes and features.
  • Comfortable with SSH, the Hetzner Robot panel, YAML, and oc/kubectl.
  • No wildcard DNS entry for your domain (e.g. *.example.com), otherwise the DNS wildcard validation might fail during the installation process.

Example terminology

This tutorial uses the following example values — replace them everywhere they appear:

Placeholder Example value Meaning
<server-ip> 203.0.113.45 Public IPv4 of your dedicated server
<prefix> 26 Netmask prefix length of the server subnet
<gateway> 203.0.113.1 Default gateway
<interface> enp41s0 Primary NIC name in rescue
<mac> a8:a1:59:b3:c2:01 MAC address of that NIC
<dns> 192.0.2.53 DNS resolver (Hetzner's are fine)
<disk-serial> S63CNF0X212063 Serial of the install disk
<your-admin-ip> 203.0.113.10/32 The IP/CIDR you administer from
<base-domain> example.com Cluster base domain
<cluster-name> sno Cluster name (API → api.sno.example.com)
<ocp-version> 4.22.1 OpenShift release (use latest GA 4.21 or 4.22)

<cluster-name> and <base-domain> are separate fields that OpenShift stitches together: cluster name sno + base domain example.com gives an API endpoint at api.sno.example.com. For a lab you don't strictly need public DNS, but if you want to reach the console by name from elsewhere, point api.sno.example.com and a wildcard *.apps.sno.example.com at the server's IP.

Step 1 - Lock down the Robot firewall first

Do this before you ever boot the installer. The moment kexec hands control to RHCOS, the node is live on a public IP and OpenShift opens a fair number of ports.

The Hetzner Robot firewall is a free, stateless packet filter at the switch port. Stateless matters: it doesn't track connection state, so return traffic for connections the server initiates (image pulls from quay.io, Red Hat API calls, DNS, NTP) must be allowed explicitly. Rules are top-to-bottom, first match wins, no match means drop.

Go to https://robot.hetzner.com/server, pick your server, open the Firewall tab, and enable two global options: Filter IPv6 packets (SNO here is IPv4-only) and Hetzner Services (lets Hetzner's own infrastructure through).

For a single-admin lab, this rule set works.

Outgoing:

# Name Protocol Action
1 Allow all outgoing * accept

Incoming:

Run curl -4 https://ip.hetzner.com on your local machine to get your admin IP.

# Name Source IP Dst Port Protocol TCP Flags Action
1 Trusted admin <your-admin-ip> * accept
2 ICMP icmp accept
3 TCP established 32768-65535 tcp ack accept
4 UDP responses 32768-65535 udp accept

Rule 1 lets your workstation reach everything (SSH 22, API 6443, console 443). Rules 3 and 4 are the stateless workaround — they accept the ACK and ephemeral-port return packets for outbound connections. Without them, image pulls can hang and the install can stall. You get 10 incoming rules total, and each trusted IP uses one. The firewall usually takes about 20-40 seconds to apply after you save.

Step 2 - Boot the Rescue System and wipe the disks

In the Robot panel, on the server's Rescue tab, enable the Linux rescue system (64-bit), then reboot so the server comes up into rescue. Rescue is Debian 12, and DHCP is already working — if you can SSH in, the network is fine.

ssh root@<server-ip>

The install disk must be clean. List your disks, then wipe the target:

Warning: This is destructive — be sure

lsblk -o NAME,SIZE,TYPE,MODEL,SERIAL
wipefs -a /dev/nvme0n1

Note the serial number of your install disk. On multi-disk servers you'll pin the install by serial, because NVMe kernel names (nvme0n1, nvme1n1…) can reorder across reboots while the serial doesn't.

Note that some systems may use different names (sda, sdb,...).


From here, you have two ways to build and boot the installer. Steps 3 through 8 below show the manual process. If you prefer, you can use the optional shortcut and then continue at Step 9.

Shortcut: automate Steps 3-8 with helper scripts (optional)

If you do not want to do every preparation step manually, the community project tomazb/hetzner-sno-provision-host automates the same flow shown below. It detects the network settings, downloads and verifies oc and openshift-install, builds nmstatectl, generates the configs, creates the PXE files, and runs the padded-initrd kexec.

Same caveats apply: experimental, unsupported, lab use only. You're trusting a third-party repository to run as root in your rescue environment — read the scripts first.

On the rescue host:

Save the Red Hat pull secret in /root/pull-secret.json and store the public SSH key of your workstation in a file such as /root/.ssh/workstation.pub. Then continue with the commands below.

git clone https://github.com/tomazb/hetzner-sno-provision-host.git
cd hetzner-sno-provision-host

# Dry-run first: prints the detected interface, IP, gateway, DNS, and disk
# without installing or writing anything.
./hetzner-sno-prepare-pxe.sh --dry-run \
  --disk-serial <disk-serial> \
  <ocp-version> /root/pull-secret.json <base-domain> <cluster-name>

# Real run (interactive): prompts for anything it can't detect, then builds
# the PXE artifacts into /root and prints the kubeadmin password + kubeconfig.
./hetzner-sno-prepare-pxe.sh --interactive --disk-serial <disk-serial>

It auto-discovers pull-secret.* files and *.pub SSH keys under $HOME, so putting them in /root first is the simplest approach. Save the kubeadmin password and kubeconfig it prints. The next command replaces the rescue environment, so those files will be lost unless you copy them out first. Then boot:

./hetzner-sno-provision-host-agentbased.sh --artifact-dir /root

Your SSH session drops on kexec — that's expected. Skip ahead to Step 9.

Optional — reserve boot-disk space for local storage: add --csi-reserve-size 500G to the prepare command to carve a raw, unformatted partition out of the boot disk for LVMS or another CSI operator. After install it appears at /dev/disk/by-partlabel/openshift-csi. This requires pinning the disk by serial. The script only creates the partition — you point your storage operator at it afterward.


Step 3 - Collect the network facts you'll need

The Agent-based Installer wants an exact description of the node's network. Grab the values from the live rescue session so you don't guess. Interface, IPv4/prefix, and gateway:

ip -brief addr show
ip route show default

The MAC address of that interface:

cat /sys/class/net/<interface>/address

And the resolvers (or just use your provider's resolvers — example values: 192.0.2.53 and 192.0.2.54):

grep nameserver /etc/resolv.conf

Write these down. They go into the YAML in Step 5.

Run the following cat command to get information about your machine in list form:

cat <<EOF
---------------------------------------
Interface:                $(ip route get 1.1.1.1 | awk '{for(i=1;i<=NF;i++) if($i=="dev") print $(i+1)}')
IP/prefix:                $(ip -o -4 addr show $(ip route get 1.1.1.1 | awk '{for(i=1;i<=NF;i++) if($i=="dev") print $(i+1)}') | awk '{print $4}')
Rendezvous IP:            $(curl -4 -s https://ip.hetzner.com)
Gateway:                  $(ip route | awk '/default/ {print $3}')
Machine network:          $(python3 -c 'import ipaddress,subprocess; ip=subprocess.check_output("ip -o -4 addr show $(ip route get 1.1.1.1 | awk '\''{for(i=1;i<=NF;i++)if($i==\"dev\")print $(i+1)}'\'') | awk '\''{print $4}'\''",shell=True,text=True).strip(); print(ipaddress.ip_interface(ip).network)')
MAC:                      $(ip link show "$(ip route get 1.1.1.1 | awk '{for(i=1;i<=NF;i++) if($i=="dev") print $(i+1)}')" | awk '/link\/ether/ {print $2}')

DNS servers:
$(resolvectl status | grep -oE '([0-9a-fA-F]{0,4}:){2,}[0-9a-fA-F:.]+|([0-9]{1,3}\.){3}[0-9]{1,3}' | sort -u)

Disks and their serial:
$(lsblk -ndo NAME,SERIAL | awk '$2 != "" {print "/dev/"$1, $2}')
EOF

Step 4 - Install the toolchain in rescue

You need three things on the rescue host: the OpenShift binaries (oc, openshift-install), nmstatectl (the installer validates the network YAML with it), and kexec-tools.

Download and verify oc and openshift-install

Pull the version-matched binaries from the Red Hat mirror and check them against the published checksums — don't skip the verification:

See latest version

cd /root
VER=<ocp-version>
BASE=https://mirror.openshift.com/pub/openshift-v4/x86_64/clients/ocp/${VER}

curl -fL -O ${BASE}/openshift-client-linux.tar.gz
curl -fL -O ${BASE}/openshift-install-linux.tar.gz
curl -fL -O ${BASE}/sha256sum.txt

sha256sum --ignore-missing --check sha256sum.txt

tar -xzf openshift-client-linux.tar.gz -C /usr/local/bin oc kubectl
tar -xzf openshift-install-linux.tar.gz -C /usr/local/bin openshift-install
oc version --client
openshift-install version

Install nmstatectl (the tricky bit on Debian 12)

This is the one place where the Debian rescue environment does not help much: there is no nmstatectl package for it, and openshift-install needs that binary to validate the NMState networkConfig. The practical fix is to build a pinned release from crates.io with Cargo:

apt-get update
apt-get install -y build-essential pkg-config libssl-dev curl

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source "$HOME/.cargo/env"

cargo install nmstatectl --version 2.2.60   # pin a known-good release
nmstatectl --version

The cargo build takes a few minutes and may need an extra -dev package. If it stops on a missing library, install the matching lib*-dev package and run it again. The shortcut script handles this step for you.

Install kexec-tools

The kexec-tools postinstall on Debian tries to hook itself into the boot process, which you don't want here. Preseed it to stay out of the way, then install:

echo 'kexec-tools kexec-tools/use_grub_config select false' | debconf-set-selections
echo 'kexec-tools kexec-tools/load_kexec select true'        | debconf-set-selections
apt-get install -y kexec-tools

Step 5 - Write the install and agent configs

Now you can create the files. Replace the placeholders with the information you gathered in step 3.

The Agent-based Installer reads two files. Put them in a working directory — and keep copies, because openshift-install consumes (deletes) them when it generates the assets.

mkdir -p /root/ocp-install

Create /root/ocp-install/install-config.yaml. This is the standard SNO shape: one control-plane replica, zero compute, platform: none. Drop in your pull secret (the whole JSON, single-quoted on one line) and your SSH public key:

apiVersion: v1
baseDomain: <base-domain>
metadata:
  name: <cluster-name>
compute:
- name: worker
  replicas: 0
controlPlane:
  name: master
  replicas: 1
networking:
  networkType: OVNKubernetes
  clusterNetwork:
  - cidr: 10.128.0.0/14
    hostPrefix: 23
  serviceNetwork:
  - 172.30.0.0/16
  machineNetwork:
  - cidr: 203.0.113.0/<prefix>      # your server's subnet, not the host IP
platform:
  none: {}
pullSecret: '<paste-pull-secret-json-here>'
sshKey: |
  ssh-ed25519 AAAA... your-public-key

Then /root/ocp-install/agent-config.yaml. This is where the network facts from Step 3 go, plus rootDeviceHints pinned to the disk serial so the installer targets the right drive:

apiVersion: v1beta1
kind: AgentConfig
metadata:
  name: <cluster-name>
rendezvousIP: <server-ip>
hosts:
- hostname: <cluster-name>.<base-domain>
  interfaces:
  - name: <interface>
    macAddress: <mac>
  rootDeviceHints:
    serialNumber: <disk-serial>
  networkConfig:
    interfaces:
    - name: <interface>
      type: ethernet
      state: up
      mac-address: <mac>
      ipv4:
        enabled: true
        dhcp: false
        address:
        - ip: <server-ip>
          prefix-length: <prefix>
    dns-resolver:
      config:
        server:
        - <dns>
    routes:
      config:
      - destination: 0.0.0.0/0
        next-hop-address: <gateway>
        next-hop-interface: <interface>

For a single node, rendezvousIP is just the node's own IP - like 203.0.113.45 . The machineNetwork CIDR in install-config.yaml is the subnet (e.g. 203.0.113.0/26), not the host address.

Back up both files now:

cp /root/ocp-install/install-config.yaml /root/install-config.yaml.bak
cp /root/ocp-install/agent-config.yaml   /root/agent-config.yaml.bak

The next command eats the originals. If you want to tweak and re-run, you'll restore from these.

Step 6 - Generate the PXE boot artifacts

Run the installer. It validates your configs (needs nmstatectl), then produces the three RHCOS boot files plus an auth/ directory with your credentials:

openshift-install agent create pxe-files --dir /root/ocp-install

When it finishes you'll have:

/root/ocp-install/boot-artifacts/agent.x86_64-vmlinuz
/root/ocp-install/boot-artifacts/agent.x86_64-initrd.img
/root/ocp-install/boot-artifacts/agent.x86_64-rootfs.img
/root/ocp-install/auth/kubeconfig
/root/ocp-install/auth/kubeadmin-password

Copy the boot artifacts to /root with their original names — the kexec step expects them there:

cp /root/ocp-install/boot-artifacts/agent.x86_64-* /root/
ls -lh /root/agent.x86_64-*

Step 7 - Save your cluster credentials

Do not skip this step. The kexec in the next step replaces the entire rescue filesystem, so these files will be lost. Save both to your workstation before continuing:

cat /root/ocp-install/auth/kubeadmin-password   # copy this string
cat /root/ocp-install/auth/kubeconfig           # save to ~/.kube/config locally

There is no password recovery flow here. If you lose the kubeconfig before the cluster is fully up, you will need to reinstall. Copy the files out before continuing.

Step 8 - kexec into the Agent installer

kexec boots a new kernel from inside the running one — no firmware round-trip. The problem: kexec takes a single initrd, but the Agent installer ships two images (the initrd and a separate rootfs). The fix is to concatenate them into one combined initrd, with a little zero-padding so the second cpio archive starts on a 4-byte boundary.

The padding is 4 + ((4 - (initrd_size % 4)) % 4) bytes — align the initrd to 4 bytes, then add 4 more. Build it:

cd /root
INITRD=agent.x86_64-initrd.img
ROOTFS=agent.x86_64-rootfs.img
COMBINED=agent.x86_64-combinedinitrd.img

INITRD_SIZE=$(wc -c < "$INITRD")
PAD=$(( 4 + ((4 - (INITRD_SIZE % 4)) % 4) ))

cat "$INITRD" > "$COMBINED"
dd if=/dev/zero bs=1 count="$PAD" status=none >> "$COMBINED"
cat "$ROOTFS" >> "$COMBINED"

Now load the RHCOS kernel with that combined initrd and the kernel arguments the Agent installer expects, and fire:

kexec -l /root/agent.x86_64-vmlinuz \
  --initrd=/root/agent.x86_64-combinedinitrd.img \
  --append='rw ignition.firstboot ignition.platform.id=metal'

kexec -e

Your SSH session will drop the instant you run kexec -e. That is normal. The new kernel is taking over, so do not reset the server.

Step 9 - Watch the install finish

The node is now booting RHCOS and running the OpenShift bootstrap on its own. Total install time is typically 30-60 minutes, depending on hardware and image-pull speed.

After ~5-10 minutes the box answers SSH again — as the core user, not root:

ssh core@<server-ip>

Heads up on SSH host keys: the host key changes entering rescue, and again when RHCOS takes over, so you will see WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED. Normal. Clear the stale key and reconnect:

ssh-keygen -R <server-ip>

You can run sudo journalctl -f to watch the progress.

Run the following commands to check if everything is running:

Note that it can take a while before everything is ready and running.

sudo podman ps
systemctl --no-pager status kubelet
systemctl --no-pager status crio
ss -tlnp | grep 6443

After everything is running, save the kubeconfig in ~/.kube/config, then run:

export KUBECONFIG=~/.kube/config
oc get nodes
oc get clusteroperators

The API will not answer at first. It comes up partway through the install. Once it does, the single node should report all three roles:

$ oc get nodes
NAME              STATUS   ROLES                         AGE   VERSION
sno.example.com   Ready    control-plane,master,worker   42m   v1.35.1+a2c5e01

Watch oc get clusteroperators until every operator is AVAILABLE=True and PROGRESSING=False. When they settle, you're done. Log in to the console as kubeadmin with the password from Step 7 at:

https://console-openshift-console.apps.<cluster-name>.<base-domain>

You can double-check the domain by running this command:

oc whoami --show-console
curl -vk $(oc whoami --show-console)

If the node is still unreachable after ~15 minutes, the most likely cause is a wrong value in agent-config.yaml, such as an incorrect gateway or prefix. At that point, use a KVM console to see what the server is doing.

Step 10 - Next Steps

Useful next steps:

Description
Post-installation configuration Configure authentication, certificates, networking, and monitoring for your cluster.
Storage configuration SNO ships without a default storage class. Consider LVM Storage for local persistent volumes.
Building applications Learn how to deploy your first application using Source-to-Image (S2I), Dockerfiles, or Helm charts.
Networking Operators Explore additional networking features like the Ingress Node Firewall Operator for host-level filtering.
Backup and restore Essential for SNO since it's a single point of failure. Set up etcd backups and application-level backups with OADP.

For comprehensive guidance, refer to the OpenShift Container Platform 4.22 documentation.

Conclusion

At this point, you should have a working single-node OpenShift cluster running on a Hetzner dedicated server. The installation was prepared from the Rescue System and booted into the RHCOS installer with kexec.

The next practical steps are to configure storage, set up DNS for the API and application routes, and review your firewall rules once the installation is complete. If you want additional host-level filtering inside the cluster, you can look at the Ingress Node Firewall Operator.

This setup is best suited for labs, testing, or edge use. Because this is a single-node cluster, the host is a single point of failure. Make sure you plan for backups accordingly.

License: MIT
Want to contribute?

Get Rewarded: Get up to €50 in credit! Be a part of the community and contribute. Do it for the money. Do it for the bragging rights. And do it to teach others!

Report Issue

Get new tutorials via RSS / Atom

Discover our

Dedicated Servers

Get €20/$20 free credit!

Valid until: 31 December 2026 Valid for: 3 months and only for new customers
Configure now
Want to contribute?

Get Rewarded: Get up to €50 credit on your account for every tutorial you write and we publish!

Find out more
Report Issue

Get new tutorials via RSS / Atom