Skip to content

lukmanc405/hermes-stack

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

hermes-stack

Provision a hardened single-node host on Hetzner Cloud and run services behind a Cloudflare Tunnel — zero inbound HTTP ports, no public origin IP.

Built from a self-hosted infra pattern I run for AI agents, scrapers, and small services. The same playbook works for any single-node deployment.

What you get

  • Terraform — creates the server, SSH key, and a tight firewall (SSH only by default).
  • Ansible — hardens SSH, sets up ufw + fail2ban, installs cloudflared, deploys your services as systemd units.
  • Cloudflare Tunnel (optional) — outbound-only connection to Cloudflare. Public traffic hits Cloudflare; the origin IP stays private.
  • Idempotent — re-run anytime; nothing breaks.

Stack

Layer Tool Why
Provision Terraform + hetznercloud/hcloud reproducible servers, cheap (cx22 ~€4/mo)
Configure Ansible declarative roles, no agent on the box
Tunnel cloudflared no inbound ports, free, DDoS-fronted
Service mgmt systemd units (templated) restart, journal, no Docker required

Quickstart

1. Prereqs

# macOS
brew install terraform ansible

# Debian/Ubuntu
sudo apt install -y terraform ansible

# Need a Hetzner Cloud project + API token: https://console.hetzner.cloud
export TF_VAR_hcloud_token=hcloud_...

2. Provision

cd terraform
cp terraform.tfvars.example terraform.tfvars  # edit values
terraform init
terraform apply

Outputs the server IP and a ready-to-run SSH command. Also writes ansible/inventory.ini.

3. Configure

cd ../ansible
ansible-playbook -i inventory.ini site.yml

This installs base packages, hardens SSH, enables ufw + fail2ban, and applies any services you defined.

4. (Optional) Add Cloudflare Tunnel

Create a tunnel in the Cloudflare Zero Trust dashboard, copy the connector token, then:

export CF_TUNNEL_TOKEN=eyJh...
cd ansible
ansible-playbook -i inventory.ini site.yml -e cloudflare_tunnel_enabled=true

Route hostnames to http://localhost:PORT from the dashboard. No inbound ports.

Adding a service

Edit ansible/group_vars/all.yml:

managed_services:
  - name: myapp
    description: "My App"
    user: root
    working_directory: /opt/myapp
    exec_start: "/usr/bin/node /opt/myapp/server.js"
    environment:
      NODE_ENV: production
      PORT: "8080"
    restart: always

Re-run the playbook. The unit is templated, enabled, and started.

Layout

.
├── terraform/
│   ├── main.tf                 server + firewall + SSH key
│   ├── variables.tf            tunable inputs
│   ├── outputs.tf              IP, SSH command
│   └── terraform.tfvars.example
└── ansible/
    ├── site.yml                top-level play
    ├── group_vars/all.yml      defaults — override here
    └── roles/
        ├── common/             packages, timezone, auto-upgrades
        ├── hardening/          SSH config, ufw, fail2ban
        ├── cloudflare-tunnel/  cloudflared install + service
        └── services/           systemd unit templater

Cost

Hetzner cx22 (2 vCPU / 4 GB / 40 GB) is around €4/month. Cloudflare Tunnel is free.

Security notes

  • SSH password auth is disabled. Use keys.
  • ssh_allow_from defaults to 0.0.0.0/0 — narrow it to your IP for production.
  • expose_http is false by default. Keep it that way if you front everything with a tunnel.
  • All variables are non-sensitive. Tokens (TF_VAR_hcloud_token, CF_TUNNEL_TOKEN) come from the environment, never tfvars.

License

MIT

About

Hetzner + Cloudflare Tunnel IaC — Terraform provisioning, Ansible hardening, systemd services

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors