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
-
Go to: https://www.hetzner.com/cloud
-
Click Get Started → Register
-
Enter email, set password, complete email verification
-
Add payment method --- credit card or PayPal (Hetzner uses hourly billing --- you are only charged for uptime)
-
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
-
In the Hetzner Cloud Console: click + New Project
-
Name it: atlantis-vps
-
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 | +-----------------------------------------------------------------------+
-
In Hetzner Console → Security → SSH Keys → Add SSH Key
-
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
- 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
-
On your LOCAL n8n (localhost:5678 or n8n.atlantisits.co via old tunnel): Settings → Export → Download all workflows as JSON
-
On the NEW Hetzner n8n: Settings → Import → Upload the exported JSON file
-
Re-enter credentials (API keys, webhook secrets) --- credentials are not exported for security reasons
-
Update Vercel environment variable N8N_WEBHOOK_URL to point to the new server URL
-
Test the Roofing Lead Engine end-to-end --- submit a test lead and verify all 3 notification channels fire (Discord, NTFY, Twilio)
-
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