Hetzner Cloud with Terraform: Low-Cost Infrastructure
Practical tutorial for managing Hetzner Cloud with Terraform. Servers, networks, firewalls, load balancers. Ready-to-use code examples.
Hetzner has become my favorite cloud provider for personal projects and startups. The prices are unbeatable — a server with 2 vCPU, 4GB RAM, 40GB SSD costs about €4/month. Compare that with AWS or GCP and you understand why it's interesting.
But Hetzner without automation is manual management. Terraform solves this problem. Let's see how to use them together.
Why Hetzner
Before getting into Terraform, a few words on why Hetzner.
Prices. Drastically lower than big cloud. Not 10-20% less, 50-80% less. For workloads that don't require specific managed services from AWS/GCP, the savings are significant.
Performance. Physical servers in European data centers (Germany, Finland). Fast network, SSD/NVMe storage. These aren't cheap servers, they're serious machines.
Simplicity. Fewer services than big cloud, but the ones they have work well. Servers, volumes, networks, load balancers, firewalls, DNS. The essentials for most cases.
Limitations. Fewer regions (mainly Europe, US recently), fewer managed services, fewer integrations. If you need Lambda, DynamoDB, or specific AWS services, Hetzner isn't for you.
Initial Setup
API Token
Go to Hetzner Cloud Console, create a project, generate an API token with Read & Write permissions.
Terraform Provider
# versions.tf
terraform {
required_providers {
hcloud = {
source = "hetznercloud/hcloud"
version = "~> 1.45"
}
}
required_version = ">= 1.0"
}
# provider.tf
provider "hcloud" {
token = var.hcloud_token
}
# variables.tf
variable "hcloud_token" {
description = "Hetzner Cloud API Token"
type = string
sensitive = true
}
The token should be passed as a variable, not hardcoded. Use a .tfvars file (don't commit!) or environment variables:
export TF_VAR_hcloud_token="your-token-here"
The First Server
Let's create a basic server:
# server.tf
resource "hcloud_server" "web" {
name = "web-server"
image = "ubuntu-22.04"
server_type = "cx22" # 2 vCPU, 4GB RAM
location = "fsn1" # Falkenstein, Germany
ssh_keys = [hcloud_ssh_key.default.id]
labels = {
environment = "production"
role = "web"
}
}
resource "hcloud_ssh_key" "default" {
name = "default"
public_key = file("~/.ssh/id_rsa.pub")
}
output "server_ip" {
value = hcloud_server.web.ipv4_address
}
terraform init
terraform plan
terraform apply
In a minute you have a running server. The output gives you the IP to connect via SSH.
Server Types
Hetzner has several lines:
- CX — Shared vCPU, economical, good for most uses
- CPX — Shared vCPU AMD, same performance at similar prices
- CCX — Dedicated vCPU, for workloads requiring consistent performance
- CAX — ARM (Ampere), excellent price/performance ratio
For general use, cx22 (€4/month) or cx32 (€8/month) are great starting points.
Private Network
For communication between servers without going through the internet:
# network.tf
resource "hcloud_network" "private" {
name = "private-network"
ip_range = "10.0.0.0/16"
}
resource "hcloud_network_subnet" "subnet" {
network_id = hcloud_network.private.id
type = "cloud"
network_zone = "eu-central"
ip_range = "10.0.1.0/24"
}
# Connect the server to the private network
resource "hcloud_server_network" "web_network" {
server_id = hcloud_server.web.id
network_id = hcloud_network.private.id
ip = "10.0.1.10"
}
Servers on the same private network communicate without traffic costs and with minimal latency.
Firewall
Hetzner has managed firewalls at project level:
# firewall.tf
resource "hcloud_firewall" "web" {
name = "web-firewall"
# SSH
rule {
direction = "in"
protocol = "tcp"
port = "22"
source_ips = [
"0.0.0.0/0",
"::/0"
]
}
# HTTP
rule {
direction = "in"
protocol = "tcp"
port = "80"
source_ips = [
"0.0.0.0/0",
"::/0"
]
}
# HTTPS
rule {
direction = "in"
protocol = "tcp"
port = "443"
source_ips = [
"0.0.0.0/0",
"::/0"
]
}
}
# Apply firewall to server
resource "hcloud_firewall_attachment" "web_fw" {
firewall_id = hcloud_firewall.web.id
server_ids = [hcloud_server.web.id]
}
By default all inbound traffic is blocked. Only open what you need.
Volume Storage
For persistent storage separate from the server:
# volume.tf
resource "hcloud_volume" "data" {
name = "data-volume"
size = 50 # GB
location = "fsn1"
format = "ext4"
}
resource "hcloud_volume_attachment" "data_attachment" {
volume_id = hcloud_volume.data.id
server_id = hcloud_server.web.id
automount = true
}
The volume is mounted automatically. If the server dies, data on the volume remains.
Load Balancer
To distribute traffic across multiple servers:
# load_balancer.tf
resource "hcloud_load_balancer" "web_lb" {
name = "web-lb"
load_balancer_type = "lb11"
location = "fsn1"
}
resource "hcloud_load_balancer_network" "lb_network" {
load_balancer_id = hcloud_load_balancer.web_lb.id
network_id = hcloud_network.private.id
ip = "10.0.1.1"
}
resource "hcloud_load_balancer_service" "http" {
load_balancer_id = hcloud_load_balancer.web_lb.id
protocol = "http"
listen_port = 80
destination_port = 80
health_check {
protocol = "http"
port = 80
interval = 10
timeout = 5
retries = 3
http {
path = "/health"
status_codes = ["200"]
}
}
}
resource "hcloud_load_balancer_target" "web_target" {
load_balancer_id = hcloud_load_balancer.web_lb.id
type = "server"
server_id = hcloud_server.web.id
use_private_ip = true
}
output "lb_ip" {
value = hcloud_load_balancer.web_lb.ipv4
}
The load balancer costs about €5/month. If you have significant traffic or want HA, it's worth it.
Cloud-init for Configuration
Instead of manually configuring servers, use cloud-init:
# server_with_cloudinit.tf
resource "hcloud_server" "app" {
name = "app-server"
image = "ubuntu-22.04"
server_type = "cx22"
location = "fsn1"
ssh_keys = [hcloud_ssh_key.default.id]
user_data = <<-EOF
#cloud-config
package_update: true
packages:
- docker.io
- docker-compose
runcmd:
- systemctl enable docker
- systemctl start docker
- usermod -aG docker ubuntu
write_files:
- path: /etc/environment
content: |
ENVIRONMENT=production
EOF
}
On first boot, the server installs Docker and configures the environment. No manual SSH.
State Management
For serious projects, don't keep state local.
S3 Backend (with Hetzner Object Storage)
Hetzner has S3-compatible object storage:
terraform {
backend "s3" {
bucket = "terraform-state"
key = "hetzner/terraform.tfstate"
region = "eu-central-1"
endpoint = "https://fsn1.your-objectstorage.com"
skip_credentials_validation = true
skip_metadata_api_check = true
skip_region_validation = true
}
}
Terraform Cloud
Free alternative for small teams:
terraform {
cloud {
organization = "your-org"
workspaces {
name = "hetzner-production"
}
}
}
Tips and Best Practices
Use different locations for HA. Hetzner has data centers in Falkenstein (fsn1), Nuremberg (nbg1), Helsinki (hel1), and US (ash, hil). Distribute for fault tolerance.
Snapshot before risky changes. Hetzner supports server snapshots. Take them before important upgrades.
Monitor costs. Hetzner is cheap, but costs add up. Use labels to track who uses what.
Consider ARM. CAX (ARM) servers have excellent price/performance ratio. If your software supports ARM, evaluate them.
Terraform state locking. If working in a team, ensure the backend supports locking to avoid conflicts.
Conclusion
Hetzner + Terraform is a powerful combination for those who want cloud infrastructure without spending a fortune. The limitations exist (fewer managed services, fewer regions), but for many use cases they're not a problem.
If you're paying hundreds of euros per month on AWS for servers that could run on Hetzner, do the math. The savings can be significant, and with Terraform management isn't more complicated.
It's not for everyone. If you need specific AWS/GCP services, stay there. But if you primarily need servers, networks, and storage, Hetzner is an option worth considering.
The best cloud is the one that solves your problem at the right cost. Sometimes it's not the biggest.