Provider Setup
terraform { required_providers { hcloud = { source = "hetznercloud/hcloud" version = "~> 1.50" } } }
provider "hcloud" {
Uses HCLOUD_TOKEN env var
}
Authentication
export HCLOUD_TOKEN="your-api-token"
Or with 1Password
HCLOUD_TOKEN=op://Infrastructure/Hetzner/api_token
Locations
Code Region Network Zone
fsn1
Germany eu-central
nbg1
Germany eu-central
hel1
Finland eu-central
ash
US East us-east
hil
US West us-west
See references/hetzner-server-types.md for all server types (CX, CPX, CAX, CCX).
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" } }
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" 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]
network { network_id = hcloud_network.private.id ip = "10.0.1.10" }
public_net { ipv4_enabled = false ipv6_enabled = false }
labels = { role = "database" }
depends_on = [hcloud_network_subnet.private] }
Firewalls
Never default SSH to 0.0.0.0/0 . Force explicit IP: tofu apply -var="admin_ip=$(curl -s ifconfig.me)/32"
Web Server Firewall
resource "hcloud_firewall" "web" { name = "${var.project}-web-firewall"
rule { description = "SSH" direction = "in" protocol = "tcp" port = "22" source_ips = [var.admin_ip] }
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"] }
apply_to { label_selector = "role=web" } }
variable "admin_ip" { description = "Admin IP for SSH access (CIDR)" type = string }
Database Firewall (Private Only)
resource "hcloud_firewall" "db" { name = "${var.project}-db-firewall"
rule { description = "PostgreSQL" direction = "in" protocol = "tcp" port = "5432" source_ips = ["10.0.0.0/16"] }
rule { description = "SSH from bastion" direction = "in" protocol = "tcp" port = "22" source_ips = ["10.0.1.1/32"] }
apply_to { label_selector = "role=database" } }
Floating IPs (High Availability)
resource "hcloud_floating_ip" "app" { type = "ipv4" name = "${var.project}-vip" home_location = "fsn1" }
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) }
References
-
references/hetzner-server-types.md - All server types with specs
-
references/ansible-integration.md - Hetzner+Ansible patterns, Kamal-ready playbooks
-
references/best-practices.md - Labels, cost optimization, placement groups, snapshots
-
references/object-storage.md - S3-compatible Object Storage with AWS provider
-
references/production-stack.md - Complete production setup