Claude-skill-registry hetzner-coder

This skill guides provisioning Hetzner Cloud infrastructure with OpenTofu/Terraform. Use when creating servers, networks, firewalls, load balancers, or volumes on Hetzner Cloud.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/hetzner-coder" ~/.claude/skills/majiayu000-claude-skill-registry-hetzner-coder && rm -rf "$T"
manifest: skills/data/hetzner-coder/SKILL.md
source content

Hetzner Coder

Overview

Hetzner Cloud offers high-performance, cost-effective cloud infrastructure with European data centers. This skill covers OpenTofu/Terraform patterns for Hetzner resources.

Provider Setup

terraform {
  required_providers {
    hcloud = {
      source  = "hetznercloud/hcloud"
      version = "~> 1.50"
    }
  }
}

provider "hcloud" {
  # Token from environment: HCLOUD_TOKEN
  # Or explicitly (not recommended):
  # token = var.hcloud_token
}

Authentication

# Set token via environment variable
export HCLOUD_TOKEN="your-api-token"

# Or with 1Password
HCLOUD_TOKEN=op://Infrastructure/Hetzner/api_token

Token Permissions:

  • Read - GET requests only (monitoring, auditing)
  • Read & Write - Full access (required for Terraform)

Locations and Datacenters

LocationCodeRegionNetwork Zone
Falkenstein
fsn1
Germany
eu-central
Nuremberg
nbg1
Germany
eu-central
Helsinki
hel1
Finland
eu-central
Ashburn
ash
US East
us-east
Hillsboro
hil
US West
us-west

Server Types

Shared CPU (Best for general workloads)

TypevCPUsRAMStorageBest For
cx22
24 GB40 GBSmall apps
cx32
48 GB80 GBMedium apps
cx42
816 GB160 GBProduction
cx52
1632 GB320 GBHigh traffic

AMD EPYC (CPX - Better single-thread)

TypevCPUsRAMStorage
cpx11
22 GB40 GB
cpx21
34 GB80 GB
cpx31
48 GB160 GB
cpx41
816 GB240 GB
cpx51
1632 GB360 GB

ARM64 (CAX - Best price/performance)

TypevCPUsRAMStorage
cax11
24 GB40 GB
cax21
48 GB80 GB
cax31
816 GB160 GB
cax41
1632 GB320 GB

Dedicated vCPU (CCX - Guaranteed resources)

TypevCPUsRAMStorage
ccx13
28 GB80 GB
ccx23
416 GB160 GB
ccx33
832 GB240 GB
ccx43
1664 GB360 GB

Servers (Compute)

Basic Server

resource "hcloud_server" "app" {
  name        = "${var.project}-${var.environment}-app"
  server_type = "cx22"
  image       = "ubuntu-24.04"
  location    = "fsn1"

  ssh_keys = [hcloud_ssh_key.deploy.id]

  labels = {
    environment = var.environment
    project     = var.project
    role        = "app"
  }

  public_net {
    ipv4_enabled = true
    ipv6_enabled = true
  }
}

Server with Cloud-Init

resource "hcloud_server" "app" {
  name        = "${var.project}-app"
  server_type = "cx22"
  image       = "ubuntu-24.04"
  location    = "fsn1"

  ssh_keys = [hcloud_ssh_key.deploy.id]

  user_data = <<-EOT
    #cloud-config
    package_update: true
    packages:
      - docker.io
      - docker-compose-plugin

    users:
      - name: deploy
        groups: docker, sudo
        sudo: ALL=(ALL) NOPASSWD:ALL
        shell: /bin/bash
        ssh_authorized_keys:
          - ${var.deploy_ssh_key}

    runcmd:
      - systemctl enable --now docker
      - sed -i 's/PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config
      - sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
      - systemctl restart sshd
  EOT

  labels = {
    environment = var.environment
    role        = "app"
  }
}

ARM64 Server (Cost-Effective)

resource "hcloud_server" "worker" {
  name        = "${var.project}-worker"
  server_type = "cax21"  # ARM64 - great price/performance
  image       = "ubuntu-24.04"
  location    = "fsn1"

  ssh_keys = [hcloud_ssh_key.deploy.id]

  labels = {
    role = "worker"
    arch = "arm64"
  }
}

Private Networks

Network with Subnet

resource "hcloud_network" "private" {
  name     = "${var.project}-network"
  ip_range = "10.0.0.0/16"

  labels = {
    project = var.project
  }
}

resource "hcloud_network_subnet" "private" {
  network_id   = hcloud_network.private.id
  type         = "cloud"
  network_zone = "eu-central"  # Must match server location zone
  ip_range     = "10.0.1.0/24"
}

Server in Private Network

resource "hcloud_server" "db" {
  name        = "${var.project}-db"
  server_type = "cpx31"
  image       = "ubuntu-24.04"
  location    = "fsn1"

  ssh_keys = [hcloud_ssh_key.deploy.id]

  # Attach to private network
  network {
    network_id = hcloud_network.private.id
    ip         = "10.0.1.10"  # Optional: specific IP
  }

  # Optionally disable public IP for security
  public_net {
    ipv4_enabled = false
    ipv6_enabled = false
  }

  labels = {
    role = "database"
  }

  depends_on = [hcloud_network_subnet.private]
}

Firewalls

Web Server Firewall

resource "hcloud_firewall" "web" {
  name = "${var.project}-web-firewall"

  # SSH from specific IPs only (NEVER use 0.0.0.0/0!)
  rule {
    description = "SSH"
    direction   = "in"
    protocol    = "tcp"
    port        = "22"
    source_ips  = [var.admin_ip]  # Use variable, no default
  }

  # HTTP/HTTPS from anywhere
  rule {
    description = "HTTP"
    direction   = "in"
    protocol    = "tcp"
    port        = "80"
    source_ips  = ["0.0.0.0/0", "::/0"]
  }

  rule {
    description = "HTTPS"
    direction   = "in"
    protocol    = "tcp"
    port        = "443"
    source_ips  = ["0.0.0.0/0", "::/0"]
  }

  # ICMP for debugging (ping)
  rule {
    description = "ICMP"
    direction   = "in"
    protocol    = "icmp"
    source_ips  = ["0.0.0.0/0", "::/0"]
  }

  # Apply to servers with label
  apply_to {
    label_selector = "role=web"
  }
}

# IMPORTANT: admin_ip variable has NO default for security
variable "admin_ip" {
  description = "Admin IP for SSH access (CIDR) - REQUIRED, no default"
  type        = string
  # NO DEFAULT - forces explicit value
}

Security pattern: Never default SSH access to

0.0.0.0/0
. Force explicit IP:

tofu apply -var="admin_ip=$(curl -s ifconfig.me)/32"

Database Firewall (Private Only)

resource "hcloud_firewall" "db" {
  name = "${var.project}-db-firewall"

  # PostgreSQL from private network only
  rule {
    description = "PostgreSQL"
    direction   = "in"
    protocol    = "tcp"
    port        = "5432"
    source_ips  = ["10.0.0.0/16"]  # Private network range
  }

  # SSH from bastion only
  rule {
    description = "SSH from bastion"
    direction   = "in"
    protocol    = "tcp"
    port        = "22"
    source_ips  = ["10.0.1.1/32"]  # Bastion IP
  }

  apply_to {
    label_selector = "role=database"
  }
}

Floating IPs (High Availability)

resource "hcloud_floating_ip" "app" {
  type          = "ipv4"
  name          = "${var.project}-vip"
  home_location = "fsn1"

  labels = {
    project = var.project
    purpose = "failover"
  }
}

resource "hcloud_floating_ip_assignment" "app" {
  floating_ip_id = hcloud_floating_ip.app.id
  server_id      = hcloud_server.app.id
}

output "floating_ip" {
  value = hcloud_floating_ip.app.ip_address
}

Load Balancers

HTTP Load Balancer

resource "hcloud_load_balancer" "web" {
  name               = "${var.project}-lb"
  load_balancer_type = "lb11"
  location           = "fsn1"

  labels = {
    project = var.project
  }
}

resource "hcloud_load_balancer_network" "web" {
  load_balancer_id = hcloud_load_balancer.web.id
  network_id       = hcloud_network.private.id
  ip               = "10.0.1.100"
}

resource "hcloud_load_balancer_service" "http" {
  load_balancer_id = hcloud_load_balancer.web.id
  protocol         = "http"
  listen_port      = 80
  destination_port = 8080

  health_check {
    protocol = "http"
    port     = 8080
    interval = 10
    timeout  = 5
    retries  = 3

    http {
      path         = "/health"
      status_codes = ["200"]
    }
  }
}

resource "hcloud_load_balancer_target" "web" {
  load_balancer_id = hcloud_load_balancer.web.id
  type             = "server"
  server_id        = hcloud_server.app.id
  use_private_ip   = true

  depends_on = [hcloud_load_balancer_network.web]
}

HTTPS Load Balancer with Certificate

resource "hcloud_managed_certificate" "web" {
  name         = "${var.project}-cert"
  domain_names = [var.domain, "www.${var.domain}"]

  labels = {
    project = var.project
  }
}

resource "hcloud_load_balancer_service" "https" {
  load_balancer_id = hcloud_load_balancer.web.id
  protocol         = "https"
  listen_port      = 443
  destination_port = 8080

  http {
    certificates = [hcloud_managed_certificate.web.id]
    redirect_http = true
  }

  health_check {
    protocol = "http"
    port     = 8080
    interval = 10
    timeout  = 5
  }
}

Volumes (Persistent Storage)

resource "hcloud_volume" "data" {
  name      = "${var.project}-data"
  size      = 100  # GB
  location  = "fsn1"
  format    = "ext4"

  labels = {
    project = var.project
    purpose = "database"
  }
}

resource "hcloud_volume_attachment" "data" {
  volume_id = hcloud_volume.data.id
  server_id = hcloud_server.db.id
  automount = true
}

SSH Keys

resource "hcloud_ssh_key" "deploy" {
  name       = "${var.project}-deploy"
  public_key = file(var.ssh_public_key_path)

  labels = {
    project = var.project
    purpose = "deployment"
  }
}

Ansible Integration

Post-Provisioning with Ansible

Cloud-init runs at first boot. For ongoing configuration or re-running setup, use Ansible.

# outputs.tf
output "server_ip" {
  value       = hcloud_server.app.ipv4_address
  description = "Server IP for Ansible inventory"
}

output "ansible_inventory" {
  value = <<-EOT
    [web]
    ${hcloud_server.app.ipv4_address} ansible_user=root
  EOT
  description = "Ansible inventory content"
}

Provision Script (Terraform → Ansible → Kamal)

#!/usr/bin/env bash
# infra/bin/provision
set -euo pipefail

INFRA_DIR="$(dirname "$0")/.."

# 1. Terraform
cd "$INFRA_DIR"
tofu apply

# 2. Wait for SSH
SERVER_IP=$(tofu output -raw server_ip)
until ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=accept-new root@$SERVER_IP true 2>/dev/null; do
  echo "Waiting for server..."
  sleep 5
done

# 3. Ansible
cd ansible
tofu output -raw ansible_inventory > hosts.ini
ansible-galaxy install -r requirements.yml --force
ANSIBLE_HOST_KEY_CHECKING=False ansible-playbook -i hosts.ini playbook.yml

# 4. Kamal bootstrap
cd ../..
bundle exec kamal server bootstrap

Kamal-Ready Server Playbook

Based on kamal-ansible-manager:

# infra/ansible/playbook.yml
---
- name: Configure Hetzner server for Kamal
  hosts: web
  become: true

  vars:
    swap_file_size_mb: "2048"
    timezone: "UTC"

  roles:
    - role: geerlingguy.swap
      when: ansible_swaptotal_mb < 1

  tasks:
    - name: Install Docker
      ansible.builtin.shell: curl -fsSL https://get.docker.com | sh
      args:
        creates: /usr/bin/docker

    - name: Enable Docker
      ansible.builtin.systemd:
        name: docker
        state: started
        enabled: true

    - name: Install security packages
      ansible.builtin.apt:
        name: [fail2ban, ufw]
        state: present
        update_cache: true

    - name: Configure fail2ban
      ansible.builtin.copy:
        dest: /etc/fail2ban/jail.local
        content: |
          [sshd]
          enabled = true
          maxretry = 5
          bantime = 3600
        mode: "0644"

    - name: Configure UFW
      community.general.ufw:
        rule: allow
        port: "{{ item }}"
        proto: tcp
      loop: [22, 80, 443]

    - name: Enable UFW
      community.general.ufw:
        state: enabled
        policy: deny
        direction: incoming

    - name: Harden SSH
      ansible.builtin.lineinfile:
        path: /etc/ssh/sshd_config
        regexp: "^#?PasswordAuthentication"
        line: "PasswordAuthentication no"
      notify: Restart ssh

  handlers:
    - name: Restart ssh
      ansible.builtin.systemd:
        name: ssh
        state: restarted

Requirements

# infra/ansible/requirements.yml
---
roles:
  - name: geerlingguy.swap
    version: 2.0.0

When to Use Each Approach

ApproachUse Case
Cloud-init onlyImmutable infra, destroy/recreate pattern
Ansible onlyExisting servers, complex multi-step config
Cloud-init + AnsibleFirst boot basics, then Ansible for hardening

Additional Resources