Examples
Ready-to-copy root configurations under terraform/examples/, one per
substrate/scenario. Each is a complete tofu apply — infra + bootstrap
(+ platform where applicable). Copy the one that matches your substrate, fill
in terraform.tfvars (where provided), and follow
Getting started.
Two patterns appear across the set:
<cloud>-shc— onemodule "shc"call (a substrate front door) folds infra + bootstrap into a single root; the platform (DNS / ACME / apps) is wired either through the module’stopologyblock (Hetzner) or with root-levelshc_connection/shc_config/shc_deploymentresources (the cloudflare-parent-mix estates).- scenario variants (
hetzner-lan,hetzner-wan-nebula,hetzner-lan-lb,mixed-proxmox-hetzner,proxmox-lan) — demonstrate a specific topology or a multi-substrate composition.
Every example
Section titled “Every example”| Example | Substrate | Module(s) | Topology | What it provisions / when to pick it |
|---|---|---|---|---|
hetzner-shc | Hetzner Cloud | shc/hetzner | lb | Canonical front-door root. Two node groups (control: compose ×3, workers: swarm ×2, own region + per-group firewall) proving multi-pool + mixed-runtime + one shared network. DNS/ACME/LB via the module’s topology block. Start here for Hetzner. |
hetzner-lan | Hetzner Cloud | shc/hetzner | none | 3 nodes on a private hcloud network; cluster plane pinned to the private NIC. No overlay, no LB, SSH-only public firewall. Pick when the cluster is purely LAN-internal. |
hetzner-lan-lb | Hetzner Cloud | shc/hetzner | lb | The LAN scenario plus the SHC-managed edge: hetzner connection (dns + volume), dns.provider default, LB port attach on system traefik, hcloud volume config. Pick for a private-plane cluster that still needs a public LB. |
hetzner-wan-nebula | Hetzner Cloud | shc/hetzner | wan | 3 nodes, NO private network — cluster plane on public IPs, Nebula overlay (shc init --nebula + shc cluster mesh enable). firewall.inter_node is left UNSET, which derives "plane" from the public-IP plane declared in networking. Pick for a geo-spread / no-private-network cluster. |
aws-shc | AWS (EC2/VPC) | shc/aws | off (root shc_*) | Mirrors a live AWS reference estate. EC2/VPC/subnet in one AZ, per-group SGs, one EIP/node; a scoped daemon IAM user + S3 restic bucket; aws connection (volume/backup/mint + Route53 DNS) or cloudflare DNS; demo apps. Representative full-featured cloud root. |
azure-shc | Azure | shc/azure | off (root shc_*) | One resource group with a VNet + subnet, 3 compose nodes; cloudflare (default) or Azure DNS connection; a postgres demo deployment. |
gcp-shc | Google Cloud | shc/gcp | off (root shc_*) | GCE instances (startup-script bootstrap), cloudflare-parent-mix DNS by default; postgres demo. Pins the module to a Pages tarball (module_version var). |
digitalocean-shc | DigitalOcean | shc/digitalocean | off (root shc_*) | Droplets + region VPC + cloud firewalls + reserved IPs; one DO connection lighting dns + proxy (dolb) + volume (dovolume), or cloudflare for DNS; postgres (LAN) + whoami (public, real LE). |
linode-shc | Linode/Akamai | shc/linode | off | Instances + region-scoped VPC + cloud firewalls; one control group (compose ×3). Cloudflare-parent-mix DNS. (Unverified-live.) |
scaleway-shc | Scaleway | shc/scaleway | off (root shc_*) | Substrate + a cloudflare connection (Scaleway has no LB proxy provider, so no lb topology); IAM key backs the project. |
vultr-shc | Vultr | shc/vultr | off (root shc_*) | Instances + one network; one vultr connection (proxy vultr-lb + volume) + cloudflare for DNS (Vultr DNS can’t host subdomain zones). Mirrors a live Vultr reference estate. |
exoscale-shc | Exoscale | shc/exoscale | off (root shc_*) | Substrate + a cloudflare DNS connection (Exoscale DNS is a paid add-on); whoami demo. Mints the SSH keypair + vault password in-config. |
alibaba-shc | Alibaba Cloud | shc/alibaba | none | ECS/VPC + VSwitch + cluster security group; one control group (compose ×3) in a single zone. Infra + bootstrap only. (Unverified-live.) |
cloudstack-shc | Apache CloudStack | shc/cloudstack | none | Isolated-network substrate + public IPs/static NAT/firewalls; one control group (compose ×3). Infra + bootstrap only. (Unverified-live.) |
openstack-shc | OpenStack (OVH) | shc/openstack | off (root shc_*) | Substrate + a cloudflare connection + shc_config (no Designate on OVH, so DNS runs through cloudflare). Pins the module to a Pages tarball. |
proxmox-lan | Proxmox VE | shc/proxmox | none | A separate Proxmox VM set on a flat LAN with static IPs; whole plane rides the single virtio NIC. No overlay/LB/pfsense. The SSH engine installs the .deb (no cloud-init user-data on the telmate provider). |
mixed-proxmox-hetzner | Hetzner + Proxmox | shc/hetzner + shc/proxmox (both manage_platform = false) + root core | wan | The split pattern: two infra-only front doors expose their nodes, then ONE root module "core" bootstraps + wires the platform over concat()’d nodes. Bootstrap on the Hetzner node; Nebula overlay ties the substrates together. The mixed-substrate reference. |
Notes:
- “off (root
shc_*)” means the module’stopologyblock is deliberately omitted and the platform is wired with root-levelshc_connection/shc_config/shc_deploymentresources — the cloudflare-parent-mix shape, the default across live estates (the module’s single-connection topology block can’t express a cloudflare DNS provider split from a cloud-native volume/proxy provider). - Examples that ship a
registry.tfrc(gcp, digitalocean, scaleway) include a readyprovider_installationblock — use it viaexport TF_CLI_CONFIG_FILE="$PWD/registry.tfrc". - “Unverified-live” means the substrate module is built + unit-tested but has not been applied against a live account yet.
Walkthrough: hetzner-shc
Section titled “Walkthrough: hetzner-shc”The canonical single-root example. It wires infra + bootstrap + the lb
platform in one tofu apply with no bearer token and no driver.sh.
versions.tf — declares both providers; the shc provider resolves from
the network mirror (see Getting started):
terraform { required_version = ">= 1.6.0" required_providers { hcloud = { source = "hetznercloud/hcloud", version = "~> 1.45" } shc = { source = "selfhosted-cloud/shc" } }}main.tf — one module "shc" call folding all three layers:
provider "hcloud" { # Token from the HCLOUD_TOKEN environment variable — never in tfvars/state.}
module "shc" { source = "../../modules/shc/hetzner"
cluster_name = var.cluster_name region = var.region image = var.image ssh_public_key = var.ssh_public_key
# Default SSH — both node groups resolve to this. Drives the install+join # transport AND, for the bootstrap node, the platform-resource SSH transport. ssh = { user = "admin" private_key_path = var.ssh_private_key_path }
# LAYER 1: infra (always applies) — two pools, mixed runtime + region. node_groups = { control = { count = 3, server_type = "cx33", runtime = "compose" } workers = { count = 2, server_type = "cx43", runtime = "swarm", region = var.workers_region firewall = { # PER-GROUP firewall (only workers) public_ports = [ { port = "30000-32767", protocol = "tcp" }, { port = "30000-32767", protocol = "udp" }, ] } } }
networking = { private = true, ip_range = var.network_cidr, subnet = var.subnet_cidr } firewall = { public_ports = [] } # inter_node unset → derives "none" from this private plane
# LAYER 2: bootstrap — installs the .deb + runs shc init / shc node join # over SSH. Public Let's Encrypt via a named ACME provider. bootstrap = { package = { deb_url = var.deb_url } vault_password = var.vault_pw nebula = false host = var.shc_host acme = { providers = { prod = { email = var.acme_email } } default = "prod" } }
# LAYER 3: topology / platform — DNS + ACME + traefik LB port attach, over # the same SSH transport (requires bootstrap, enforced by a check block). topology = { kind = "lb" dns = { name = "hetzner" type = "hetzner" zones = [var.dns_zone] credentials = { token = var.hcloud_token } } }}What each layer maps to:
- Layer 1 (infra) →
hcloud_server,hcloud_network,hcloud_network_subnet,hcloud_firewall(per group),hcloud_ssh_key,hcloud_placement_group. Plain hcloud resources, authed fromHCLOUD_TOKEN. - Layer 2 (bootstrap) → inside
module.core: one pure-configshc_cluster, oneshc_node.bootstrap(shc initoncontrolnode 1), andshc_node.workers(shc node joinon the other 4). Seeshc_node. - Layer 3 (topology
lb) → a proxy-capableshc_connection(namedhetzner), ashc_configsettingdns.provider, and ashc_stack_portsattaching80/443to the system traefik stack.
The bootstrap node’s runtime is DERIVED from node_groups.control.runtime
(the alphabetically-first group), not a standalone field.
Outputs (outputs.tf) surface the cluster endpoint and node facts:
tofu output cluster_endpointtofu output node_public_ipv4sTo manage more of the cluster declaratively, add root-level shc_deployment
blocks (with a cluster { ssh { hosts = module.shc.cluster_ssh_hosts, user = "admin", private_key = file(var.ssh_private_key_path) } } override so they
reach the cluster — hosts names every node, so the operation still lands
when the leader moves), or SSH in and use the CLI.
See The module library for the full shc/hetzner and core
input/output reference, and the cloud guides
for per-cloud account prerequisites.