Skip to content
SHC Docs

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 — one module "shc" call (a substrate front door) folds infra + bootstrap into a single root; the platform (DNS / ACME / apps) is wired either through the module’s topology block (Hetzner) or with root-level shc_connection / shc_config / shc_deployment resources (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.
ExampleSubstrateModule(s)TopologyWhat it provisions / when to pick it
hetzner-shcHetzner Cloudshc/hetznerlbCanonical 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-lanHetzner Cloudshc/hetznernone3 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-lbHetzner Cloudshc/hetznerlbThe 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-nebulaHetzner Cloudshc/hetznerwan3 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-shcAWS (EC2/VPC)shc/awsoff (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-shcAzureshc/azureoff (root shc_*)One resource group with a VNet + subnet, 3 compose nodes; cloudflare (default) or Azure DNS connection; a postgres demo deployment.
gcp-shcGoogle Cloudshc/gcpoff (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-shcDigitalOceanshc/digitaloceanoff (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-shcLinode/Akamaishc/linodeoffInstances + region-scoped VPC + cloud firewalls; one control group (compose ×3). Cloudflare-parent-mix DNS. (Unverified-live.)
scaleway-shcScalewayshc/scalewayoff (root shc_*)Substrate + a cloudflare connection (Scaleway has no LB proxy provider, so no lb topology); IAM key backs the project.
vultr-shcVultrshc/vultroff (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-shcExoscaleshc/exoscaleoff (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-shcAlibaba Cloudshc/alibabanoneECS/VPC + VSwitch + cluster security group; one control group (compose ×3) in a single zone. Infra + bootstrap only. (Unverified-live.)
cloudstack-shcApache CloudStackshc/cloudstacknoneIsolated-network substrate + public IPs/static NAT/firewalls; one control group (compose ×3). Infra + bootstrap only. (Unverified-live.)
openstack-shcOpenStack (OVH)shc/openstackoff (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-lanProxmox VEshc/proxmoxnoneA 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-hetznerHetzner + Proxmoxshc/hetzner + shc/proxmox (both manage_platform = false) + root corewanThe 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’s topology block is deliberately omitted and the platform is wired with root-level shc_connection / shc_config / shc_deployment resources — 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 ready provider_installation block — use it via export 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.

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 from HCLOUD_TOKEN.
  • Layer 2 (bootstrap) → inside module.core: one pure-config shc_cluster, one shc_node.bootstrap (shc init on control node 1), and shc_node.workers (shc node join on the other 4). See shc_node.
  • Layer 3 (topology lb) → a proxy-capable shc_connection (named hetzner), a shc_config setting dns.provider, and a shc_stack_ports attaching 80/443 to 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:

Terminal window
tofu output cluster_endpoint
tofu output node_public_ipv4s

To 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.