Self-Host OpenClaw: VPS Setup to Production (2026)
Self-host OpenClaw from zero to production. Server provisioning, SSH lockdown, Docker, reverse proxy, Fail2ban, performance tuning, and full backup strategy in one guide.
Most OpenClaw guides end where the interesting problems start. They get you through npm install -g openclaw, show you the web UI loading on localhost, and call it done. Then you go live, skip the auth config, and three weeks later you're in the 42,000 exposed instances statistic.
This guide does not end at localhost.
It starts with an empty VPS and finishes with a hardened, monitored, production-ready OpenClaw deployment that you can actually trust with your data, your credentials, and your team's workflow. Every step is explicit. Every command runs. Every configuration decision has a reason behind it.
The guide works for two audiences. If you've never set up a Linux server, start at the beginning and do every step in order. If you've done this before, use the section headers to jump to the part you care about. The security sections are worth reading regardless of your experience level because OpenClaw's threat model has specifics that generic hardening guides don't cover.
One thing before we start: if you want to understand why OpenClaw had 138 CVEs tracked in its first year, what ClawBleed does at the wire level, and how the complete kill chain from "visit a URL" to "full RCE" works, that's a separate article. The OpenClaw CVE-2026-25253 hardening guide covers the vulnerability mechanics in depth. This guide covers the infrastructure. Read both.
What Server Do You Need to Run OpenClaw in Production?
The honest answer is less than you think, more than the minimum listed in the docs.
The OpenClaw docs say 2 vCPU and 2GB RAM. That's the floor for running the gateway process alone, with no inference model attached. A production deployment with an Ollama backend and a small team using it simultaneously needs at minimum:
- 4 vCPU
- 8GB RAM (16GB if you run a 14B+ inference model on the same machine)
- 40GB NVMe SSD (OS + Docker + logs + backup staging)
- Ubuntu 24.04 LTS (long-term support, well-maintained package ecosystem, Docker support is clean)
If you're running the inference model on a separate machine (which we recommend for anything beyond personal use), the OpenClaw gateway itself is lightweight. The gateway does not do inference. It routes requests to whatever LLM backend you configure.
Which VPS Provider Should You Use?
Three providers worth considering, ordered by price-to-performance:
Hetzner (our pick for CoderOasis infrastructure): CX32 gives you 4 vCPU, 8GB RAM, 80GB SSD for roughly €9/month. EU-based data centers, excellent network, no egress pricing surprises. The control panel is no-nonsense. Reference our self-hosted productivity stack guide for more on how we use Hetzner as our primary provider.
DigitalOcean: Premium pricing for well-documented infrastructure. The Droplet product line is reliable and their managed firewall is better than raw UFW for teams. More expensive than Hetzner for equivalent specs.
Vultr: Competitive pricing, good global coverage, solid SSD storage. High Frequency Compute instances are worth it if latency to your users matters.
Avoid shared hosting entirely. OpenClaw runs as a persistent daemon with WebSocket connections. Shared hosts kill long-running processes. You need a VPS.
Does OpenClaw Need a GPU?
Not for the gateway. OpenClaw routes requests -- it does not run inference. If you configure an Ollama backend for the inference layer and want GPU acceleration on that backend, then yes, a GPU matters for inference speed. For the gateway process running on the same machine: no.
If you want GPU inference on the same VPS, Hetzner's GPU servers and Lambda Labs cloud instances are the realistic options for 2026 pricing. The local LLM setup guide covers GPU selection, VRAM requirements, and model sizing in detail.
How Do You Set Up a Fresh Ubuntu 24.04 VPS From Scratch?
Provision the VPS with Ubuntu 24.04 LTS through your provider's control panel. When the machine boots, you'll have a root session via the provider's console or a root SSH connection using a password they generated.
The first 15 minutes on a fresh VPS follow a sequence you should do on every machine, every time, before anything else goes on it.
Step 1: Create a Non-Root User
Never operate as root for day-to-day work. Create a dedicated admin user immediately.
# Log in as root initially
adduser traven
# Fill in the password prompt -- make it strong
# Grant sudo access
usermod -aG sudo traven
# Verify sudo works
su - traven
sudo whoami
# Output: root
Step 2: Generate and Deploy SSH Keys
If you haven't already generated an SSH key pair on your local machine:
# On your LOCAL machine, not the server
ssh-keygen -t ed25519 -C "traven@coderoasis" -f ~/.ssh/coderoasis_vps
# This creates:
# ~/.ssh/coderoasis_vps (private key -- never share this)
# ~/.ssh/coderoasis_vps.pub (public key -- this goes on the server)
Copy the public key to the server:
# From your local machine
ssh-copy-id -i ~/.ssh/coderoasis_vps.pub traven@YOUR_SERVER_IP
# Or manually if ssh-copy-id is not available:
cat ~/.ssh/coderoasis_vps.pub | ssh traven@YOUR_SERVER_IP \
"mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
Test the key login before doing anything else:
# Still on your LOCAL machine
ssh -i ~/.ssh/coderoasis_vps traven@YOUR_SERVER_IP
# If this succeeds, proceed. If it fails, debug before continuing.
Add this to your local ~/.ssh/config so you don't repeat yourself:
Host openclaw-vps
HostName YOUR_SERVER_IP
User traven
IdentityFile ~/.ssh/coderoasis_vps
ServerAliveInterval 60
ServerAliveCountMax 3
Now ssh openclaw-vps connects without specifying anything.
Step 3: Harden SSH
Every publicly accessible SSH port gets scanned within minutes of a VPS coming online. The default configuration allows password authentication, which means every credential-stuffing attack in the world has a clean shot at your server. Lock it down before the bots find it.
# Back on the SERVER as traven (with sudo)
sudo nano /etc/ssh/sshd_config
Set these values (search for existing lines and change them, add lines that aren't present):
# Disable root login entirely
PermitRootLogin no
# Disable password authentication -- key-only from here forward
PasswordAuthentication no
ChallengeResponseAuthentication no
UsePAM no
# Disable empty passwords
PermitEmptyPasswords no
# Only allow your specific user
AllowUsers traven
# Limit authentication attempts
MaxAuthTries 3
MaxSessions 3
# Disable unused authentication methods
X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding no
# Timeout idle sessions after 30 minutes
ClientAliveInterval 300
ClientAliveCountMax 6
# Use only modern SSH protocol
Protocol 2
# Restrict to strong key algorithms
HostKeyAlgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256
KexAlgorithms curve25519-sha256,diffie-hellman-group16-sha512
MACs [email protected],[email protected]
Ciphers [email protected],[email protected],[email protected]
Test the configuration before restarting:
sudo sshd -t
# Output: (nothing) means the config is valid
# Any output means there's a syntax error -- fix it before restarting
sudo systemctl restart sshd
Open a second terminal and test that your key login still works before closing the first session. If you close the first session and SSH is misconfigured, you're locked out and relying on the provider's console.
Step 4: Configure UFW Firewall
# Install UFW if not present
sudo apt update && sudo apt install -y ufw
# Set restrictive defaults
sudo ufw default deny incoming
sudo ufw default allow outgoing
# Allow SSH (essential -- do this first or you lock yourself out)
sudo ufw allow 22/tcp comment 'SSH'
# Allow HTTP and HTTPS for the reverse proxy
sudo ufw allow 80/tcp comment 'HTTP (redirects to HTTPS)'
sudo ufw allow 443/tcp comment 'HTTPS'
# Enable the firewall
sudo ufw enable
# Verify the rules
sudo ufw status verbose
The OpenClaw gateway (port 18789 by default) does NOT get a UFW rule. It binds to 127.0.0.1 only. All external traffic goes through the Nginx reverse proxy on port 443. This is intentional. The gateway is never publicly reachable.
Step 5: Update All Packages
sudo apt update && sudo apt upgrade -y
# Remove packages that serve no purpose on a server
sudo apt remove -y snapd cups bluetooth bluez avahi-daemon
# Clean up
sudo apt autoremove -y && sudo apt autoclean
# Install essentials
sudo apt install -y \
curl \
wget \
git \
htop \
fail2ban \
unattended-upgrades \
net-tools \
lsof \
ncdu \
jq
Enable automatic security updates:
sudo dpkg-reconfigure --priority=low unattended-upgrades
# Select "Yes" when prompted
Edit /etc/apt/apt.conf.d/50unattended-upgrades to enable automatic reboot for kernel updates:
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "03:00";
Step 6: Set the Timezone and Hostname
# Set timezone (adjust to your actual timezone)
sudo timedatectl set-timezone UTC
# Set a recognizable hostname
sudo hostnamectl set-hostname openclaw-prod
# Update /etc/hosts to match
echo "127.0.0.1 openclaw-prod" | sudo tee -a /etc/hosts
How Do You Install Docker for OpenClaw?
OpenClaw runs natively via npm or inside Docker. Docker is the correct choice for production because it provides process isolation, simplified updates, and consistent behavior across environments. If you're not familiar with containers, the What is Docker article covers the concepts.
# Remove any old Docker installations
sudo apt remove -y docker docker-engine docker.io containerd runc 2>/dev/null || true
# Install prerequisites
sudo apt update && sudo apt install -y ca-certificates curl gnupg
# Add Docker's official GPG key
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
# Add Docker's repository
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Install Docker Engine and Compose plugin
sudo apt update && sudo apt install -y \
docker-ce \
docker-ce-cli \
containerd.io \
docker-buildx-plugin \
docker-compose-plugin
# Add your user to the docker group (log out and back in after this)
sudo usermod -aG docker traven
# Verify Docker works
docker run --rm hello-world
Configure Docker's logging to prevent log files from growing without bound:
sudo nano /etc/docker/daemon.json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"live-restore": true,
"no-new-privileges": true,
"userland-proxy": false
}
sudo systemctl restart docker
sudo systemctl enable docker
How Do You Install OpenClaw?
Three install paths exist. Choose based on your operational preference.
Option A: Docker Compose (Recommended for Production)
Create the project directory:
sudo mkdir -p /opt/openclaw/{data,logs,config,skills}
sudo chown -R traven:traven /opt/openclaw
cd /opt/openclaw
Create /opt/openclaw/docker-compose.yml:
version: "3.9"
services:
openclaw:
image: ghcr.io/openclaw/openclaw:v2026.3.28
container_name: openclaw
restart: unless-stopped
networks:
- openclaw-internal
ports:
# Bind ONLY to loopback -- Nginx proxies external traffic
- "127.0.0.1:18789:18789"
volumes:
- ./data:/data
- ./logs:/logs
- ./config:/config:ro # Config is read-only inside container
- ./skills:/skills # Local skills only -- ClawHub disabled
environment:
HOME: /var/empty
ZDOTDIR: /var/empty
OPENCLAW_CONFIG: /config/openclaw.json
OPENCLAW_LOG_FILE: /logs/openclaw.log
NODE_ENV: production
user: "1001:1001"
read_only: true
tmpfs:
- /tmp:size=256m,noexec,nosuid
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:18789/api/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 15s
networks:
openclaw-internal:
driver: bridge
internal: false
Create the dedicated system user OpenClaw runs as:
sudo useradd --system --no-create-home --shell /bin/false --uid 1001 openclaw
sudo chown -R openclaw:openclaw /opt/openclaw/data /opt/openclaw/logs
Option B: npm Global Install + systemd
If you prefer npm (simpler, but less isolated):
# Install Node.js 22 LTS
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs
# Install OpenClaw globally
sudo npm install -g [email protected]
# Verify
openclaw --version
Create the systemd service at /etc/systemd/system/openclaw.service:
[Unit]
Description=OpenClaw AI Agent Gateway
Documentation=https://openclaw.dev/docs
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=openclaw
Group=openclaw
# CVE-2026-32056 mitigation: sanitize environment before shell startup
Environment=HOME=/var/empty
Environment=ZDOTDIR=/var/empty
Environment=PATH=/usr/local/bin:/usr/bin:/bin
Environment=NODE_ENV=production
ExecStart=/usr/local/bin/openclaw gateway run \
--config /etc/openclaw/openclaw.json \
--bind 127.0.0.1 \
--port 18789
# Restart on failure, not on clean exit
Restart=on-failure
RestartSec=10
StartLimitInterval=60
StartLimitBurst=5
# Systemd security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=read-only
PrivateTmp=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictRealtime=true
RestrictSUIDSGID=true
CapabilityBoundingSet=
SystemCallFilter=@system-service
# Writable paths only where needed
ReadWritePaths=/var/lib/openclaw /var/log/openclaw /tmp
[Install]
WantedBy=multi-user.target
# Create directories for the npm install path
sudo mkdir -p /etc/openclaw /var/lib/openclaw /var/log/openclaw
sudo chown -R openclaw:openclaw /var/lib/openclaw /var/log/openclaw
sudo systemctl daemon-reload
sudo systemctl enable openclaw
We recommend reading How to Run a Local LLM on Your PC in 2026 (Complete Guide) to continue reading our selection of content. OpenClaw routes requests to an LLM inference backend — before configuring that connection in the next section, that guide covers Ollama installation, model selection, and VRAM requirements for every hardware tier.
https://coderoasis.com/run-local-llm-on-your-pc-complete-guide-2026/
How Do You Configure OpenClaw for Production?
This is the section most tutorials skip entirely. The default configuration ships with authentication disabled, the gateway bound to all interfaces, and no origin validation. None of that is acceptable for a machine on the public internet.
Create /opt/openclaw/config/openclaw.json (or /etc/openclaw/openclaw.json for the npm path):
{
"gateway": {
"bind": "127.0.0.1",
"port": 18789,
"allowedOrigins": [
"https://ai.yourdomain.com"
],
"requirePairingCode": true,
"trustProxy": true,
"trustedProxies": ["127.0.0.1"],
"realIpHeader": "X-Real-IP",
"auth": {
"required": true,
"tokenRotationIntervalHours": 24,
"enforceRotationOnConnect": true,
"maxTokenAge": "24h",
"maxFailedAttempts": 5,
"lockoutDurationMinutes": 30
},
"rateLimit": {
"websocketConnectionsPerMinute": 10,
"apiRequestsPerMinute": 60
},
"cors": {
"enabled": true,
"credentials": true
}
},
"exec": {
"approvals": {
"required": true,
"commands": ["*"],
"destructiveCommandsRequireExplicitConfirm": true,
"timeout": 30,
"allowList": [
"ls", "cat", "grep", "find", "echo",
"python3", "node", "git status", "git log",
"curl --head", "ping -c 1"
],
"blockList": [
"rm -rf /",
"dd if=",
"mkfs",
"> /dev/",
"curl | bash",
"wget | bash",
"wget -O- | sh",
"nc -e",
"python -c 'import socket"
]
}
},
"skills": {
"rootDir": "/skills",
"allowedWritePaths": ["/skills", "/tmp"],
"clawHubEnabled": false,
"requireSignedSkills": false,
"sandboxMode": "strict",
"blockPathTraversal": true,
"allowedNetworkDomains": []
},
"llm": {
"provider": "ollama",
"baseUrl": "http://127.0.0.1:11434",
"model": "qwen2.5:14b",
"timeout": 120,
"retries": 2
},
"integrations": {
"email": { "enabled": false },
"calendar": { "enabled": false, "readOnly": true },
"filesystem": {
"allowedPaths": [
"~/Documents",
"~/Downloads",
"/tmp/openclaw-workspace"
],
"deniedPaths": [
"~/.ssh",
"~/.gnupg",
"~/.aws",
"~/.config/gcloud",
"~/.kube",
"~/.npmrc",
"~/.gitconfig",
"/etc/passwd",
"/etc/shadow",
"/etc/sudoers"
]
}
},
"memory": {
"storageDir": "/data/memory",
"encryptAtRest": true,
"maxFileSizeBytes": 52428800
},
"logging": {
"level": "warn",
"file": "/logs/openclaw.log",
"auditLog": {
"enabled": true,
"file": "/logs/audit.log",
"includeToolCalls": true,
"includeAuthEvents": true,
"includePrompts": false,
"includeResponses": false
}
}
}
Walk through the decisions that matter most here.
bind: "127.0.0.1" binds the gateway only to loopback. No external traffic ever reaches port 18789 directly. Nginx handles everything external.
allowedOrigins contains exactly one entry: your actual production domain over HTTPS. This is the patch for CVE-2026-25253 (ClawBleed). The gateway validates the Origin header on every WebSocket upgrade request. Any connection from a different origin gets rejected immediately.
requirePairingCode: true forces a one-time pairing ceremony for any new browser connecting. An attacker who discovers your domain cannot initiate an authenticated session without the pairing code that appears in your terminal.
exec.approvals.required: true is the control you cannot leave off. The ClawBleed kill chain specifically disabled this via an API call (exec.approvals.set = off). With this set in config, the API call is ignored.
filesystem.deniedPaths blocks the directories that credential-stealing skills target. Include ~/.npmrc and ~/.gitconfig -- these are active infostealer targets alongside the more obvious ~/.ssh and ~/.aws.
llm.baseUrl points to Ollama running on the same machine (127.0.0.1:11434) or to a separate inference server. If you use a cloud LLM provider, set llm.provider to openai, anthropic, or the appropriate value and provide the API key via an environment variable, not in the config file.
How Do You Set Up Nginx as a Reverse Proxy for OpenClaw?
Nginx sits in front of OpenClaw, terminates TLS, and forwards requests to port 18789. The gateway never needs to handle certificates or external connections directly.
sudo apt install -y nginx certbot python3-certbot-nginx
# Create the OpenClaw server block
sudo nano /etc/nginx/sites-available/openclaw
# /etc/nginx/sites-available/openclaw
# Rate limiting zone -- protect against brute force on the WebSocket upgrade
limit_req_zone $binary_remote_addr zone=openclaw_ws:10m rate=5r/s;
limit_conn_zone $binary_remote_addr zone=openclaw_conn:10m;
# HTTP to HTTPS redirect
server {
listen 80;
listen [::]:80;
server_name ai.yourdomain.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name ai.yourdomain.com;
# TLS -- Certbot will populate these after certificate issuance
ssl_certificate /etc/letsencrypt/live/ai.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ai.yourdomain.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
# Connection limits
limit_conn openclaw_conn 20;
# Gzip for text content
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml;
# Main proxy location -- forwards to OpenClaw gateway
location / {
limit_req zone=openclaw_ws burst=20 nodelay;
proxy_pass http://127.0.0.1:18789;
proxy_http_version 1.1;
# WebSocket support -- required for OpenClaw real-time communication
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Pass real client information to OpenClaw
# Required for correct IP logging and origin validation
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Do NOT override the Origin header
# OpenClaw validates Origin against allowedOrigins
# If you set proxy_set_header Origin here, you'd bypass that check
# Timeouts appropriate for long-running LLM responses
proxy_connect_timeout 10s;
proxy_send_timeout 120s;
proxy_read_timeout 120s;
# WebSocket keepalive
proxy_buffering off;
}
# Health check endpoint -- for monitoring, no auth required
location /api/health {
proxy_pass http://127.0.0.1:18789;
proxy_http_version 1.1;
proxy_set_header Host $host;
access_log off;
}
# Block direct access to admin paths from outside if needed
# location /admin {
# allow 203.0.113.0/24; # Your IP range
# deny all;
# proxy_pass http://127.0.0.1:18789;
# proxy_http_version 1.1;
# }
# Logging
access_log /var/log/nginx/openclaw.access.log;
error_log /var/log/nginx/openclaw.error.log;
}
Enable the site and get the certificate:
# Test Nginx config syntax before enabling
sudo nginx -t
# Enable the site
sudo ln -s /etc/nginx/sites-available/openclaw /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
# Reload Nginx to apply changes
sudo systemctl reload nginx
# Issue the TLS certificate
sudo certbot --nginx -d ai.yourdomain.com \
--non-interactive \
--agree-tos \
--email [email protected] \
--redirect
# Certbot sets up automatic renewal via a systemd timer
# Verify the timer is active
sudo systemctl status certbot.timer
Test HTTPS:
curl -I https://ai.yourdomain.com/api/health
# HTTP/2 200 -- good
# Check for the security headers
How Do You Configure Fail2ban for OpenClaw?
Fail2ban monitors OpenClaw's log files and blocks IPs that fail authentication repeatedly. It also protects the SSH port from brute force attacks.
# Verify fail2ban is installed and running
sudo systemctl status fail2ban
Create the OpenClaw log filter:
sudo nano /etc/fail2ban/filter.d/openclaw.conf
[INCLUDES]
before = common.conf
[Definition]
# Match failed authentication events from OpenClaw's audit log
failregex = ^.*"event":"auth.failed".*"ip":"<ADDR>".*$
^.*Username or password is incorrect.*IP: <ADDR>.*$
^.*Invalid admin token.*IP: <ADDR>.*$
^.*"event":"pairing.failed".*"ip":"<ADDR>".*$
ignoreregex =
Create the jail configuration:
sudo nano /etc/fail2ban/jail.d/openclaw.conf
[DEFAULT]
bantime = 3600
findtime = 300
maxretry = 5
# SSH protection
[sshd]
enabled = true
port = ssh
logpath = %(sshd_log)s
maxretry = 3
bantime = 86400
# OpenClaw gateway authentication failures
[openclaw]
enabled = true
port = 80,443
filter = openclaw
# Adjust log path based on install method
# Docker: /opt/openclaw/logs/audit.log
# npm: /var/log/openclaw/audit.log
logpath = /opt/openclaw/logs/audit.log
maxretry = 5
findtime = 300
bantime = 3600
# OpenClaw admin panel -- stricter limits
[openclaw-admin]
enabled = true
port = 80,443
filter = openclaw
logpath = /opt/openclaw/logs/audit.log
maxretry = 3
findtime = 300
bantime = 86400
sudo systemctl restart fail2ban
# Check active jails
sudo fail2ban-client status
# Test that the openclaw jail loaded
sudo fail2ban-client status openclaw
For the Docker install path, the audit log is written inside the container and mounted to the host. Verify the path:
ls -la /opt/openclaw/logs/
# audit.log and openclaw.log should both be present after first startup
How Do You Start OpenClaw and Verify It Works?
For the Docker Compose path:
cd /opt/openclaw
docker compose up -d
# Check startup logs
docker logs openclaw --follow --tail 50
# Expected output includes:
# [INFO] OpenClaw gateway starting on 127.0.0.1:18789
# [INFO] Auth: required
# [INFO] Origin validation: enabled
# Verify the health endpoint through Nginx
curl https://ai.yourdomain.com/api/health
# {"status":"ok","version":"2026.3.28"}
For the systemd path:
sudo systemctl start openclaw
sudo systemctl status openclaw
# Check the log
sudo journalctl -u openclaw -f --since "5 minutes ago"
# Test health
curl https://ai.yourdomain.com/api/health
Pairing a browser client: Navigate to https://ai.yourdomain.com in your browser. Because requirePairingCode: true is set, OpenClaw will prompt for a pairing code. Check the server logs for the generated code:
# Docker
docker logs openclaw | grep -i "pairing code"
# systemd
sudo journalctl -u openclaw | grep -i "pairing code"
Enter the code in the browser. The connection is established and stored. Future connections from the same browser do not need re-pairing unless the token expires or is rotated.
How Do You Connect OpenClaw to an Ollama Inference Backend?
OpenClaw routes user requests to an LLM. Configure the connection in openclaw.json:
{
"llm": {
"provider": "ollama",
"baseUrl": "http://127.0.0.1:11434",
"model": "qwen2.5:14b",
"timeout": 120,
"maxRetries": 2,
"fallbackModel": "qwen2.5:7b"
}
}
If Ollama runs on a separate machine (recommended for GPU inference):
{
"llm": {
"provider": "ollama",
"baseUrl": "http://10.0.0.5:11434",
"model": "qwen2.5:14b",
"timeout": 120
}
}
The inference machine should not be publicly reachable. Use a private network or WireGuard between the OpenClaw gateway and the Ollama backend if they're on separate VPS instances.
Test the connection:
# Verify OpenClaw can reach Ollama
curl http://127.0.0.1:11434/api/tags
# Returns a JSON list of available models
# Verify OpenClaw reports the model available
curl http://127.0.0.1:18789/api/models
# Returns models OpenClaw can route to
We recommend reading Zero Trust Everything: The Complete Guide to Self-Hosting Teleport to continue reading our selection of content. At this point your OpenClaw deployment is secured and running – Teleport is the next logical layer: replacing the public HTTPS endpoint with identity-verified, hardware-MFA-protected access that logs every session and issues short-lived certificates instead of stored credentials.
How Do You Harden OpenClaw Against the Known CVE Attack Surface?
The OpenClaw CVE hardening guide covers the full CVE ledger. This section consolidates the configuration controls into a single verification checklist you can run against your deployment.
Run this script to audit your current configuration:
#!/bin/bash
# openclaw-security-audit.sh
# Run this after every config change and before considering the deployment production-ready
set -euo pipefail
CONFIG="/opt/openclaw/config/openclaw.json"
LOG="/opt/openclaw/logs/openclaw.log"
PASS=0
FAIL=0
check() {
local desc="$1"
local result="$2"
if [ "$result" = "pass" ]; then
echo " [PASS] $desc"
((PASS++))
else
echo " [FAIL] $desc"
((FAIL++))
fi
}
echo "=== OpenClaw Security Audit ==="
echo ""
# Version check
VERSION=$(docker exec openclaw openclaw --version 2>/dev/null || openclaw --version 2>/dev/null || echo "unknown")
echo "Version: $VERSION"
if [[ "$VERSION" > "2026.3.5" ]] || [[ "$VERSION" == "2026.3."* ]]; then
check "Version >= v2026.3.5 (minimum safe)" "pass"
else
check "Version >= v2026.3.5 (minimum safe)" "fail"
fi
echo ""
# Config checks
echo "--- Configuration ---"
BIND=$(jq -r '.gateway.bind // "0.0.0.0"' "$CONFIG")
check "Gateway binds to 127.0.0.1 only" \
"$([[ "$BIND" == "127.0.0.1" ]] && echo pass || echo fail)"
ORIGINS=$(jq -r '.gateway.allowedOrigins | length' "$CONFIG")
check "allowedOrigins is set (not empty)" \
"$([[ "$ORIGINS" -gt 0 ]] && echo pass || echo fail)"
PAIRING=$(jq -r '.gateway.requirePairingCode // false' "$CONFIG")
check "requirePairingCode is true" \
"$([[ "$PAIRING" == "true" ]] && echo pass || echo fail)"
AUTH=$(jq -r '.gateway.auth.required // false' "$CONFIG")
check "auth.required is true" \
"$([[ "$AUTH" == "true" ]] && echo pass || echo fail)"
APPROVALS=$(jq -r '.exec.approvals.required // false' "$CONFIG")
check "exec.approvals.required is true" \
"$([[ "$APPROVALS" == "true" ]] && echo pass || echo fail)"
CLAWHUB=$(jq -r '.skills.clawHubEnabled // true' "$CONFIG")
check "clawHubEnabled is false" \
"$([[ "$CLAWHUB" == "false" ]] && echo pass || echo fail)"
SSH_DENIED=$(jq -r '.integrations.filesystem.deniedPaths | map(select(. == "~/.ssh")) | length' "$CONFIG")
check "~/.ssh is in filesystem.deniedPaths" \
"$([[ "$SSH_DENIED" -gt 0 ]] && echo pass || echo fail)"
AWS_DENIED=$(jq -r '.integrations.filesystem.deniedPaths | map(select(. == "~/.aws")) | length' "$CONFIG")
check "~/.aws is in filesystem.deniedPaths" \
"$([[ "$AWS_DENIED" -gt 0 ]] && echo pass || echo fail)"
echo ""
# Process checks
echo "--- Process ---"
RUNNING=$(docker inspect --format='{{.State.Running}}' openclaw 2>/dev/null || \
systemctl is-active openclaw 2>/dev/null | grep -q active && echo true || echo false)
check "OpenClaw process is running" \
"$([[ "$RUNNING" == "true" ]] && echo pass || echo fail)"
PORT_PUBLIC=$(ss -tlnp | grep ':18789' | grep -v '127.0.0.1' | wc -l)
check "Port 18789 NOT publicly bound" \
"$([[ "$PORT_PUBLIC" -eq 0 ]] && echo pass || echo fail)"
echo ""
# Fail2ban
echo "--- Fail2ban ---"
F2B_RUNNING=$(systemctl is-active fail2ban 2>/dev/null)
check "fail2ban is running" \
"$([[ "$F2B_RUNNING" == "active" ]] && echo pass || echo fail)"
F2B_JAIL=$(sudo fail2ban-client status 2>/dev/null | grep -c "openclaw" || true)
check "openclaw fail2ban jail is active" \
"$([[ "$F2B_JAIL" -gt 0 ]] && echo pass || echo fail)"
echo ""
# Results
echo "=============================="
echo "PASSED: $PASS"
echo "FAILED: $FAIL"
if [[ "$FAIL" -eq 0 ]]; then
echo "Status: READY FOR PRODUCTION"
else
echo "Status: FIX FAILURES BEFORE PRODUCTION"
fi
chmod +x openclaw-security-audit.sh
sudo ./openclaw-security-audit.sh
Every check must pass before you consider the deployment production-ready.
How Do You Tune OpenClaw for Performance?
A default Docker deployment uses whatever resources are available. Production deployments need explicit resource boundaries so that a runaway skill execution or a large LLM response doesn't starve the host.
Docker Resource Limits
Add resource constraints to your docker-compose.yml:
services:
openclaw:
image: ghcr.io/openclaw/openclaw:v2026.3.28
# ... existing config ...
deploy:
resources:
limits:
cpus: '2.0' # Maximum 2 CPU cores
memory: 2G # Maximum 2GB RAM
reservations:
cpus: '0.5' # Minimum guaranteed 0.5 cores
memory: 512M # Minimum guaranteed 512MB
System-Level Tuning
File descriptor limits matter for services that handle many simultaneous WebSocket connections:
sudo nano /etc/security/limits.d/openclaw.conf
openclaw soft nofile 65536
openclaw hard nofile 65536
openclaw soft nproc 4096
openclaw hard nproc 4096
Kernel network tuning for WebSocket-heavy workloads:
sudo nano /etc/sysctl.d/99-openclaw.conf
# Increase connection backlog for high connection volume
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 65536
# Improve TCP performance for long-lived WebSocket connections
net.ipv4.tcp_keepalive_time = 300
net.ipv4.tcp_keepalive_intvl = 30
net.ipv4.tcp_keepalive_probes = 9
# Allow more connections in TIME_WAIT state
net.ipv4.tcp_tw_reuse = 1
# Increase the maximum number of open files system-wide
fs.file-max = 200000
# Improve memory management under high connection count
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
sudo sysctl -p /etc/sysctl.d/99-openclaw.conf
Nginx Performance Tuning
Update your main Nginx config at /etc/nginx/nginx.conf:
worker_processes auto; # One worker per CPU core
worker_rlimit_nofile 65536;
events {
worker_connections 4096;
use epoll;
multi_accept on;
}
http {
# ... existing settings ...
# WebSocket connection caching
keepalive_timeout 75s;
keepalive_requests 1000;
# Upstream connection pooling to OpenClaw
upstream openclaw_backend {
server 127.0.0.1:18789;
keepalive 32; # Keep 32 connections warm to the backend
}
}
Log Rotation
OpenClaw produces audit logs that grow continuously. Configure log rotation so they don't fill the disk:
sudo nano /etc/logrotate.d/openclaw
/opt/openclaw/logs/*.log {
daily
rotate 30
compress
delaycompress
missingok
notifempty
create 640 openclaw openclaw
postrotate
docker kill --signal=USR1 openclaw 2>/dev/null || \
systemctl kill --signal=USR1 openclaw.service 2>/dev/null || true
endscript
}
# Test the rotation config
sudo logrotate --debug /etc/logrotate.d/openclaw
How Do You Monitor OpenClaw in Production?
Monitoring belongs in production before your first real user hits the system, not after the first incident.
Systemd Watchdog
For the npm/systemd install path, add watchdog support to the service file:
[Service]
# ... existing options ...
WatchdogSec=60
NotifyAccess=main
Systemd will restart OpenClaw automatically if it stops responding within 60 seconds.
For Docker, the healthcheck defined in docker-compose.yml serves the same purpose with Docker's own restart policy.
Prometheus Metrics
OpenClaw exposes basic metrics at /api/metrics when enabled:
{
"monitoring": {
"enabled": true,
"metricsEndpoint": "/api/metrics",
"metricsAuth": true
}
}
Add OpenClaw to your Prometheus scrape configuration if you're running the Prometheus/Grafana monitoring stack:
# In your prometheus.yml scrape_configs
- job_name: 'openclaw'
scheme: https
metrics_path: /api/metrics
static_configs:
- targets: ['ai.yourdomain.com']
basic_auth:
username: metrics
password: your-metrics-password
Key metrics to alert on:
# Active WebSocket connections
openclaw_websocket_connections_active
# Failed authentication attempts per minute
rate(openclaw_auth_failures_total[5m])
# Command execution approval queue depth
openclaw_exec_pending_approvals
# Memory usage of the gateway process
process_resident_memory_bytes{job="openclaw"}
# Response latency percentiles from the LLM backend
histogram_quantile(0.95, openclaw_llm_response_duration_seconds_bucket)
Health Check Monitoring
For a simple external health check without a full Prometheus setup:
# Add to crontab: check every 5 minutes
*/5 * * * * curl -sf https://ai.yourdomain.com/api/health > /dev/null || \
echo "OpenClaw health check failed at $(date)" | \
mail -s "ALERT: OpenClaw down" [email protected]
How Do You Back Up OpenClaw's Data?
Two categories of data need backup: configuration (small, changes rarely) and persistent state (memory files, session data, audit logs).
#!/bin/bash
# /opt/scripts/openclaw-backup.sh
# Run daily at 2 AM
set -euo pipefail
BACKUP_DATE=$(date +%Y%m%d-%H%M)
BACKUP_DIR="/opt/backups/openclaw"
REMOTE="traven@backup-server:/backups/openclaw"
SOURCE="/opt/openclaw"
mkdir -p "${BACKUP_DIR}"
echo "[${BACKUP_DATE}] Starting OpenClaw backup..."
# 1. Export the current running configuration
docker exec openclaw cat /config/openclaw.json > \
"${BACKUP_DIR}/config-${BACKUP_DATE}.json" 2>/dev/null || \
cp /etc/openclaw/openclaw.json "${BACKUP_DIR}/config-${BACKUP_DATE}.json"
# 2. Back up persistent data (memory, skills, session state)
tar -czf "${BACKUP_DIR}/data-${BACKUP_DATE}.tar.gz" \
-C "${SOURCE}" \
data/ \
skills/ \
--exclude="data/tmp" \
--exclude="data/cache"
# 3. Back up recent audit logs (last 7 days only -- full logs go to SIEM)
find "${SOURCE}/logs" -name "audit.log*" -mtime -7 | \
tar -czf "${BACKUP_DIR}/audit-${BACKUP_DATE}.tar.gz" -T -
echo " Backup created: ${BACKUP_DIR}/data-${BACKUP_DATE}.tar.gz"
# 4. Sync to remote
rsync -avz \
"${BACKUP_DIR}/data-${BACKUP_DATE}.tar.gz" \
"${BACKUP_DIR}/config-${BACKUP_DATE}.json" \
"${REMOTE}/" && echo " Remote sync: done"
# 5. Clean up local backups older than 14 days
find "${BACKUP_DIR}" -name "*.tar.gz" -mtime +14 -delete
find "${BACKUP_DIR}" -name "*.json" -mtime +14 -delete
echo "[${BACKUP_DATE}] Backup complete."
chmod +x /opt/scripts/openclaw-backup.sh
# Schedule it
echo "0 2 * * * traven /opt/scripts/openclaw-backup.sh >> /var/log/openclaw-backup.log 2>&1" | \
sudo tee /etc/cron.d/openclaw-backup
How Do You Update OpenClaw Safely?
Updates ship frequently. The correct update process prevents downtime and gives you a rollback path.
#!/bin/bash
# openclaw-update.sh
set -euo pipefail
NEW_VERSION="${1:-latest}"
CONFIG="/opt/openclaw/config/openclaw.json"
echo "Updating OpenClaw to: ${NEW_VERSION}"
# Step 1: Back up before touching anything
/opt/scripts/openclaw-backup.sh
# Step 2: Pull the new image
docker pull "ghcr.io/openclaw/openclaw:${NEW_VERSION}"
# Step 3: Update the compose file version (if not using 'latest')
sed -i "s|ghcr.io/openclaw/openclaw:.*|ghcr.io/openclaw/openclaw:${NEW_VERSION}|" \
/opt/openclaw/docker-compose.yml
# Step 4: Restart with the new image
cd /opt/openclaw
docker compose up -d --force-recreate
# Step 5: Wait for health check
sleep 10
HEALTH=$(curl -sf https://ai.yourdomain.com/api/health | jq -r '.status')
if [[ "$HEALTH" == "ok" ]]; then
echo "Update successful. Version: ${NEW_VERSION}"
docker image prune -f
else
echo "Health check failed. Rolling back..."
# Rollback: the previous image is still cached
docker compose down
git -C /opt/openclaw checkout docker-compose.yml 2>/dev/null || \
sed -i "s|ghcr.io/openclaw/openclaw:${NEW_VERSION}|ghcr.io/openclaw/openclaw:$(docker images ghcr.io/openclaw/openclaw --format '{{.Tag}}' | sed -n '2p')|" \
/opt/openclaw/docker-compose.yml
docker compose up -d
echo "Rolled back to previous version."
exit 1
fi
Frequently Asked Questions
Can I run OpenClaw on a shared host or cPanel?
No. OpenClaw runs as a persistent daemon with WebSocket server processes and requires OS-level process management. Shared hosting kills persistent processes. A VPS is required.
How do I add multiple users to a single OpenClaw instance?
Configure the users section in openclaw.json and set each user's role and permissions. Each user gets their own token via the pairing flow. The organization is scoped per user -- users don't share memory or conversation history by default.
Is the Ollama backend safe to run on the same VPS as OpenClaw?
Yes, if Ollama is bound to 127.0.0.1. The risk of running inference on the same machine is resource contention -- a long-running LLM inference job can starve the OpenClaw gateway of CPU. For production workloads with more than a few users, separate inference onto a dedicated machine with a GPU.
How do I update Ollama when a new version ships?
# If installed via the official install script
sudo systemctl stop ollama
curl -fsSL https://ollama.com/install.sh | sh
sudo systemctl start ollama
ollama --version
What happens if the TLS certificate expires?
Certbot's systemd timer renews certificates automatically 30 days before expiry. Verify the timer is active with sudo systemctl status certbot.timer. The Nginx config will serve an expired certificate if renewal fails -- set up the SSL cert expiry Prometheus alert from the monitoring guide to catch this before it becomes an incident.
Can I run this behind Cloudflare's proxy?
Yes, with two configuration changes. Set gateway.trustProxy: true and add Cloudflare's IP ranges to gateway.trustedProxies. Also ensure your allowedOrigins matches the domain Cloudflare serves, not localhost. The origin validation must see the actual browser origin, not a Cloudflare-modified one.
The full production deployment path runs from an empty Ubuntu server to a hardened, monitored, backup-configured OpenClaw instance. The OpenClaw CVE hardening guide sits alongside this one – this guide builds the infrastructure, that guide explains the vulnerability mechanics and per-CVE mitigations. Read both.
For zero-trust remote access to the server itself (replacing SSH key management with short-lived certificates and hardware MFA), the Teleport self-hosted guide covers the full setup. For monitoring the entire stack with Prometheus and Grafana dashboards, the monitoring complete guide integrates directly with the metrics configuration shown here. And if you want to build a full AI agent pipeline on top of this OpenClaw deployment, the self-hosted AI agent stack guide covers n8n, Flowise, Qdrant, and Langfuse in the same Docker-first pattern. For SysAdmin and security articles across the full CoderOasis library, check the SysAdmin topic section and the Security section.
