Running OpenClaw on your laptop works well for experimentation, but a VPS deployment is what makes it genuinely useful. When your agent runs on a server, it is available 24/7 without your laptop needing to be on, it can handle cron jobs and scheduled tasks reliably, and it does not compete with your development workload for resources.
This post covers the full production deployment: provisioning a Linux server, installing OpenClaw, configuring it as a systemd service, securing it with a Cloudflare tunnel so the gateway is never exposed publicly, and setting up monitoring so you know when something goes wrong.
All commands are tested on Ubuntu 24.04 LTS, which is the recommended OS for this setup. The same steps work on Debian 12 with minor adjustments.
Choosing a VPS
OpenClaw is a Node.js process with an LLM connection. It does not need much compute on its own since the heavy lifting happens at the LLM provider. A minimal VPS is enough for a personal setup. Here is what to look for:
- RAM: 1 GB minimum, 2 GB recommended if you plan to run browser automation or multiple agents
- CPU: 1 vCPU is sufficient for personal use
- Storage: 20 GB minimum to accommodate logs, memory files, and skill scripts
- OS: Ubuntu 24.04 LTS
- Location: choose a region close to you for lower latency on Telegram and API calls
Good options include DigitalOcean Droplets, Hetzner Cloud, Vultr, and Azure B1s instances. For a personal agent, the cheapest tier on any of these providers is more than sufficient.
The Full Deployment Architecture
flowchart TD
Phone["Your Phone\n(Telegram / WhatsApp)"]
CF["Cloudflare Tunnel\n(encrypted, no open ports)"]
VPS["Linux VPS\nUbuntu 24.04"]
GW["OpenClaw Gateway\n(systemd service, loopback only)"]
LLM["LLM Provider\n(Anthropic / OpenAI / DeepSeek)"]
Skills["Skills and Memory\n(/home/openclaw/workspace)"]
Phone -->|"Message"| CF
CF -->|"Secure tunnel"| VPS
VPS --> GW
GW -->|"API call"| LLM
GW -->|"Reads / writes"| Skills
LLM -->|"Response"| GW
GW -->|"Reply via tunnel"| CF
CF -->|"Message"| Phone
The Cloudflare tunnel is the key security element here. The gateway binds to loopback only (127.0.0.1) and is never directly reachable from the internet. All external traffic flows through the Cloudflare tunnel, which handles TLS termination and authentication. This eliminates the most common OpenClaw security mistake: an exposed port 18789.
Step 1: Initial Server Setup
SSH into your fresh VPS as root and run the initial setup:
# Update the system
apt update && apt upgrade -y
# Create a dedicated non-root user for OpenClaw
adduser openclaw
usermod -aG sudo openclaw
# Set up SSH key authentication for the new user
mkdir -p /home/openclaw/.ssh
cp ~/.ssh/authorized_keys /home/openclaw/.ssh/
chown -R openclaw:openclaw /home/openclaw/.ssh
chmod 700 /home/openclaw/.ssh
chmod 600 /home/openclaw/.ssh/authorized_keys
# Disable root SSH login and password authentication
sed -i 's/PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config
sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
systemctl restart sshd
# Set up a basic firewall - allow SSH and nothing else inbound
ufw allow OpenSSH
ufw --force enable
ufw status
From this point, SSH in as the openclaw user, not root.
Step 2: Install Node.js 22
# Install nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
# Load nvm into current session
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
# Install Node.js 22 and set as default
nvm install 22
nvm use 22
nvm alias default 22
# Verify
node --version # v22.x.x
npm --version # 10.x.x
# Add nvm to shell profile so it loads on login
echo 'export NVM_DIR="$HOME/.nvm"' >> ~/.bashrc
echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> ~/.bashrc
source ~/.bashrc
Step 3: Install OpenClaw
# Install OpenClaw globally
npm install -g openclaw@latest
# Verify
openclaw --version
# If openclaw command is not found, add npm global bin to PATH
echo 'export PATH="$(npm config get prefix)/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
Step 4: Run the Onboard Wizard
On the VPS, run the onboard wizard without the daemon flag first. We will configure systemd manually for more control.
openclaw onboard
Complete the wizard: configure your LLM provider and API key, set your workspace directory (use the default ~/openclaw), and connect your Telegram bot. Skip the daemon installation step since we will handle that with systemd.
After the wizard completes, open ~/openclaw/openclaw.json and make sure the gateway is bound to loopback:
{
"gateway": {
"port": 18789,
"bind": "loopback"
}
}
Step 5: Configure systemd Service
We create a systemd user service so OpenClaw starts automatically on boot and restarts if it crashes, without needing root privileges.
# Create the systemd user service directory
mkdir -p ~/.config/systemd/user
# Find the full path to the openclaw binary
which openclaw # note this path, e.g. /home/openclaw/.nvm/versions/node/v22.x.x/bin/openclaw
Create the service file:
cat > ~/.config/systemd/user/openclaw-gateway.service << 'EOF'
[Unit]
Description=OpenClaw Gateway
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/home/openclaw/openclaw
ExecStart=/home/openclaw/.nvm/versions/node/v22.14.0/bin/openclaw gateway start --foreground
Restart=always
RestartSec=10
Environment=PATH=/home/openclaw/.nvm/versions/node/v22.14.0/bin:/usr/local/bin:/usr/bin:/bin
Environment=HOME=/home/openclaw
Environment=NODE_ENV=production
# Resource limits
MemoryMax=512M
CPUQuota=50%
[Install]
WantedBy=default.target
EOF
Note: replace v22.14.0 with the actual Node.js version from node --version.
# Enable lingering so the user service runs without an active SSH session
loginctl enable-linger openclaw
# Reload systemd and enable the service
systemctl --user daemon-reload
systemctl --user enable openclaw-gateway
systemctl --user start openclaw-gateway
# Check status
systemctl --user status openclaw-gateway
# View logs
journalctl --user -u openclaw-gateway -f
Step 6: Set Up a Cloudflare Tunnel
The Cloudflare tunnel lets you access your gateway dashboard remotely without exposing any ports on your VPS. It also protects the Telegram webhook endpoint if you use it.
sequenceDiagram
participant You as Your Browser
participant CF as Cloudflare Edge
participant Tunnel as cloudflared daemon
participant GW as OpenClaw Gateway (127.0.0.1:18789)
You->>CF: https://openclaw.yourdomain.com
CF->>Tunnel: Route via persistent tunnel
Tunnel->>GW: Forward to localhost:18789
GW-->>Tunnel: Response
Tunnel-->>CF: Response
CF-->>You: Secure HTTPS response
# Install cloudflared
curl -L --output cloudflared.deb \
https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared.deb
# Log in to Cloudflare (opens a browser link - copy it to your local machine)
cloudflared tunnel login
# Create a named tunnel
cloudflared tunnel create openclaw-tunnel
# Note the tunnel ID from the output, then create the config file
mkdir -p ~/.cloudflared
# Replace YOUR_TUNNEL_ID and yourdomain.com with your values
cat > ~/.cloudflared/config.yml << 'EOF'
tunnel: YOUR_TUNNEL_ID
credentials-file: /home/openclaw/.cloudflared/YOUR_TUNNEL_ID.json
ingress:
- hostname: openclaw.yourdomain.com
service: http://localhost:18789
- service: http_status:404
EOF
# Create a DNS record for the tunnel
cloudflared tunnel route dns openclaw-tunnel openclaw.yourdomain.com
Now create a systemd user service for cloudflared as well:
cat > ~/.config/systemd/user/cloudflared.service << 'EOF'
[Unit]
Description=Cloudflare Tunnel for OpenClaw
After=network-online.target openclaw-gateway.service
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/bin/cloudflared tunnel --config /home/openclaw/.cloudflared/config.yml run
Restart=always
RestartSec=15
[Install]
WantedBy=default.target
EOF
systemctl --user daemon-reload
systemctl --user enable cloudflared
systemctl --user start cloudflared
systemctl --user status cloudflared
Your OpenClaw dashboard is now accessible at https://openclaw.yourdomain.com without any open ports on your VPS. You can also add Cloudflare Access in front of it to require authentication before anyone can reach the dashboard.
Step 7: Add Cloudflare Access Authentication (Recommended)
Cloudflare Access adds a login layer in front of your tunnel so only you can reach the dashboard, even if someone discovers the hostname.
- Go to your Cloudflare dashboard and navigate to Zero Trust
- Under Access, create a new Application of type Self-Hosted
- Set the domain to
openclaw.yourdomain.com - Create a policy: allow only your email address (or a specific identity provider)
- Save the application
Now anyone visiting openclaw.yourdomain.com will hit a Cloudflare login page before reaching your gateway. This is a free feature on Cloudflare's Zero Trust plan for up to 50 users.
Step 8: Set Up Log Rotation
OpenClaw writes daily memory logs and conversation history. Without log rotation these accumulate and consume disk space over time.
# Create a logrotate config for OpenClaw memory logs
sudo tee /etc/logrotate.d/openclaw << 'EOF'
/home/openclaw/openclaw/memory/*.md {
weekly
rotate 12
compress
delaycompress
missingok
notifempty
create 644 openclaw openclaw
}
EOF
# Test the config
sudo logrotate --debug /etc/logrotate.d/openclaw
Step 9: Set Up a Health Check Script
This Node.js script checks that the gateway is responding and sends you a Telegram alert if it is not. You can run it from a cron job every few minutes.
// healthcheck.js
// Run with: node healthcheck.js
// Add to crontab: */5 * * * * node /home/openclaw/healthcheck.js
const http = require("http");
const https = require("https");
const GATEWAY_URL = "http://127.0.0.1:18789/health";
const TELEGRAM_TOKEN = process.env.TELEGRAM_BOT_TOKEN;
const TELEGRAM_CHAT_ID = process.env.TELEGRAM_CHAT_ID;
const TIMEOUT_MS = 5000;
function sendTelegramAlert(message) {
if (!TELEGRAM_TOKEN || !TELEGRAM_CHAT_ID) return;
const body = JSON.stringify({
chat_id: TELEGRAM_CHAT_ID,
text: `OpenClaw Health Alert\n\n${message}\n\nServer: ${process.env.HOSTNAME || "VPS"}`,
});
const req = https.request({
hostname: "api.telegram.org",
path: `/bot${TELEGRAM_TOKEN}/sendMessage`,
method: "POST",
headers: { "Content-Type": "application/json", "Content-Length": body.length },
});
req.write(body);
req.end();
}
function checkHealth() {
const req = http.get(GATEWAY_URL, { timeout: TIMEOUT_MS }, (res) => {
if (res.statusCode === 200) {
console.log(`[${new Date().toISOString()}] Gateway healthy - HTTP ${res.statusCode}`);
} else {
const msg = `Gateway returned HTTP ${res.statusCode}`;
console.error(`[${new Date().toISOString()}] ${msg}`);
sendTelegramAlert(msg);
}
});
req.on("timeout", () => {
req.destroy();
const msg = "Gateway health check timed out after 5 seconds";
console.error(`[${new Date().toISOString()}] ${msg}`);
sendTelegramAlert(msg);
});
req.on("error", (err) => {
const msg = `Gateway unreachable: ${err.message}`;
console.error(`[${new Date().toISOString()}] ${msg}`);
sendTelegramAlert(msg);
});
}
checkHealth();
# Save the script and add environment variables
cp healthcheck.js ~/healthcheck.js
# Add to crontab - runs every 5 minutes
(crontab -l 2>/dev/null; echo "*/5 * * * * TELEGRAM_BOT_TOKEN=your_token TELEGRAM_CHAT_ID=your_chat_id node /home/openclaw/healthcheck.js >> /home/openclaw/healthcheck.log 2>&1") | crontab -
Monitoring Your Deployment
flowchart TD
subgraph Monitor["Monitoring Stack"]
HC["healthcheck.js\n(cron every 5 min)"]
JC["journalctl\n(systemd logs)"]
OC["openclaw logs --follow\n(gateway logs)"]
DF["df -h\n(disk usage)"]
end
HC -->|"Failure"| TG["Telegram Alert"]
JC -->|"Review"| Dev["You"]
OC -->|"Debug"| Dev
DF -->|"Low disk"| Action["Clean old logs\nExpand storage"]
Key commands to keep your deployment healthy:
# View gateway service status
systemctl --user status openclaw-gateway
# Stream live gateway logs
journalctl --user -u openclaw-gateway -f
# View OpenClaw's own logs
openclaw logs --follow
# Check disk usage
df -h
du -sh ~/openclaw/memory/
# Restart gateway after config changes
systemctl --user restart openclaw-gateway
# Update OpenClaw to latest version
npm install -g openclaw@latest
systemctl --user restart openclaw-gateway
openclaw --version
# Run diagnostics
openclaw doctor
Handling Automatic Updates
OpenClaw updates frequently. A simple update script with a cron job keeps your installation current without manual intervention:
# update-openclaw.sh
#!/bin/bash
CURRENT=$(openclaw --version 2>/dev/null)
npm install -g openclaw@latest --quiet
UPDATED=$(openclaw --version 2>/dev/null)
if [ "$CURRENT" != "$UPDATED" ]; then
echo "[$(date)] Updated from $CURRENT to $UPDATED"
systemctl --user restart openclaw-gateway
systemctl --user status openclaw-gateway --no-pager
else
echo "[$(date)] Already on latest: $CURRENT"
fi
chmod +x ~/update-openclaw.sh
# Run update check daily at 3 AM
(crontab -l 2>/dev/null; echo "0 3 * * * /home/openclaw/update-openclaw.sh >> /home/openclaw/update.log 2>&1") | crontab -
Complete Deployment Checklist
flowchart TD
A["VPS provisioned\nUbuntu 24.04"]
A --> B["Non-root user created\nSSH hardened\nufw enabled"]
B --> C["Node.js 22 installed\nvia nvm"]
C --> D["OpenClaw installed\nfrom npm"]
D --> E["Onboard wizard complete\nLLM + Telegram configured"]
E --> F["gateway.bind = loopback\nin openclaw.json"]
F --> G["systemd user service\ncreated and enabled"]
G --> H["loginctl enable-linger\nset for auto-start"]
H --> I["Cloudflare tunnel\ninstalled and running"]
I --> J["Cloudflare Access\nprotecting dashboard"]
J --> K["Log rotation\nconfigured"]
K --> L["Health check cron\nwith Telegram alerts"]
L --> M["Auto-update cron\ndaily at 3 AM"]
M --> N["Production ready"]
Troubleshooting Common VPS Issues
- Service not starting after reboot: check that
loginctl enable-linger openclawwas run and that nvm is loaded in the service's PATH - Gateway starts but Telegram does not respond: check outbound connectivity to api.telegram.org from the VPS with
curl https://api.telegram.org - Memory growing unbounded: check
du -sh ~/openclaw/memory/and confirm logrotate is configured correctly - High CPU usage: usually caused by a skill running a tight loop or the browser tool opening many pages. Check
openclaw logs --followfor repeated activity - Cloudflare tunnel disconnecting: check
systemctl --user status cloudflaredand review logs withjournalctl --user -u cloudflared -f
What Is Next
Your agent is now running 24/7 on a server with no exposed ports, automatic restarts, and health monitoring. In Part 6 we go deep on security: mitigating CVE-2026-25253 and similar vulnerabilities, hardening skill permissions, auditing community skills before installation, and building a secure configuration checklist you can apply to any OpenClaw deployment.
References
- OpenClaw GitHub Repository - github.com/openclaw/openclaw
- OpenClaw Documentation - docs.openclaw.ai
- Valletta Software - OpenClaw Architecture and Security Guide 2026
- Smithery - OpenClaw Setup Skill (AWS deployment reference)
- Cloudflare - Cloudflare Tunnel Documentation
- systemd Service Documentation - freedesktop.org
- Towards AI - OpenClaw Complete Tutorial 2026
