Self-hosted OpenClaw gateway on a Hetzner VPS with zero-trust Tailscale networking. No public ports exposed. ~€11.39/month.
This is a reference template. Clone it and adapt for your own deployment — the config values (timezone, model, cron prompts) are working examples you'll customize.
- Cheap: Hetzner CX43 x86 (8 vCPU, 16 GB) ~€9.49/mo + backups (~€11.39/mo total)
- Secure: Hetzner firewall + UFW + Tailscale-only access + device pairing
- Simple: Pulumi IaC, single command deploy, systemd user service
- Telegram: Optional scheduled tasks (configurable cron jobs)
- Workspace sync: Optional hourly git backup of the agent's workspace to GitHub
- Node.js 18+
- Pulumi CLI
- Ansible (
pip install ansible) - Tailscale installed and connected on your machine
- Hetzner Cloud API token (console.hetzner.cloud)
- Tailscale auth key (login.tailscale.com/admin/settings/keys)
- Tailscale MagicDNS and HTTPS enabled (login.tailscale.com/admin/dns) — required for Tailscale Serve
- Claude setup token (run
claude setup-token)
See CLAUDE.md for detailed setup instructions.
If you've never used Tailscale before:
-
Create account: Go to https://tailscale.com/start
- Sign up with GitHub (recommended for infra projects), Google, or email
- Free tier supports up to 100 devices
-
Install on your Mac:
brew install --cask tailscale
- Open Tailscale from Applications
- Click "Allow" for System Extension and VPN Configuration prompts
- Click menu bar icon → Log in → Authorize in browser
-
Generate auth key for server:
- Go to https://login.tailscale.com/admin/settings/keys
- Click "Generate auth key"
- Enable: Reusable, Ephemeral
- Copy the key (starts with
tskey-auth-...)
To enable optional Telegram notifications:
-
Create a bot: Open Telegram, search for @BotFather, send
/newbot- Choose a display name (e.g., "OpenClaw Assistant")
- Choose a username (must end in "bot", e.g.,
openclaw_assistant_bot) - Copy the bot token (format:
123456789:ABCdefGHIjklMNOpqrsTUVwxyz)
-
Get your user ID (either method):
- Run
./scripts/get-telegram-id.sh— briefly pauses the gateway, you send a message, it shows your IDs - Or search for @userinfobot on Telegram, send
/start, copy your numeric user ID
- Run
-
Configure: See Quick Start below for the Pulumi commands.
npm install
cd pulumi
pulumi login # Authenticate with Pulumi Cloud
pulumi stack init prod
# Required
pulumi config set hcloud:token --secret # Hetzner API token
pulumi config set tailscaleAuthKey --secret # Tailscale auth key
pulumi config set claudeSetupToken --secret # From `claude setup-token`
# Optional: Telegram notifications (daily digests, weekly planning)
pulumi config set telegramBotToken --secret # From @BotFather
pulumi config set telegramUserId "YOUR_ID" # ./scripts/get-telegram-id.sh or @userinfobot
# Optional: hourly workspace backup to a private GitHub repo
pulumi config set workspaceRepoUrl "git@github.com:YOU/openclaw-workspace.git"
# Deploy
pulumi up
# Verify (wait ~5 min for cloud-init)
cd ..
./scripts/verify.shAfter verifying, clean up the cloud-init log (contains secrets):
ssh ubuntu@openclaw-vps.<tailnet>.ts.net "sudo shred -u /var/log/cloud-init-openclaw.log"
State and secrets are managed by Pulumi Cloud — no local passphrase needed.
Wait ~5 minutes after deployment for cloud-init + Ansible to finish, then open:
https://openclaw-vps.<tailnet>.ts.net/chat
OpenClaw requires device pairing for all connections — including the server's own CLI. On a fresh install:
-
Use the tokenized URL to access the web UI without pairing:
cd pulumi && pulumi stack output tailscaleUrlWithToken --show-secrets
Open that URL in your browser. This bypasses pairing for initial setup.
-
Approve devices that need pairing. The server's CLI (used by Ansible) may show as pending:
ssh ubuntu@openclaw-vps.<tailnet>.ts.net 'openclaw devices list' ssh ubuntu@openclaw-vps.<tailnet>.ts.net 'openclaw devices approve <request-id>'
-
If Ansible failed during first deploy (cron setup skipped due to pairing), re-run after approving:
./scripts/provision.sh --tags telegram
No public SSH port is exposed. SSH works over Tailscale only.
See CLAUDE.md for details and docs/TROUBLESHOOTING.md for common issues.
Your Machine ──(Tailscale)──> Hetzner VPS ──> OpenClaw Gateway
Hetzner FW: no inbound
UFW: tailscale0 only
Gateway: localhost:18789 (systemd --user)
Tailscale Serve: HTTPS proxy
- CLAUDE.md — Setup, operations, security, and troubleshooting
- docs/SECURITY.md — Threat model and mitigations
- docs/TROUBLESHOOTING.md — Common issues
- docs/BROWSER-CONTROL-PLANNING.md — Future browser automation approaches