VPS Setup (Hostinger)
How to set up a fresh Hostinger VPS for running OpenClaw.
Overview
All VPS scripts live in scripts/vps/:
| Script | Purpose | Runs on |
|---|---|---|
provision.sh | Orchestrator — creates VPS or runs modules on existing one | Your Mac |
modules/1password.sh | Install 1Password CLI, configure secrets injection | VPS |
modules/ssh-hardening.sh | Harden SSH — key-only auth, no passwords | VPS |
modules/firewall.sh | Configure iptables with profile-based rules | VPS |
modules/webserver.sh | Nginx + webhook auto-deploy + certbot | VPS |
lib/common.sh | Shared utilities (logging, OS detection, package install) | VPS |
Quick Start
New VPS
# Full provisioning: create VPS + run all modules
just vps-new --alias openclaw-vps-3
Note: Most VPS operations are initiated through
justrecipes. Runjust --listto see all available tasks. The underlying scripts (scripts/vps/provision.sh, etc.) can be called directly, butjustis the standard entry point.
The script will:
- Prompt for plan, data center, and template (with sensible defaults)
- Fetch credentials from 1Password (API token, SSH key, service account token)
- Create the VPS via the Hostinger API with your SSH key injected
- Wait for the VPS to become ready
- Run all modules remotely (1Password, SSH, firewall, webserver)
- Add an entry to
~/.ssh/config - Store VPS details in 1Password (Shared-Infrastructure vault)
- Verify SSH and
openclaw-runwork
Register an Existing VPS
If you have a VPS that was created outside of just vps-new (e.g., via the Hostinger panel), register it first:
# 1. List VPSes from Hostinger API to find hostname/IP
just vps-list
# 2. Register it — adds host key + SSH config with 1Password agent
just vps-add openclaw-vps-2 srv1460704.hstgr.cloud
# 3. Test SSH
ssh openclaw-vps-2 echo ok
vps-add does two things:
- Scans the host key and adds it to
~/.ssh/known_hosts - Adds an entry to
~/.ssh/configusing the 1Password SSH agent
After registering, you can run modules:
# All modules
just vps openclaw-vps-2 --all
# Just SSH hardening
just vps openclaw-vps-2 --ssh
# Firewall + webserver
just vps openclaw-vps-2 --firewall web --webserver
VPS Commands Reference
| Command | Purpose |
|---|---|
just vps-list | List all VPSes with hostname, IP, state, and detected role |
just vps-add ALIAS HOST | Register an existing VPS in SSH config |
just vps-new --alias NAME | Create a new VPS via Hostinger API + run all modules |
just vps HOST --all | Run all modules on an existing VPS |
just vps HOST --ssh | SSH hardening only |
just vps HOST --1password | 1Password CLI setup only |
just vps HOST --firewall PROFILE | Firewall only (profiles: base, web, custom) |
just vps HOST --webserver | Nginx + webhook + certbot only |
After provisioning, connect with:
ssh <your-alias> # e.g. ssh openclaw-vps-3
ssh <your-alias> openclaw-run env # verify secrets injection
Module Details
modules/1password.sh
Sets up secrets management so the app can access API keys without storing them in files or env vars:
- Installs 1Password CLI (
op) — supports both Debian/Ubuntu (apt) and Arch (binary) - Creates
/etc/openclaw/for sensitive config (mode 700) - Stores the Service Account Token in
/etc/openclaw/op-token.env(mode 600) - Verifies access to the Shared-Infrastructure vault
- Creates
/opt/openclaw/.env.tpl— maps env vars toop://secret references - Creates
/usr/local/bin/openclaw-run— helper that injects secrets at runtime - Creates
/docker/openclaw-00kx/start.sh— resolves secrets and starts the Docker container - Enables
openclaw-secretssystemd service — runsstart.shon boot so the container starts with fresh secrets automatically
Modes:
- Interactive (default): Prompts you to paste the service account token
- Non-interactive (
--token TOKEN): Uses the provided token directly - Non-interactive (
--token-env): Reads token fromOP_SA_TOKENenv var
modules/ssh-hardening.sh
Locks down SSH so only key-based auth works:
- Adds the provided SSH public key to
~/.ssh/authorized_keys - Backs up
/etc/ssh/sshd_config - Disables password authentication, empty passwords, and PAM
- Sets root login to key-only (
prohibit-password) - Validates the config and restarts the SSH daemon
Why piped from Mac: The script needs the SSH public key as an argument. Since the key is in the Shared-Infrastructure vault (which the VPS service account can't access), we fetch it locally with op and pass it in.
modules/firewall.sh
Configures iptables rules using profiles:
| Profile | Ports |
|---|---|
base | 22 (SSH only) |
web | 22, 80, 443 |
custom | Only the ports you specify |
Extra ports can be appended to any profile:
# web profile + port 9000 for webhook
just vps HOST --firewall web 9000
modules/webserver.sh
Sets up nginx, webhook auto-deploy, and certbot for a site:
- Installs nginx, git, webhook, certbot
- Clones the repo to
/var/www/openclaw - Configures nginx with the site and webhook proxy (
/hooks/→ port 9000) - Sets up the webhook listener as a systemd service
- Creates the GitHub webhook (if
ghis authenticated) - Verifies the deployment
Configurable via environment variables:
DOMAIN=example.com SITE_DIR=mysite bash modules/webserver.sh
Hostinger API
The provisioning script uses the Hostinger API to create VPSes programmatically.
Authentication
API requests use a Bearer token stored in 1Password:
Shared-Infrastructure > Hostinger API Token
Common Values
| Field | Value | Meaning |
|---|---|---|
item_id | hostingercom-vps-kvm2-usd-1m | KVM 2 plan, monthly |
data_center_id | 17 | Boston |
template_id | 1121 | Ubuntu 24.04 with Docker |
Usage After Setup
# Verify secrets are available
openclaw-run env | grep -i api
# Run any command with all secrets injected
openclaw-run python main.py
Channel Setup
After the container is running, configure each messaging channel. Use ssh -t (not plain ssh) for any interactive commands — the -t flag allocates a TTY through the SSH connection.
Telegram
-
Create a bot via @BotFather on Telegram and save the token in 1Password (Shared-Infrastructure vault, item
Telegram Bot Token). -
Set the channel to allowlist mode with your Telegram user ID:
ssh openclaw-vps-1 docker exec openclaw-00kx-openclaw-1 \
openclaw config set channels.telegram.dmPolicy allowlist
ssh openclaw-vps-1 docker exec openclaw-00kx-openclaw-1 \
openclaw config set 'channels.telegram.allowFrom' '["YOUR_TELEGRAM_USER_ID"]'To find your user ID: message the bot while it's in
pairingmode — it will show your ID in the response. -
Restart the container to apply:
ssh openclaw-vps-1 docker restart openclaw-00kx-openclaw-1 -
Test by messaging the bot on Telegram.
Note: Do not use
dmPolicy: pairing— pairing approvals don't persist across gateway restarts. Useallowlistinstead.
WhatsApp
WhatsApp uses selfChatMode — you message yourself and the bot responds.
-
Link WhatsApp Web by scanning a QR code:
ssh -t openclaw-vps-1 docker exec -it openclaw-00kx-openclaw-1 \
openclaw channels login --channel whatsappScan the QR with WhatsApp on your phone (Settings > Linked Devices > Link a Device).
-
Restart the container after linking — the gateway needs a restart to pick up the new WhatsApp session:
ssh openclaw-vps-1 docker restart openclaw-00kx-openclaw-1 -
Test by sending a message to yourself on WhatsApp.
Note: The restart after linking is required. Without it, WhatsApp shows as "not linked" even though
channels loginsaid "Linked!".
Choosing the AI Model
The default model is set in the OpenClaw config. Change it and restart:
ssh openclaw-vps-1 docker exec openclaw-00kx-openclaw-1 \
openclaw config set agents.defaults.model.primary "Claude Sonnet 4.5"
ssh openclaw-vps-1 docker restart openclaw-00kx-openclaw-1
Available models:
| Model | Provider | Speed | Notes |
|---|---|---|---|
Claude Haiku 4.5 | Anthropic | Fast | Good for quick replies |
Claude Sonnet 4.5 | Anthropic | Medium (~10s) | Balanced quality/speed |
Claude Opus 4.6 | Anthropic | Slow | Highest quality |
ChatGPT 5.2 | OpenAI | Medium | Default from Hostinger |
ChatGPT 5 Mini | OpenAI | Fast | Lightweight |
Streaming
By default, Telegram uses partial streaming — it waits for chunks before sending. For faster perceived responses, switch to full:
ssh openclaw-vps-1 docker exec openclaw-00kx-openclaw-1 \
openclaw config set channels.telegram.streaming full
ssh openclaw-vps-1 docker restart openclaw-00kx-openclaw-1
Verifying Channels
# Check all channel statuses
ssh openclaw-vps-1 docker exec openclaw-00kx-openclaw-1 openclaw channels status
# Check recent logs
ssh openclaw-vps-1 docker logs openclaw-00kx-openclaw-1 --tail 20
# Run diagnostics
ssh openclaw-vps-1 docker exec openclaw-00kx-openclaw-1 openclaw doctor
Expected output for healthy channels:
- Telegram default: enabled, configured, running
- WhatsApp default: enabled, configured, linked, running, connected
Docker Container Secrets
The Hostinger-managed Docker container (/docker/openclaw-00kx/) gets secrets injected at startup via op run — no plaintext .env file. The start.sh script resolves all op:// references and passes them as environment variables to docker compose:
# Restart the container with fresh secrets from 1Password
/docker/openclaw-00kx/start.sh
docker-compose.yml lists env var names under environment: (without values). op run resolves them from .env.tpl and injects them into the docker compose process, which passes them to the container. Secrets never touch disk.
A systemd service (openclaw-secrets) runs start.sh automatically on boot:
# Check service status
systemctl status openclaw-secrets
# Manually restart (resolves fresh secrets + restarts container)
systemctl restart openclaw-secrets
To add a new secret:
- Create the item in the Shared-Infrastructure vault
- Add the
op://reference to/opt/openclaw/.env.tpl - Add the env var name to the
environment:list in/docker/openclaw-00kx/docker-compose.yml - Run
systemctl restart openclaw-secretsto pick it up
Web / Ephemeral Environment Access
For environments without the 1Password desktop app (Claude Code web, CI runners, remote dev containers), a parallel access path is available using generated SSH keys and the Hostinger REST API.
Prerequisites: Environment Configuration
1. Network access — Claude Code web sessions use an egress proxy that blocks connections to hosts not on its allowlist. In your environment settings (claude.ai/code), set network access to Custom or Full and add:
developers.hostinger.com(Hostinger API)srv1296613.hstgr.cloud(VPS hostname)76.13.100.227(VPS IP)
2. Environment variables — Set these in your Claude Code project settings (never in CLAUDE.md or committed files):
| Variable | Required for | How to get the value |
|---|---|---|
HOSTINGER_API_TOKEN | just vps-status | op item get "Hostinger API Token" --vault="Shared-Infrastructure" --fields credential --reveal |
VPS_SSH_PRIVATE_KEY | SSH access | op item get "openclaw-vps-1" --vault="Shared-Infrastructure" --fields "private key" --reveal | base64 |
Alternatively, set OP_SERVICE_ACCOUNT_TOKEN with a service account that has Shared-Infrastructure vault access. The op CLI can then fetch everything at runtime.
Setup and Usage
just setup-web # Install openssh-client + just, generate SSH key, configure access
just vps-test # Verify SSH connectivity
just vps-status # VPS status via Hostinger API
just vps-exec "cmd" # Run any command on the VPS
just vps-logs # View bot container logs
just vps-deploy # Deploy (git pull + rebuild)
just vps-pubkey # Show public key (if using generated key)
How SSH Key Authorization Works
If using VPS_SSH_PRIVATE_KEY (recommended): the key is decoded from the env var and written to ~/.ssh/ during just setup-web. The matching public key must already be in authorized_keys on the VPS.
If not using VPS_SSH_PRIVATE_KEY: just vps-setup-web generates a new ed25519 key pair. The public key must be added to the VPS manually from your Mac:
ssh openclaw-vps-1 'echo "PUBLIC_KEY_HERE" >> ~/.ssh/authorized_keys'
Recovery
If locked out of SSH, use the Hostinger VPS console panel to:
cp /etc/ssh/sshd_config.bak /etc/ssh/sshd_config
# Service name varies by distro
systemctl restart ssh || systemctl restart sshd
1Password Vault Structure
All secrets are stored in 1Password, organized by vault. Nothing is hardcoded in scripts or config files.
Vault Layout
| Vault | Purpose | Access |
|---|---|---|
| Shared-Infrastructure | Infra credentials (SSH keys, PATs, server details, service account tokens) | Your Mac only |
| Shared-Infrastructure | App runtime secrets (API keys, bot tokens) | VPS via service account |
| Deploy-Production | Production app secrets (mirrors staging structure) | Production VPS via service account |
Secret Reference Format
1Password uses op:// URIs to reference secrets without exposing values:
op://<Vault>/<Item Title>/<Field>
Examples:
op://Shared-Infrastructure/Anthropic API Key/credential
op://Shared-Infrastructure/openclaw-vps-1/public key
These references are resolved at runtime by op run (gateway) or varlock (bot) — the actual secret values never touch disk or source control.
Current Items
Shared-Infrastructure (accessed from your Mac):
| Item | Type | Used By |
|---|---|---|
| GitHub SSH Key | SSH Key | Git push/pull via 1Password SSH agent |
| openclaw-vps-1 | SSH Key | SSH into VPS via 1Password SSH agent |
| Hostinger API Token | API Credential | provision.sh — creates VPSes via Hostinger API |
| Hostinger VPS | Server | VPS connection details (hostname, IP) |
| Hostinger VPS Service Account Token | API Credential | Authenticates op CLI on the VPS |
Shared-Infrastructure (accessed from VPS via service account):
| Item | Type | Used By |
|---|---|---|
| Anthropic API Key | API Credential | Bot (varlock), Gateway (op run) |
| OpenAI API Key | API Credential | Bot (varlock) |
| Telegram Bot Token | API Credential | Bot (varlock) |
| OpenClaw Gateway Token | Password | Gateway (op run) |
| WhatsApp Number | Password | Gateway (op run) |
| Brave Search API | API Credential | Bot (varlock) |
How Secrets Flow
1Password
│
├── Your Mac (Shared-Infrastructure vault)
│ ├── 1Password SSH Agent ──→ ~/.ssh/config ──→ git / ssh to VPS
│ └── Hostinger API Token ──→ provision.sh ──→ Hostinger API
│
└── VPS (Shared-Infrastructure vault)
└── Service Account Token
├── Bot: passed to container ──→ varlock ──→ .env.schema ──→ op CLI ──→ env vars
└── Gateway: op run --env-file .env.gateway.tpl ──→ env vars ──→ Docker container
On your Mac: The 1Password desktop app's SSH agent provides keys on demand. No private keys on disk. Keys must be registered in the agent config:
~/.config/1Password/ssh/agent.toml
[[ssh-keys]]
item = "openclaw-vps-1"
vault = "Shared-Infrastructure"
[[ssh-keys]]
vault = "Private"
VPS keys are listed before the Private vault so the SSH agent offers them first — see Too many authentication failures for why this matters. Keys in the Private vault are enabled by default; keys in other vaults must be added explicitly.
On the VPS: A Service Account Token (stored in /etc/openclaw/op-token.env) authenticates the op CLI. For the bot container, the token is passed as the only secret — varlock resolves everything else inside the container from .env.schema. For the gateway (external image), op run resolves secrets on the host. The token only grants access to Shared-Infrastructure — it cannot read SSH keys or other infra secrets.
Adding a New Secret
- Create the item in the Shared-Infrastructure vault (or Shared-Infrastructure for infra)
- Add the
op(op://...)reference to.env.schemawith@sensitiveannotation - If the gateway also needs it, add the
op://reference todeploy/.env.gateway.tpland add the env var name todeploy/docker-compose.gateway.yml - Commit, push, and the webhook auto-deploys (rebuilds the bot container with the updated schema)
File Locations on VPS
| Path | Purpose |
|---|---|
/etc/openclaw/op-token.env | Service Account Token (mode 600) |
/opt/openclaw/.env.tpl | Secret reference template |
/usr/local/bin/openclaw-run | Helper script |
/docker/openclaw-00kx/start.sh | Resolves secrets via op run and starts Docker container |
/docker/openclaw-00kx/docker-compose.yml | Docker compose config (env var names only, no secrets) |
/etc/systemd/system/openclaw-secrets.service | Runs start.sh on boot |
/etc/ssh/sshd_config.bak | SSH config backup (pre-hardening) |