Docs

Terraform Infrastructure

Stacknaut includes a separate Terraform configuration that provisions a security-hardened Hetzner Cloud server, ready for Kamal deployment. One command gives you a production server with Docker, firewall, fail2ban, and automatic security updates.

Why Hetzner

Hetzner offers significantly better pricing than AWS, DigitalOcean, or other cloud providers for the same specs. A CPX11 instance (2 vCPU, 2GB RAM) costs around EUR 5/month — enough to run multiple SaaS products on a single server while you find product-market fit.

You own the server. No vendor lock-in, no surprise bills, no cold starts.

What Gets Provisioned

Running terraform apply creates:

  • Hetzner Cloud server — Ubuntu 24.04, configured via cloud-init
  • Cloud firewall — Allows only SSH (22), HTTP (80), and HTTPS (443)
  • Private network — For secure internal communication if you add more servers later

Server Hardening

The cloud-init configuration automatically applies these security measures on first boot:

SSH hardening:

  • Root login disabled
  • Password authentication disabled
  • Only public key authentication
  • Only the kamal user can SSH in

Firewall (UFW):

  • Default deny incoming
  • Default allow outgoing
  • Allows SSH, HTTP, HTTPS only

Fail2ban:

  • SSH brute force protection
  • Bans IPs after 3 failed attempts for 10 minutes

Network security:

  • IP spoofing protection
  • ICMP redirect blocking
  • Source packet routing disabled
  • SYN flood protection
  • Cloudflare DNS (1.1.1.1)

Automatic updates:

  • Unattended security upgrades enabled
  • Unused packages auto-removed

Performance:

  • BBR TCP congestion control
  • 2GB swap file
  • Increased file descriptor limits
  • Daily Docker cleanup cron (removes images older than 48 hours)

Infrastructure Files

terraform-hetzner/
  main.tf           — Server, firewall, and network resources
  variables.tf      — Configurable parameters (server type, location, etc.)
  providers.tf      — Hetzner provider configuration
  outputs.tf        — Server IP output after provisioning
  data.tf           — Cloud-init template composition
  cloudinit/
    base.yml.tpl    — Base server config (users, SSH, security, packages)
    server.yml      — Server-specific config (swap, Docker cleanup)
  backup-state.sh   — Script to back up Terraform state

Setup

  1. Create a Hetzner Cloud account and generate an API token
  2. Have your SSH public key ready for the kamal deploy user
  3. Create a terraform.tfvars file (see terraform.tfvars.example):
hcloud_token         = "your-hetzner-api-token"
ssh_public_key       = "ssh-ed25519 AAAA... your-key"
ssh_kamal_public_key = "ssh-ed25519 AAAA... kamal-key"
server_name          = "my-server"

The server also picks up all SSH keys already registered in your Hetzner project. Root login is disabled after setup — you'll SSH in as the kamal user.

  1. Run:
terraform init
terraform plan    # Review what will be created
terraform apply   # Provision the server
  1. Note the server IP from the output — you'll need it for config/deploy.yml

Configuration Options

Variable Default Description
server_type cpx11 Server size (cpx11 = 2 vCPU, 2GB RAM)
location fsn1 Datacenter (Falkenstein, Germany)
os_image ubuntu-24.04 Operating system
server_name vps Server hostname
private_network_range 10.0.1.0/24 Private network CIDR

Hetzner locations: fsn1 (Falkenstein), nbg1 (Nuremberg), hel1 (Helsinki). Note: the private network zone is hard-coded to eu-central, so EU locations work out of the box. For US locations (ash, hil), you'll need to update the network zone in main.tf.

After Provisioning

Once the server is up:

  1. Point your domain's DNS A records to the server IP
  2. Update config/deploy.yml in the app repo with the server IP and your hostnames
  3. Run kamal setup to deploy your app for the first time

The server reboots once after initial provisioning to apply all changes. Wait a minute before attempting SSH or deployment.

7a9a8821

© 2026 Stacknaut