> For the complete documentation index, see [llms.txt](https://docs.rumi.systems/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.rumi.systems/rumi-platform/concepts/hetzner-binding.md).

# Hetzner Binding

The Hetzner binding deploys the canonical Rumi private cloud onto [Hetzner Cloud](https://www.hetzner.com/cloud). The same Rumi application, described once in DDL, runs unchanged across a developer laptop, local Docker, AWS, and Hetzner; the Hetzner binding is the component that turns that model into real networks, firewalls, servers, and DNS.

It follows the same **prebaked-image** shape as the AWS binding (bake a role once, boot many nodes from it), adapted to Hetzner's primitives: private networks, label-selected cloud firewalls, server snapshots, and a bastion that doubles as the fleet's DNS server.

This page is a feature catalogue. For the commands that drive each feature, see the [`rumi cloud hetzner`](/rumi-cli/commands/cloud/hetzner.md) reference.

{% hint style="success" %}
The Hetzner binding is **live-verified end to end**: provision → deploy → lifecycle → expose → HTTPS with a real Let's Encrypt wildcard certificate.
{% endhint %}

## Deployment model

Hetzner uses a **prebaked-snapshot** model, the same shape as the AWS AMI model. Each role's software is baked once into a Hetzner **snapshot** via `create_image`, and every launch boots a node from that snapshot; launches select the right snapshot by label. Every non-worker role is baked *from* the `service` (Worker base) snapshot, mirroring "build on the Rumi Worker AMI," so a role recipe installs only its delta.

The seven roles and how each boots:

| Role       | What it runs                                                                                                                                |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `service`  | The Worker base: `rumi` user, Java 8/11/17, a `boot.sh` extension point run by a `rumi-boot.service` systemd unit at boot. No agent.        |
| `admin`    | ActiveMQ discovery broker + agent + admin server (via the official versioned installers), discovery pinned to `activemq://localhost:61616`. |
| `monitor`  | Grafana (`neeve/nvx-rumi-monitor`) as a restart-always container.                                                                           |
| `solace`   | Solace PubSub+ standard as a restart-always container.                                                                                      |
| `kafka`    | Kafka in KRaft mode as a restart-always container.                                                                                          |
| `activemq` | Native ActiveMQ as the `rumi-messaging` broker (distinct from the admin node's `rumi-discovery`).                                           |
| `bastion`  | The public entry point and internal DNS server (see below).                                                                                 |

## Network topology

The topology is **public-only**: every node has a public IP and also joins a private network (`10.0.0.0/16`, subnet `10.0.0.0/24`). Only the bastion is reachable from the internet; all other roles open their ports only to the network CIDR, so every fleet operation flows through the bastion.

* `create-network` creates the private network plus **seven per-role cloud firewalls** that **auto-apply by label selector** (`rumi-network=<name>,rumi-role=<role>`). The bastion firewall opens `22`/`80`/`443` to the world; every other role opens its ports to the network CIDR; all roles allow in-network ICMP.
* Because firewalls apply by label, a launch is secured the instant its labels are set — the per-service app-port firewall can be created before *or* after the instance it protects.

{% hint style="info" %}
A managed public-DNS + NAT / private-only-egress topology (the equivalent of the AWS `public-private-nat` topology) is on the roadmap. Today every node is public and on the private network.
{% endhint %}

## Bastion model

* **DNS server**: the bastion runs **BIND** authoritative for the `rumi.local` zone, mapping each role's short internal names to its private (`10.x`) IP. `admin` also gets `discovery` and `influxdb` names. Each launched node's resolver is pointed at the bastion's private IP, and the bastion fans out over the private network.
* **Public entry point**: only the bastion is SSH-reachable from outside; the private key is copied to the bastion (`~/.ssh/rumi`) so it can reach the rest of the fleet.
* **Reverse proxy fronting**: each service's public port is served through the bastion's nginx. It must be launched before any other role.
* **First-boot guard**: `launchBastion` waits for `:80` before the platform launch continues, so DNS and the proxy are ready before dependent roles register.

## Service instances

* `launch-instance` boots a worker from the `service` snapshot, labelled `rumi-role=service` and `rumi-service=<network>-<name>`, so both the base service firewall (`:22`) and a per-service app-port firewall auto-apply. It then registers `<service>.rumi.local`.
* **Dynamic app-port opening**: the per-service firewall opens the requested ports to the network CIDR and applies by label, so ports can be opened before or after launch. It is labelled with the network so teardown sweeps it.

## HTTPS

* **Let's Encrypt wildcard certificates** via certbot's DNS-01 challenge, with autonomous renewal. One domain per bastion; the bastion's nginx terminates TLS for every service it proxies.
* **Provider-pluggable DNS-01**: the public DNS for the domain can live wherever you keep it — `--dns-provider route53 | hetzner | cloudflare` selects the matching `certbot-dns-<provider>` plugin (all three are baked into the bastion). Credentials are written **root-only** on the bastion and reused for hands-free renewal.
* Run `enable-https` right after launching the bastion and before adding service proxies, so each proxy is created as a TLS block.

{% hint style="warning" %}
**Rumi does not manage public DNS.** Pointing the public name at the bastion — `*.<domain>` or `<service>.<domain>` → the bastion's public IP — is the operator's job, in whatever DNS hosts the domain. `enable-https` only proves control of the domain (the DNS-01 challenge) to issue the certificate.
{% endhint %}

## Lifecycle and idempotency

* **Granular commands** launch the bastion, platform services, brokers, and workers individually; **convenience commands** (`provision`, `launch-platform`) stand up a whole single-bastion deployment in one step.
* **Per-component lifecycle**: `start-admin`, `stop-solace`, `terminate-monitor`, and the rest give every role and service its own `start-*` / `stop-*` / `terminate-*` verb (the same vocabulary as AWS and Civo). `start-platform` / `stop-platform` / `terminate-platform` walk the whole platform in dependency order (bastion first on start, last on stop/terminate), and the env-wide `start` / `stop` act on every instance in the network. `resize-instance` retypes a stopped server (stop → `change_type` → restart, since Hetzner requires the server off to retype).
* **Idempotent teardown**: `deprovision` deletes the servers, **waits for them to be gone** (a label-applied firewall cannot be deleted while still attached — Hetzner rejects it), then deletes the firewalls and the network, with no leaks. Terminating a service also removes its per-service firewall after the server is gone.

## High-level operations

* `deploy` copies the XAR to the bastion staging directory and hands it to the admin agent.
* `configure` adds, updates, or removes a controller config property on the admin node.
* `add-proxy` adds a bastion reverse-proxy entry so a service is reachable at `<service>.<domain>` (nginx only — it does not touch public DNS).

## Getting started

1. Set a Hetzner Cloud project API token: `export HCLOUD_TOKEN=...` (or pass `--api-key`).
2. Bake the role snapshots into the project (the `service` snapshot first, since other roles bake from it).
3. Have an SSH key pair; the private key lives at `~/.ssh/<key-pair>` and its public half is uploaded automatically if absent.

```bash
rumi cloud hetzner create-network  --network my-cloud
rumi cloud hetzner launch-platform --network my-cloud --key-pair rumi
rumi cloud hetzner launch-instance --network my-cloud --key-pair rumi --service myapp --ports 9000
```

## Related documentation

* [AWS Binding](/rumi-platform/concepts/aws-binding.md) — the same private cloud on AWS (SOC 2 posture, SSM, NAT topology, CloudWatch).
* [Civo Binding](/rumi-platform/concepts/civo-binding.md) — the install-at-boot binding on Civo.
* [`rumi cloud hetzner`](/rumi-cli/commands/cloud/hetzner.md) — command reference.
