// 01 — introduction
Most "AI assistant" products want you to send your conversations, your files, and your integrations through someone else's infrastructure. OpenClaw doesn't. It's an open-source gateway you run yourself — it handles routing, channel connections, and agent sessions entirely on your own hardware. The only data that leaves your machine is what you explicitly send to your LLM provider.
This guide takes the long path on purpose. We're not running OpenClaw as root, we're not skipping the firewall, and we're not hand-waving the security bits. We're spinning up a Linode VPS, locking it down, and running the agent under a dedicated unprivileged account — the way a service should be run. Here's the full stack:
- ▸ A Linode 2 GB instance running Ubuntu 24.04 LTS, protected by Linode's Cloud Firewall
- ▸ A hardened OS baseline — SSH key auth only, UFW, automatic security updates
- ▸ A dedicated clawuser account with no sudo rights
- ▸ OpenClaw installed under that account and running as a systemd user service
- ▸ A Discord bot connected, paired, and responding in your server
By the end you'll have a self-hosted AI agent running on a server you control, reachable from Discord, with no unnecessary ports exposed and no elevated privileges in sight.
// 02 — vm build (linode)
We're running this on Linode (now Akamai Connected Cloud) — straightforward pricing, clean Ubuntu images, and a solid Cloud Firewall product that sits in front of your instance before traffic even hits the NIC. Good fit for a self-hosted gateway.
Step 1 — Create a Linode account
Head to cloud.linode.com and sign up. Once you're in, you'll land on the Cloud Manager dashboard. Everything below happens here.
Step 2 — Add an SSH key
Before creating the instance, upload your public SSH key so Linode can pre-provision it. This way you never need a root password to log in.
-
Generate a key pair if you don't have onelocal machinebash
ssh-keygen -t ed25519 -C "openclaw-linode" # Accept defaults — key lands in ~/.ssh/id_ed25519.pub -
Upload to Linode
In Cloud Manager: Profile (top right) → SSH Keys → Add an SSH Key. Paste the contents of ~/.ssh/id_ed25519.pub. Give it a label and save.
Step 3 — Create the Linode instance
-
Start the wizard
Click Create → Linode in the top navigation bar.
-
Choose an image
Under Choose a Distribution, select Ubuntu 24.04 LTS.
-
Choose a region
Pick the region closest to you. Latency won't matter much for this workload, but backups and egress pricing are region-dependent.
-
Choose a plan
Select Shared CPU → Linode 2 GB ($12/mo). The 1 GB Nanode isn't enough — OpenClaw's gateway plus a running agent session will exhaust it. The 2 GB plan is the minimum you want here.
-
Set a label and root password
Label it something like openclaw-01. Set a strong root password — you won't use it to log in day-to-day, but Linode requires one and it's your break-glass credential.
-
Add your SSH key
Under SSH Keys, check the key you uploaded in Step 2. Linode will drop it into /root/.ssh/authorized_keys on first boot.
-
Create the Linode
Click Create Linode. Provisioning takes about 60 seconds. The instance's public IPv4 address appears in the dashboard once it's running.
Step 4 — Create the Cloud Firewall
Linode's Cloud Firewall filters traffic at the infrastructure level — packets are dropped before they reach your instance. This is what you want: a hard perimeter that doesn't depend on anything running inside the VM.
-
Open the Firewall manager
In Cloud Manager: Network → Firewalls → Create Firewall. Give it a label (e.g. openclaw-fw) and assign it to your Linode instance.
-
Configure inbound rules
Add the following rules under Inbound Rules. Delete any default "accept all" rule if one exists.
Label Protocol Port Sources Action allow-ssh TCP 22 Your IP (not 0.0.0.0/0) Accept drop-all All All 0.0.0.0/0, ::/0 Drop Rules are evaluated top-to-bottom. SSH allow rule must come before the drop-all catch. Restrict the SSH source to your own IP — don't leave it open to the world.
-
Configure outbound rules
Leave outbound as Accept All (the default). OpenClaw needs to reach your LLM provider, Discord's API, and package mirrors. There's no benefit to restricting outbound on this workload.
-
Save and apply
Click Save Changes. The firewall is applied immediately — no reboot required. Confirm SSH still works from your machine before moving on.
// 03 — os setup & configuration
Linode provisions with root access. First login is as root — we'll create a proper admin account before we're done and lock root out of SSH.
ssh root@<LINODE_IP>
Set the hostname
hostnamectl hostname openclaw
hostnamectl # confirm
System updates
apt update && apt full-upgrade -y
apt autoremove -y
Install essentials
apt install -y \
curl git ufw fail2ban unattended-upgrades \
build-essential ca-certificates gnupg
Configure fail2ban
fail2ban watches auth logs and bans IPs that repeatedly fail SSH logins. Don't rely on it as your only line of defence — that's what the Linode Cloud Firewall is for — but it adds a useful layer inside the VM. Create a jail config for SSH:
[sshd]
enabled = true
port = ssh
filter = sshd
backend = systemd
maxretry = 5
findtime = 10m
bantime = 1h
ignoreip = 127.0.0.1/8 ::1
systemctl restart fail2ban
systemctl enable fail2ban
fail2ban-client status # should show sshd jail active
fail2ban-client status sshd
Firewall (UFW)
Linode's Cloud Firewall already blocks traffic at the network edge. UFW adds a second layer inside the instance — defence in depth.
ufw default deny incoming
ufw default allow outgoing
ufw allow ssh
ufw enable
ufw status
Harden SSH
Edit /etc/ssh/sshd_config and set the following:
PasswordAuthentication no
PermitRootLogin no
systemctl restart ssh
systemctl status ssh
Enable automatic security updates
dpkg-reconfigure --priority=low unattended-upgrades
# Choose "Yes" when prompted
Create user accounts
Two accounts: clawadmin for day-to-day administration (sudo, SSH access), and clawuser for running OpenClaw with no elevated privileges whatsoever.
# Create the admin user and add to sudo group
adduser clawadmin
usermod -aG sudo clawadmin
# Verify sudo membership
groups clawadmin
ssh-copy-id clawadmin@<LINODE_IP>
Then open a second SSH session as clawadmin and confirm sudo whoami returns root before closing your root session.
# Create the OpenClaw service user
adduser clawuser
# Verify: clawuser should NOT be in sudo group
groups clawuser
Enable lingering so clawuser's systemd user services survive logouts and start automatically on boot:
loginctl enable-linger clawuser
// 04 — openclaw install (unprivileged user)
Switch into the clawuser account. Everything in this section runs as clawuser, not your admin account.
sudo -i -u clawuser
# Prompt should now show: clawuser@openclaw:~$
Install Node.js 22 (via fnm)
We install Node under clawuser's home directory using fnm (Fast Node Manager). No root required, no system Node to conflict with.
# Install fnm into ~/.local/bin
curl -fsSL https://fnm.vercel.app/install | bash
# Load fnm in the current shell
export PATH="$HOME/.local/bin:$PATH"
eval "$(fnm env --use-on-cd)"
# Install and activate Node 22
fnm install 22
fnm use 22
fnm default 22
# Verify
node --version # should print v22.x.x
npm --version
Make fnm available in every future shell session:
cat >> ~/.bashrc <<'EOF'
# fnm (Node version manager)
export PATH="$HOME/.local/bin:$PATH"
eval "$(fnm env --use-on-cd)"
EOF
source ~/.bashrc
Install OpenClaw
npm install -g openclaw@latest
# Verify
openclaw --version
Run the onboarding wizard
The wizard sets up auth, workspace, gateway config, and the systemd user service in one guided flow.
openclaw onboard --install-daemon
When the wizard asks:
- ▸ QuickStart or Advanced? — QuickStart is fine unless you have specific needs
- ▸ Model/Auth: enter your Anthropic API key and pick a model
- ▸ Workspace: accept the default (~/.openclaw/workspace)
- ▸ Gateway port: default 18789, loopback bind — no public exposure
- ▸ Channels: skip for now — we'll configure Discord manually next
- ▸ Install daemon: Yes — this creates a systemd user unit for clawuser
Verify the gateway
# Check gateway status
openclaw gateway status
# Run the doctor — catches config issues
openclaw doctor
# Confirm the systemd user service is active
systemctl --user status openclaw
# Tail live logs
openclaw logs --follow
You should see Runtime: running and RPC probe: ok. If openclaw doctor flags anything, fix it before moving on.
// 05 — discord configuration
We need two things: a Discord bot with the right permissions, and OpenClaw configured to use it. The bot token is a secret — treat it like a password. We'll set it via the CLI so it never touches a chat message or log file.
Step 1 — Create the Discord application and bot
-
New application
Go to the Discord Developer Portal, click New Application, and name it (e.g. "OpenClaw").
-
Create the bot
Click Bot in the sidebar. Set the bot's username — this is what will appear in your Discord server. Then scroll to Privileged Gateway Intents and enable:
- ✓ Message Content Intent (required)
- ✓ Server Members Intent (recommended)
-
Copy the bot token
Click Reset Token (don't worry — this generates your first token, nothing breaks). Copy it and save it somewhere secure. You'll need it shortly.
Step 2 — Invite the bot to your server
-
Generate the invite URL
Click OAuth2 in the sidebar → OAuth2 URL Generator. Enable scopes: bot and applications.commands. A Bot Permissions panel appears below — enable:
- ✓ View Channels
- ✓ Send Messages
- ✓ Read Message History
- ✓ Embed Links
- ✓ Attach Files
- ✓ Add Reactions (optional)
Do not grant Administrator. Least privilege.
-
Add the bot
Copy the generated URL at the bottom, paste it in your browser, select your server, and click Authorize. The bot should now appear in your server's member list (offline for now).
Step 3 — Collect your IDs
Enable Discord Developer Mode: User Settings → Advanced → Developer Mode. Then right-click to copy:
- ▸ Server ID — right-click server icon → Copy Server ID
- ▸ Your User ID — right-click your avatar → Copy User ID
Keep these alongside your bot token. You'll use all three in the next step.
Step 4 — Configure OpenClaw (as clawuser)
# Set the token — replace YOUR_BOT_TOKEN with the real value
openclaw config set channels.discord.token '"YOUR_BOT_TOKEN"' --json
# Enable the Discord channel
openclaw config set channels.discord.enabled true --json
# Add your server to the guild allowlist
# Replace YOUR_SERVER_ID and YOUR_USER_ID with real values
openclaw config set \
'channels.discord.guilds.YOUR_SERVER_ID' \
'{"requireMention": false, "users": ["YOUR_USER_ID"]}' \
--json
# Set guild policy to allowlist (secure default)
openclaw config set channels.discord.groupPolicy '"allowlist"' --json
# Restart the gateway to pick up the new config
openclaw gateway restart
Step 5 — Allow DMs and pair
In Discord, right-click your server icon → Privacy Settings and enable Direct Messages. This lets the bot DM you for the pairing handshake.
Now DM your bot in Discord. It will respond with a pairing code. Back on the VM (as clawuser), approve it:
# List pending pairing requests
openclaw pairing list discord
# Approve with the code from your DM
openclaw pairing approve discord <CODE>
Step 6 — Verify
openclaw channels status --probe
Discord should show as connected. Send a message in any channel on your server — the bot will respond. Send a DM — it will respond there too.
That's the full stack: isolated VM, hardened OS, unprivileged service account, OpenClaw gateway running as a proper systemd user service, and a Discord bot that answers to you and only you. Clean, contained, and entirely yours. No subscriptions. No data leaving your control. Just a process running on metal you own, doing exactly what you tell it.
From here: explore channels, install skills from clawhub.com, and read the official docs for anything this guide skipped. There's a lot more under the hood.