HT-005 - Hetzner VPS Setup

Overview

Hetzner Cloud is a German cloud provider offering high-value VPS instances at a fraction of US provider costs. Running n8n on Docker Desktop on a local Windows machine creates a single point of failure --- if the machine goes down, the Roofing Lead Engine and all client-facing automations go down with it. This guide covers purchasing a Hetzner Cloud VPS, deploying Docker + n8n + NTFY on it, and migrating the Atlantis automation stack off the local machine for 24/7 production uptime.


💡 Why Hetzner over Linode/Hostinger? CX22 at Ashburn VA (\~\$6-8/mo) delivers 2 vCPU, 4GB RAM, 40GB NVMe --- equivalent specs to Linode\'s \$18/mo shared plan. For a bootstrap-stage platform, the savings compound fast. US location (Ashburn VA) keeps latency low (\~20-40ms from Gainesville GA) and answers the client question \'where does my data live?\' with \'a server in Virginia.\'


When to Use This Guide

  • Migrating n8n off local Docker Desktop to a production cloud server

  • Eliminating local machine as a single point of failure for client-facing webhooks

  • Setting up a persistent, always-on automation engine for the Atlantis SaaS platform

  • Standing up NTFY self-hosted notifications on a stable server

  • Replacing or supplementing the current Cloudflare Tunnel → local machine setup

Plan Selection --- What to Buy

Recommended: Hetzner CX22 --- Ashburn, VA (US East)


Plan vCPU / RAM Monthly Atlantis Use (USD) Case

CX22 2 vCPU / 4GB RAM / \~\$6--8 ✅ Sweet spot --- 40GB NVMe n8n + Docker + NTFY + light web services

CX32 4 vCPU / 8GB RAM / \~\$12--15 Upgrade path --- 80GB NVMe add Portainer, Grafana, future containers

CAX21 (ARM) 4 vCPU / 8GB RAM / \~\$7--10 Budget ARM option 80GB NVMe --- verify n8n Docker image ARM support first

CCX series 8+ vCPU / 16GB+ \$20+ Overkill --- save for scale phase



⚠️ US locations cap at 1TB/month included traffic (EU locations get 20TB free). For n8n webhooks and NTFY pushes, 1TB is more than sufficient. Monitor usage in the Hetzner console if you add heavy outbound workflows.


Step 1 --- Create a Hetzner Account

  1. Go to: https://www.hetzner.com/cloud

  2. Click Get Started → Register

  3. Enter email, set password, complete email verification

  4. Add payment method --- credit card or PayPal (Hetzner uses hourly billing --- you are only charged for uptime)

  5. Hetzner may request identity verification for new accounts --- have a government ID ready. This is standard practice and typically clears within a few hours.


💡 Hourly billing means you can spin up a server, test it, and destroy it for under \$0.01. Use this to validate the setup before committing to a full month.



📸 SCREENSHOT PLACEHOLDER --- Hetzner Cloud console --- new project creation screen


Step 2 --- Create a Project

  1. In the Hetzner Cloud Console: click + New Project

  2. Name it: atlantis-vps

  3. Click Add Project

Projects in Hetzner are logical containers for servers, volumes, and SSH keys. Keeping Atlantis resources in one project makes billing and access management clean.


📸 SCREENSHOT PLACEHOLDER --- Hetzner Console --- atlantis-vps project created


Step 3 --- Add Your SSH Key

You need an SSH key pair to authenticate to the server. If you already have one (e.g. from GitHub), reuse it. If not, generate one in PowerShell:

+-----------------------------------------------------------------------+ | # Run in PowerShell on your Windows machine | | | | ssh-keygen -t ed25519 -C \'atlantis-hetzner\' | | | | # Accept default path: C:\Users\Shane\.ssh\id_ed25519 | | | | # Set a passphrase (recommended) or leave blank | | | | # Copy the public key to clipboard | | | | Get-Content \$env:USERPROFILE\.ssh\id_ed25519.pub | Set-Clipboard | +-----------------------------------------------------------------------+

  1. In Hetzner Console → Security → SSH Keys → Add SSH Key

  2. Paste your public key → Name it: shane-atlantis → Add


⚠️ Never paste your private key (id_ed25519) anywhere. Only the .pub file goes into Hetzner.



📸 SCREENSHOT PLACEHOLDER --- Hetzner Console --- SSH key added successfully


Step 4 --- Create the Server

  1. Inside the atlantis-vps project → click + Create Server

4.1 Location

  • Select: Ashburn, VA (US East) --- closest to Gainesville GA, \~20-40ms latency

4.2 OS Image

  • Select: Ubuntu 24.04 LTS --- best Docker support, long-term support through 2029

4.3 Server Type

  • Select: Shared vCPU → CX22 (2 vCPU, 4GB RAM, 40GB NVMe) → \~\$6-8/mo

4.4 Networking

  • Public IPv4: ✅ Enable (required for external access)

  • Public IPv6: ✅ Enable (free, future-proof)

  • Private Network: Skip for now --- add later if expanding to multiple servers

4.5 SSH Keys

  • Select: shane-atlantis (the key you added in Step 3)

4.6 Server Name

  • Name it: atlantis-n8n-prod

  • Click Create & Buy Now

  • Server provisions in \~30 seconds. Note the public IPv4 address.


📸 SCREENSHOT PLACEHOLDER --- Hetzner Console --- atlantis-n8n-prod server created, showing IPv4 address


Step 5 --- Connect to the Server

+-----------------------------------------------------------------------+ | # In PowerShell on your Windows machine | | | | # Replace 1.2.3.4 with your actual server IPv4 | | | | ssh root@1.2.3.4 | | | | # First connection --- accept the fingerprint prompt: yes | | | | # You are now logged in as root | +-----------------------------------------------------------------------+


💡 Save the server IP in your password manager and in the Atlantis memory file. You\'ll use it often.


Step 6 --- Initial Server Hardening

Do this before installing anything else. An unprotected fresh server gets probed within minutes of creation.

+-----------------------------------------------------------------------+ | # Update system packages | | | | apt update && apt upgrade -y | | | | # Create a non-root user | | | | adduser shane | | | | usermod -aG sudo shane | | | | # Copy SSH key to new user | | | | rsync --archive --chown=shane:shane \~/.ssh /home/shane | | | | # Disable root SSH login | | | | nano /etc/ssh/sshd_config | | | | # Set: PermitRootLogin no | | | | # Set: PasswordAuthentication no | | | | systemctl restart sshd | | | | # Set up UFW firewall | | | | ufw allow OpenSSH | | | | ufw allow 80/tcp | | | | ufw allow 443/tcp | | | | ufw allow 5678/tcp # n8n | | | | ufw allow 8080/tcp # NTFY | | | | ufw enable | | | | ufw status | +-----------------------------------------------------------------------+


⚠️ After disabling root login and enabling UFW --- test SSH as your new user in a NEW terminal window BEFORE closing the root session. Locking yourself out on a fresh server means a console recovery.


Step 7 --- Install Docker

+-----------------------------------------------------------------------+ | # Switch to your non-root user if you haven\'t already | | | | su - shane | | | | # Install Docker via official script | | | | curl -fsSL https://get.docker.com | sh | | | | # Add user to docker group (no sudo needed for docker commands) | | | | sudo usermod -aG docker \$USER | | | | newgrp docker | | | | # Verify | | | | docker --version | | | | docker compose version | +-----------------------------------------------------------------------+


📸 SCREENSHOT PLACEHOLDER --- Terminal --- Docker version output confirming install


Step 8 --- Deploy n8n via Docker Compose

+-----------------------------------------------------------------------+ | # Create project directory | | | | mkdir -p \~/atlantis/n8n && cd \~/atlantis/n8n | | | | # Create .env file (replace values with your actual secrets) | | | | cat > .env \<\< \'EOF\' | | | | N8N_HOST=n8n.atlantisits.co | | | | N8N_PORT=5678 | | | | N8N_PROTOCOL=https | | | | N8N_BASIC_AUTH_ACTIVE=true | | | | N8N_BASIC_AUTH_USER=admin | | | | N8N_BASIC_AUTH_PASSWORD=your_strong_password_here | | | | WEBHOOK_URL=https://n8n.atlantisits.co/ | | | | GENERIC_TIMEZONE=America/New_York | | | | EOF | +-----------------------------------------------------------------------+

+-----------------------------------------------------------------------+ | # Create docker-compose.yml | | | | cat > docker-compose.yml \<\< \'EOF\' | | | | version: \'3.8\' | | | | services: | | | | n8n: | | | | image: n8nio/n8n | | | | restart: always | | | | ports: | | | | - \'5678:5678\' | | | | env_file: .env | | | | volumes: | | | | - n8n_data:/home/node/.n8n | | | | networks: | | | | - atlantis | | | | volumes: | | | | n8n_data: | | | | networks: | | | | atlantis: | | | | name: atlantis | | | | EOF | | | | # Start n8n | | | | docker compose up -d | | | | # Verify running | | | | docker compose ps | | | | docker compose logs n8n --tail=20 | +-----------------------------------------------------------------------+


💡 The n8n_data Docker volume persists your workflows across container restarts and updates. This is the equivalent of the docker_data.vhdx on your local Windows machine --- it survives reboots automatically.



📸 SCREENSHOT PLACEHOLDER --- Terminal --- docker compose ps showing n8n container running


Step 9 --- Deploy NTFY (Self-Hosted Notifications)

+-----------------------------------------------------------------------+ | # Add NTFY to the same docker-compose.yml | | | | # Append under services: | | | | ntfy: | | | | image: binwiederhier/ntfy | | | | restart: always | | | | ports: | | | | - \'8080:80\' | | | | volumes: | | | | - ntfy_data:/var/cache/ntfy | | | | - ntfy_config:/etc/ntfy | | | | command: serve | | | | networks: | | | | - atlantis | | | | # Add to volumes section: | | | | ntfy_data: | | | | ntfy_config: | | | | # Restart stack | | | | docker compose down && docker compose up -d | +-----------------------------------------------------------------------+

NTFY is now accessible at http://[server-ip]:8080. Update the n8n Roofing Lead Engine workflow to replace the Telegram node with an HTTP Request node pointing to your NTFY topic URL.

Step 10 --- Point Cloudflare DNS to the VPS

Update DNS records so n8n.atlantisits.co routes to the Hetzner server instead of your local Cloudflare Tunnel.


Setting Value

Record type A

Name n8n

IPv4 address [Your Hetzner server IPv4]

Proxy status DNS Only (Grey Cloud)

TTL Auto



⚠️ Keep proxy status as DNS Only (Grey Cloud) for n8n --- same rule as your existing Vercel subdomains. Proxied (Orange Cloud) breaks WebSocket connections that n8n depends on.


After DNS propagates (\~2-5 minutes for Cloudflare), n8n should be accessible at https://n8n.atlantisits.co:5678. For clean HTTPS on port 443, add an NGINX reverse proxy (see future HT-006).


📸 SCREENSHOT PLACEHOLDER --- Cloudflare DNS panel --- n8n A record pointing to Hetzner IP


Step 11 --- Migrate Workflows from Local n8n

  1. On your LOCAL n8n (localhost:5678 or n8n.atlantisits.co via old tunnel): Settings → Export → Download all workflows as JSON

  2. On the NEW Hetzner n8n: Settings → Import → Upload the exported JSON file

  3. Re-enter credentials (API keys, webhook secrets) --- credentials are not exported for security reasons

  4. Update Vercel environment variable N8N_WEBHOOK_URL to point to the new server URL

  5. Test the Roofing Lead Engine end-to-end --- submit a test lead and verify all 3 notification channels fire (Discord, NTFY, Twilio)

  6. Once confirmed working --- decommission the local Docker Compose stack to free up resources


💡 Credentials must be re-entered manually in n8n after migration. Keep your Vercel env vars (N8N_WEBHOOK_URL, ATLANTIS_WEBHOOK_SECRET) updated to match the new server. These are already secured in Vercel --- just update the URL value.



📸 SCREENSHOT PLACEHOLDER --- n8n workflow editor --- Roofing Lead Engine imported and active on Hetzner


Troubleshooting


Issue Cause Fix

Can\'t SSH after Root login Use Hetzner Console rescue mode to disabling root disabled before re-enable testing new user

n8n not reachable on UFW blocking port sudo ufw allow 5678/tcp && ufw :5678 reload

Docker permission User not in docker sudo usermod -aG docker \$USER && denied group newgrp docker

Workflows missing Credentials Re-enter API keys manually in n8n after import excluded from credentials export

Hetzner account New account --- Normal --- typically clears within verification pending German compliance hours, check email

n8n webhook URL broken Vercel env var Update N8N_WEBHOOK_URL in Vercel after migration still points to dashboard → redeploy old tunnel URL

NTFY push not received Topic URL mismatch Verify URL format: in n8n node http://[ip]:8080/[topic-name]


Quick Reference


Item Value

Recommended plan CX22 --- 2 vCPU, 4GB RAM, 40GB NVMe

Location Ashburn, VA (US East)

OS Ubuntu 24.04 LTS

Monthly cost \~\$6--8 USD

n8n port 5678

NTFY port 8080

n8n Docker volume n8n_data (persists across restarts)

Cloudflare DNS type A Record --- DNS Only (Grey Cloud)

Hetzner Console https://console.hetzner.cloud

Hetzner Docs https://docs.hetzner.com/cloud

Working directory D:\Data\AtlantisITS (local) / \~/atlantis (VPS)


Atlantis Infrastructure Impact


Before Hetzner VPS After Hetzner VPS

n8n on local Docker Desktop (SPOF) ✅ n8n on cloud --- 24/7 uptime, no local dependency

Lead Engine down when PC reboots ✅ Lead Engine survives PC off, updates, sleep

Cloudflare Tunnel required for ✅ Direct A record --- simpler, webhooks more reliable

\~\$0/mo infrastructure cost \~\$6--8/mo --- cheapest production-grade option available

Not sellable as a SaaS platform ✅ Platform ready --- pitch contractors with confidence