Skip to main content

VPS Setup (Hostinger)

How to set up a fresh Hostinger VPS for running OpenClaw.

Overview

All VPS scripts live in scripts/vps/:

ScriptPurposeRuns on
provision.shOrchestrator — creates VPS or runs modules on existing oneYour Mac
modules/1password.shInstall 1Password CLI, configure secrets injectionVPS
modules/ssh-hardening.shHarden SSH — key-only auth, no passwordsVPS
modules/firewall.shConfigure iptables with profile-based rulesVPS
modules/webserver.shNginx + webhook auto-deploy + certbotVPS
lib/common.shShared 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 just recipes. Run just --list to see all available tasks. The underlying scripts (scripts/vps/provision.sh, etc.) can be called directly, but just is the standard entry point.

The script will:

  1. Prompt for plan, data center, and template (with sensible defaults)
  2. Fetch credentials from 1Password (API token, SSH key, service account token)
  3. Create the VPS via the Hostinger API with your SSH key injected
  4. Wait for the VPS to become ready
  5. Run all modules remotely (1Password, SSH, firewall, webserver)
  6. Add an entry to ~/.ssh/config
  7. Store VPS details in 1Password (Shared-Infrastructure vault)
  8. Verify SSH and openclaw-run work

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/config using 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

CommandPurpose
just vps-listList all VPSes with hostname, IP, state, and detected role
just vps-add ALIAS HOSTRegister an existing VPS in SSH config
just vps-new --alias NAMECreate a new VPS via Hostinger API + run all modules
just vps HOST --allRun all modules on an existing VPS
just vps HOST --sshSSH hardening only
just vps HOST --1password1Password CLI setup only
just vps HOST --firewall PROFILEFirewall only (profiles: base, web, custom)
just vps HOST --webserverNginx + 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:

  1. Installs 1Password CLI (op) — supports both Debian/Ubuntu (apt) and Arch (binary)
  2. Creates /etc/openclaw/ for sensitive config (mode 700)
  3. Stores the Service Account Token in /etc/openclaw/op-token.env (mode 600)
  4. Verifies access to the Shared-Infrastructure vault
  5. Creates /opt/openclaw/.env.tpl — maps env vars to op:// secret references
  6. Creates /usr/local/bin/openclaw-run — helper that injects secrets at runtime
  7. Creates /docker/openclaw-00kx/start.sh — resolves secrets and starts the Docker container
  8. Enables openclaw-secrets systemd service — runs start.sh on 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 from OP_SA_TOKEN env var

modules/ssh-hardening.sh

Locks down SSH so only key-based auth works:

  1. Adds the provided SSH public key to ~/.ssh/authorized_keys
  2. Backs up /etc/ssh/sshd_config
  3. Disables password authentication, empty passwords, and PAM
  4. Sets root login to key-only (prohibit-password)
  5. 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:

ProfilePorts
base22 (SSH only)
web22, 80, 443
customOnly 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:

  1. Installs nginx, git, webhook, certbot
  2. Clones the repo to /var/www/openclaw
  3. Configures nginx with the site and webhook proxy (/hooks/ → port 9000)
  4. Sets up the webhook listener as a systemd service
  5. Creates the GitHub webhook (if gh is authenticated)
  6. 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

FieldValueMeaning
item_idhostingercom-vps-kvm2-usd-1mKVM 2 plan, monthly
data_center_id17Boston
template_id1121Ubuntu 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

  1. Create a bot via @BotFather on Telegram and save the token in 1Password (Shared-Infrastructure vault, item Telegram Bot Token).

  2. 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 pairing mode — it will show your ID in the response.

  3. Restart the container to apply:

    ssh openclaw-vps-1 docker restart openclaw-00kx-openclaw-1
  4. Test by messaging the bot on Telegram.

Note: Do not use dmPolicy: pairing — pairing approvals don't persist across gateway restarts. Use allowlist instead.

WhatsApp

WhatsApp uses selfChatMode — you message yourself and the bot responds.

  1. Link WhatsApp Web by scanning a QR code:

    ssh -t openclaw-vps-1 docker exec -it openclaw-00kx-openclaw-1 \
    openclaw channels login --channel whatsapp

    Scan the QR with WhatsApp on your phone (Settings > Linked Devices > Link a Device).

  2. 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
  3. 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 login said "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:

ModelProviderSpeedNotes
Claude Haiku 4.5AnthropicFastGood for quick replies
Claude Sonnet 4.5AnthropicMedium (~10s)Balanced quality/speed
Claude Opus 4.6AnthropicSlowHighest quality
ChatGPT 5.2OpenAIMediumDefault from Hostinger
ChatGPT 5 MiniOpenAIFastLightweight

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:

  1. Create the item in the Shared-Infrastructure vault
  2. Add the op:// reference to /opt/openclaw/.env.tpl
  3. Add the env var name to the environment: list in /docker/openclaw-00kx/docker-compose.yml
  4. Run systemctl restart openclaw-secrets to 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):

VariableRequired forHow to get the value
HOSTINGER_API_TOKENjust vps-statusop item get "Hostinger API Token" --vault="Shared-Infrastructure" --fields credential --reveal
VPS_SSH_PRIVATE_KEYSSH accessop 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

VaultPurposeAccess
Shared-InfrastructureInfra credentials (SSH keys, PATs, server details, service account tokens)Your Mac only
Shared-InfrastructureApp runtime secrets (API keys, bot tokens)VPS via service account
Deploy-ProductionProduction 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):

ItemTypeUsed By
GitHub SSH KeySSH KeyGit push/pull via 1Password SSH agent
openclaw-vps-1SSH KeySSH into VPS via 1Password SSH agent
Hostinger API TokenAPI Credentialprovision.sh — creates VPSes via Hostinger API
Hostinger VPSServerVPS connection details (hostname, IP)
Hostinger VPS Service Account TokenAPI CredentialAuthenticates op CLI on the VPS

Shared-Infrastructure (accessed from VPS via service account):

ItemTypeUsed By
Anthropic API KeyAPI CredentialBot (varlock), Gateway (op run)
OpenAI API KeyAPI CredentialBot (varlock)
Telegram Bot TokenAPI CredentialBot (varlock)
OpenClaw Gateway TokenPasswordGateway (op run)
WhatsApp NumberPasswordGateway (op run)
Brave Search APIAPI CredentialBot (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

  1. Create the item in the Shared-Infrastructure vault (or Shared-Infrastructure for infra)
  2. Add the op(op://...) reference to .env.schema with @sensitive annotation
  3. If the gateway also needs it, add the op:// reference to deploy/.env.gateway.tpl and add the env var name to deploy/docker-compose.gateway.yml
  4. Commit, push, and the webhook auto-deploys (rebuilds the bot container with the updated schema)

File Locations on VPS

PathPurpose
/etc/openclaw/op-token.envService Account Token (mode 600)
/opt/openclaw/.env.tplSecret reference template
/usr/local/bin/openclaw-runHelper script
/docker/openclaw-00kx/start.shResolves secrets via op run and starts Docker container
/docker/openclaw-00kx/docker-compose.ymlDocker compose config (env var names only, no secrets)
/etc/systemd/system/openclaw-secrets.serviceRuns start.sh on boot
/etc/ssh/sshd_config.bakSSH config backup (pre-hardening)