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
kamaluser 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
- Create a Hetzner Cloud account and generate an API token
- Have your SSH public key ready for the kamal deploy user
- Create a
terraform.tfvarsfile (seeterraform.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.
- Run:
terraform init
terraform plan # Review what will be created
terraform apply # Provision the server
- 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:
- Point your domain's DNS A records to the server IP
- Update
config/deploy.ymlin the app repo with the server IP and your hostnames - Run
kamal setupto 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.