Content is user-generated and unverified.

Deploy OpenClaw on AWS EC2

A practical, step-by-step guide to running your own OpenClaw AI assistant on an AWS EC2 instance, secured with Tailscale. Based on a real deployment — every command has been tested and every gotcha is documented.

What you'll get: A private, always-on AI assistant accessible from any device on your Tailscale network, with web search capabilities via Brave Search.

Time required: ~60 minutes Monthly cost: ~$33 (t3.medium + 30GB storage)


Architecture Overview

Your Mac (Pulumi CLI)  →  AWS APIs  →  Creates EC2 instance
                                         └─ Cloud-init auto-installs:
                                            ├─ Node.js 22 (via NVM)
                                            ├─ OpenClaw
                                            └─ Tailscale

Your browser  →  Tailscale VPN  →  EC2:18789 (OpenClaw Gateway)

OpenClaw runs entirely on the EC2 instance. Your Mac only needs Pulumi to provision the infrastructure and Tailscale to access the Web UI. No ports are exposed to the public internet.


Phase 1: Prerequisites

Gather credentials and install local tools before starting the deployment.

1.1 — Get an Anthropic API Key

  1. Go to https://console.anthropic.com/settings/keys
  2. Create a new API key
  3. Important: Make sure your account has billing enabled with credits loaded. A key without credits will silently fail — the assistant will appear to respond but the messages will be empty.

Save your key (starts with sk-ant-api03-...).

1.2 — Get a Brave Search API Key

  1. Go to https://brave.com/search/api/
  2. Select the "Data for Search" plan (not "Data for AI") — the free tier gives you 2,000 queries/month
  3. Copy your API key

1.3 — Set Up Tailscale

Tailscale creates a private mesh VPN so you can access OpenClaw securely without exposing any ports to the internet.

  1. Create an account at https://tailscale.com
  2. Install Tailscale on your Mac from https://tailscale.com/download
  3. Enable HTTPS certificates:
  4. Note your Tailnet name from the DNS page — it looks like taila1b2c3.ts.net
  5. Generate an auth key:

1.4 — Install Local Tools

These run on your Mac to provision the EC2 infrastructure:

bash
# Node.js 22+ (required for Pulumi)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.zshrc  # or ~/.bashrc
nvm install 22

# Pulumi CLI
curl -fsSL https://get.pulumi.com | sh

# AWS CLI
brew install awscli

Verify everything:

bash
node --version    # Should show v22.x.x
pulumi version    # Should show v3.x.x
aws --version     # Should show aws-cli/2.x.x

Phase 1 Checklist

ItemStatus
Anthropic API Key (with billing enabled)
Brave Search API Key
Tailscale Auth Key (reusable)
Tailnet DNS Name
Node.js 22+ installed locally
Pulumi CLI installed locally
AWS CLI installed locally

Phase 2: AWS Setup

Create an IAM user that Pulumi will use to provision resources.

2.1 — Create an IAM User

  1. Go to https://console.aws.amazon.com/iam
  2. Navigate to Users → Create user
  3. Name it pulumi-deployer
  4. Attach these policies directly:
    • AmazonEC2FullAccess
    • AmazonVPCFullAccess
  5. Go to the user → Security credentials → Create access key
  6. Select "Command Line Interface (CLI)" and create the key
  7. Save both the Access Key ID and Secret Access Key

2.2 — Configure AWS CLI

bash
aws configure

Enter when prompted:

AWS Access Key ID: AKIA...your-key...
AWS Secret Access Key: ...your-secret...
Default region name: us-east-1
Default output format: json

Verify it works:

bash
aws sts get-caller-identity

You should see your account ID and the pulumi-deployer user ARN.

Troubleshooting: If you get InvalidClientTokenId, the most common cause is a copy/paste error with whitespace. Run aws configure again and carefully re-enter both keys. You can verify what's stored with aws configure list.


Phase 3: Create the Pulumi Project

3.1 — Initialize the project

bash
mkdir ~/openclaw-aws
cd ~/openclaw-aws
pulumi new typescript --name openclaw-aws --description "OpenClaw deployment on AWS EC2"

When prompted:

  • Stack name: Press Enter to accept dev
  • Package manager: Select npm
  • Wait for dependencies to install

Troubleshooting: If the directory already exists from a previous attempt, either use --force or delete it first with rm -rf ~/openclaw-aws and start over.

3.2 — Install AWS dependencies

bash
npm install @pulumi/aws @pulumi/tls

3.3 — Replace index.ts

Open index.ts and replace the entire contents with the following. This creates a VPC, subnet, security group, and EC2 instance with a cloud-init script that automatically installs OpenClaw:

typescript
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
import * as tls from "@pulumi/tls";
import * as crypto from "crypto";

// ── Config ──────────────────────────────────────────────────────────
const cfg = new pulumi.Config();
const anthropicApiKey = cfg.requireSecret("anthropicApiKey");
const tailscaleAuthKey = cfg.requireSecret("tailscaleAuthKey");
const tailnetDnsName   = cfg.require("tailnetDnsName");

// Generate a random gateway token
const gatewayToken = crypto.randomBytes(24).toString("hex");

// ── VPC ─────────────────────────────────────────────────────────────
const vpc = new aws.ec2.Vpc("openclaw-vpc", {
    cidrBlock: "10.0.0.0/16",
    enableDnsHostnames: true,
    enableDnsSupport: true,
    tags: { Name: "openclaw-vpc" },
});

const subnet = new aws.ec2.Subnet("openclaw-subnet", {
    vpcId: vpc.id,
    cidrBlock: "10.0.1.0/24",
    mapPublicIpOnLaunch: true,
    availabilityZone: "us-east-1a",
    tags: { Name: "openclaw-subnet" },
});

const igw = new aws.ec2.InternetGateway("openclaw-igw", {
    vpcId: vpc.id,
    tags: { Name: "openclaw-igw" },
});

const rt = new aws.ec2.RouteTable("openclaw-rt", {
    vpcId: vpc.id,
    routes: [{ cidrBlock: "0.0.0.0/0", gatewayId: igw.id }],
    tags: { Name: "openclaw-rt" },
});

new aws.ec2.RouteTableAssociation("openclaw-rta", {
    subnetId: subnet.id,
    routeTableId: rt.id,
});

// ── Security Group ──────────────────────────────────────────────────
const sg = new aws.ec2.SecurityGroup("openclaw-sg", {
    vpcId: vpc.id,
    description: "OpenClaw - SSH only (Tailscale handles app access)",
    ingress: [
        { protocol: "tcp", fromPort: 22, toPort: 22,
          cidrBlocks: ["0.0.0.0/0"], description: "SSH fallback" },
    ],
    egress: [
        { protocol: "-1", fromPort: 0, toPort: 0,
          cidrBlocks: ["0.0.0.0/0"], description: "All outbound" },
    ],
    tags: { Name: "openclaw-sg" },
});

// ── SSH Key ─────────────────────────────────────────────────────────
const sshKey = new tls.PrivateKey("openclaw-key", { algorithm: "RSA", rsaBits: 4096 });
const keyPair = new aws.ec2.KeyPair("openclaw-keypair", {
    publicKey: sshKey.publicKeyOpenssh,
});

// ── AMI ─────────────────────────────────────────────────────────────
const ami = aws.ec2.getAmiOutput({
    mostRecent: true,
    owners: ["099720109477"], // Canonical
    filters: [
        { name: "name", values: ["ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*"] },
        { name: "state", values: ["available"] },
    ],
});

// ── Cloud-Init Script ───────────────────────────────────────────────
const userData = pulumi.all([anthropicApiKey, tailscaleAuthKey]).apply(
    ([apiKey, tsKey]) => `#!/bin/bash
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive

# System updates
apt-get update -y
apt-get upgrade -y

# Install NVM and Node.js 22 for ubuntu user
sudo -u ubuntu bash -c '
    curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
    export NVM_DIR="/home/ubuntu/.nvm"
    [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
    nvm install 22
    nvm use 22
    npm install -g openclaw@latest
'

# Install Tailscale
curl -fsSL https://tailscale.com/install.sh | sh
tailscale up --authkey="${tsKey}" --ssh

# Run OpenClaw onboarding as ubuntu user
sudo -u ubuntu bash -c '
    export NVM_DIR="/home/ubuntu/.nvm"
    [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
    export XDG_RUNTIME_DIR="/run/user/$(id -u)"
    
    # Run onboarding (may show a warning about gateway connection - this is expected)
    openclaw onboard --install-daemon --no-welcome || echo "WARNING: OpenClaw onboarding failed"
'

# Configure OpenClaw
sudo -u ubuntu bash -c '
    cat > ~/.openclaw/openclaw.json << JSONEOF
{
  "messages": {
    "ackReactionScope": "group-mentions"
  },
  "agents": {
    "defaults": {
      "maxConcurrent": 4,
      "subagents": { "maxConcurrent": 8 },
      "contextPruning": { "mode": "cache-ttl", "ttl": "1h" },
      "heartbeat": { "every": "30m" },
      "compaction": { "mode": "safeguard" },
      "workspace": "/home/ubuntu/.openclaw/workspace"
    }
  },
  "gateway": {
    "mode": "local",
    "auth": {
      "mode": "token",
      "token": "${gatewayToken}"
    },
    "port": 18789,
    "bind": "loopback",
    "tailscale": { "mode": "off", "resetOnExit": false },
    "trustedProxies": ["127.0.0.1"],
    "controlUi": {
      "enabled": true,
      "allowInsecureAuth": true
    }
  },
  "auth": {
    "profiles": {
      "anthropic:default": {
        "provider": "anthropic",
        "mode": "api_key"
      }
    }
  }
}
JSONEOF

    # Set up the Anthropic API key in the agent auth profile
    mkdir -p ~/.openclaw/agents/main/agent
    cat > ~/.openclaw/agents/main/agent/auth-profiles.json << AUTHEOF
{
  "version": 1,
  "profiles": {
    "anthropic:default": {
      "type": "api_key",
      "provider": "anthropic",
      "key": "${apiKey}"
    }
  },
  "lastGood": {
    "anthropic": "anthropic:default"
  }
}
AUTHEOF
'

# Install and start the daemon
sudo -u ubuntu bash -c '
    export NVM_DIR="/home/ubuntu/.nvm"
    [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
    export XDG_RUNTIME_DIR="/run/user/$(id -u)"
    openclaw daemon install || true
    openclaw gateway restart || true
'

# Enable lingering so the user service starts on boot
loginctl enable-linger ubuntu

# Configure Tailscale HTTPS proxy
tailscale serve --bg 18789

echo "OpenClaw setup complete!"
`);

// ── EC2 Instance ────────────────────────────────────────────────────
const instance = new aws.ec2.Instance("openclaw-instance", {
    ami: ami.id,
    instanceType: "t3.medium",
    subnetId: subnet.id,
    vpcSecurityGroupIds: [sg.id],
    keyName: keyPair.keyName,
    userData: userData,
    rootBlockDevice: {
        volumeSize: 30,
        volumeType: "gp3",
    },
    tags: { Name: "openclaw" },
});

// ── Outputs ─────────────────────────────────────────────────────────
const tailscaleHostname = instance.privateIp.apply(
    ip => `ip-${ip.replace(/\./g, "-")}.${tailnetDnsName}`
);

export const publicIp = instance.publicIp;
export const publicDns = instance.publicDns;
export const sshCommand = pulumi.interpolate`ssh -i key.pem ubuntu@${instance.publicIp}`;
export const privateKey = sshKey.privateKeyPem;
export const gatewayTokenOutput = gatewayToken;
export const tailscaleUrl = pulumi.interpolate`https://${tailscaleHostname}/`;
export const tailscaleUrlWithToken = pulumi.interpolate`https://${tailscaleHostname}/?token=${gatewayToken}`;

3.4 — Configure Secrets

Run each command one at a time, replacing the placeholder values with your actual credentials:

bash
cd ~/openclaw-aws

# Your Anthropic API key (encrypted in state)
pulumi config set anthropicApiKey YOUR_ANTHROPIC_API_KEY --secret

# Your Tailscale auth key (encrypted in state)
pulumi config set tailscaleAuthKey YOUR_TAILSCALE_AUTH_KEY --secret

# Your Tailnet DNS name (plaintext)
pulumi config set tailnetDnsName YOUR_TAILNET_NAME.ts.net

# AWS region
pulumi config set aws:region us-east-1

Verify the config file:

bash
cat Pulumi.dev.yaml

You should see your Tailnet name in plain text and the other values encrypted.


Phase 4: Deploy

4.1 — Preview the deployment

bash
pulumi preview

You should see ~15 resources to be created. Review to make sure it looks clean.

4.2 — Deploy

bash
pulumi up

Type yes when prompted. Deployment takes about 2 minutes. When complete, you'll see outputs including:

  • publicIp — the EC2 public IP
  • sshCommand — how to SSH in
  • tailscaleUrlWithToken — your Web UI URL (save this!)
  • gatewayTokenOutput — your gateway auth token

4.3 — Save your SSH key

bash
pulumi stack output privateKey --show-secrets > key.pem
chmod 600 key.pem

4.4 — Wait for cloud-init (~5 minutes)

The EC2 instance runs a cloud-init script that installs everything automatically. Monitor progress:

bash
ssh -i key.pem ubuntu@$(pulumi stack output publicIp)

Once connected:

bash
tail -f /var/log/cloud-init-output.log

Wait until you see: OpenClaw setup complete!

Note: You may see a warning like WARNING: OpenClaw onboarding failed with a message about "gateway closed (1006 abnormal closure)". This is normal — it happens because the onboarding script tried to connect to the gateway before the daemon was installed. The daemon gets installed right after, so everything works fine.

Press Ctrl+C when done watching.


Phase 5: Verify

5.1 — Check OpenClaw on the EC2 instance

While SSHed into the instance:

bash
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"

openclaw --version
openclaw gateway status

You should see:

  • Version: 2026.1.x or later
  • Gateway: running (active state)
  • Port: 18789 (loopback)

Note: You may see a "gateway token mismatch" warning from the CLI. This is cosmetic — it means the CLI doesn't have the token configured for itself, but the Web UI uses the token via URL parameter so it works fine.

5.2 — Access the Web UI

On your Mac, make sure Tailscale is connected (check the menu bar icon).

Open the Tailscale URL with token in your browser. You can get it with:

bash
# Run this on your Mac, not on the EC2
cd ~/openclaw-aws
pulumi stack output tailscaleUrlWithToken

It looks like: https://ip-10-0-1-XXX.yournet.ts.net/?token=abc123...

5.3 — Test the assistant

Send a message in the Web UI:

"Hello! What can you do?"

You should get a response from Claude within a few seconds. The assistant will introduce itself and ask you to personalize it.

Troubleshooting — Empty responses: If the assistant appears to respond but messages are blank:

  1. SSH into the EC2 and check logs: openclaw logs --limit 20
  2. If runs complete in ~300ms (too fast for real API calls), the API key likely has no billing credits
  3. Go to https://console.anthropic.com/settings/plans and add credits
  4. Retry — no restart needed

Phase 6: Add Brave Search

Give your assistant the ability to search the web.

6.1 — SSH into the EC2

bash
ssh -i ~/openclaw-aws/key.pem ubuntu@$(cd ~/openclaw-aws && pulumi stack output publicIp)

Load NVM:

bash
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"

6.2 — Edit the OpenClaw config

bash
nano ~/.openclaw/openclaw.json

Find the "auth" section and add a "tools" block before it. The relevant portion should look like:

json
  },
  "tools": {
    "web": {
      "search": {
        "enabled": true,
        "provider": "brave",
        "apiKey": "YOUR_BRAVE_API_KEY",
        "maxResults": 5
      },
      "fetch": {
        "enabled": true
      }
    }
  },
  "auth": {

Replace YOUR_BRAVE_API_KEY with your actual Brave Search API key.

Save with Ctrl+O, Enter, Ctrl+X.

6.3 — Restart the gateway

bash
openclaw gateway restart

6.4 — Test web search

Go to the Web UI and send:

"Search the web for the latest AI news today"

The assistant should perform a web search and return a summary with sources. The response will take 10-25 seconds as it searches and processes multiple results.


Phase 7: Security Hardening (Recommended)

7.1 — Verify Tailscale SSH works

Before removing the SSH fallback, confirm you can SSH via Tailscale:

bash
# Find your EC2's Tailscale hostname
tailscale status  # Run on your Mac

# SSH via Tailscale (no key needed — Tailscale handles auth)
ssh ubuntu@ip-10-0-1-XXX  # Use the Tailscale hostname

7.2 — Remove the public SSH rule

Once Tailscale SSH is confirmed working, you can remove the public SSH security group rule so the instance has zero public ports. Edit the index.ts security group to have an empty ingress array, then run pulumi up.

7.3 — Rotate credentials

Since credentials were handled during setup, rotate them all:

  1. Anthropic API Key: Generate a new one at https://console.anthropic.com/settings/keys, then update ~/.openclaw/agents/main/agent/auth-profiles.json on the EC2
  2. Brave Search API Key: Regenerate at https://brave.com/search/api/, then update ~/.openclaw/openclaw.json on the EC2
  3. Tailscale Auth Key: Delete the old one at https://login.tailscale.com/admin/settings/keys (the instance is already registered, so it stays connected)
  4. AWS Access Keys: Rotate at https://console.aws.amazon.com/iam

After updating any keys on the EC2, restart the gateway:

bash
openclaw gateway restart

Quick Reference

Useful Commands (on EC2)

bash
# Always load NVM first in a new SSH session
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"

# Gateway management
openclaw gateway status        # Check if running
openclaw gateway restart       # Restart after config changes
openclaw gateway stop          # Stop the gateway

# Logs
openclaw logs --limit 30       # Recent logs
tail -f /tmp/openclaw/openclaw-$(date +%Y-%m-%d).log  # Live log stream

# Diagnostics
openclaw --version             # Check version
openclaw doctor                # Run diagnostics

# Configuration files
cat ~/.openclaw/openclaw.json                            # Main config
cat ~/.openclaw/agents/main/agent/auth-profiles.json     # API keys

Useful Commands (on Mac)

bash
cd ~/openclaw-aws

# Infrastructure management
pulumi stack output tailscaleUrlWithToken    # Get your Web UI URL
pulumi stack output publicIp                 # Get the EC2 public IP
pulumi up                                    # Apply changes
pulumi destroy                               # Tear everything down

# SSH access
ssh -i key.pem ubuntu@$(pulumi stack output publicIp)

File Locations on EC2

FilePurpose
~/.openclaw/openclaw.jsonMain configuration (gateway, tools, agents)
~/.openclaw/agents/main/agent/auth-profiles.jsonAPI keys for the main agent
~/.openclaw/workspace/Assistant's working directory
~/.openclaw/agents/main/sessions/Conversation history
/tmp/openclaw/Log files
~/.config/systemd/user/openclaw-gateway.serviceSystemd service definition

Tear Down

To destroy all AWS resources and stop billing:

bash
cd ~/openclaw-aws
pulumi destroy

Type yes to confirm. This removes the EC2 instance, VPC, and all associated resources.


Troubleshooting

"command not found: openclaw" on EC2

NVM isn't loaded in your current shell. Run:

bash
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"

Empty assistant responses (messages appear but are blank)

The API key doesn't have billing credits. Go to https://console.anthropic.com/settings/plans, add credits, and retry. No restart needed.

"gateway token mismatch" warning

This appears when the CLI can't authenticate to the gateway. It's cosmetic — the Web UI works because it passes the token as a URL parameter. The gateway itself is running fine.

Cloud-init shows "WARNING: OpenClaw onboarding failed"

This is expected. The onboarding script runs before the daemon is fully installed and tries to connect to the gateway. The daemon gets installed immediately after, and everything works. Check for OpenClaw setup complete! at the end of the log.

Web search hangs or times out

Brave Search API may occasionally time out. Retry the message. If it keeps happening, check your Brave API key quota at https://brave.com/search/api/ (free tier is 2,000 queries/month).

Can't reach the Tailscale URL

  1. Make sure Tailscale is connected on your Mac (check menu bar icon)
  2. Make sure HTTPS certificates are enabled in Tailscale admin (DNS settings)
  3. Verify the EC2 instance has Tailscale running: SSH in and run tailscale status

Instance sizing

InstanceRAMMonthly CostNotes
t3.micro1 GB~$8❌ Installation fails — not enough memory
t3.small2 GB~$16⚠️ Marginal — may work but tight
t3.medium4 GB~$33✅ Recommended — works reliably
t3.large8 GB~$67✅ Good for heavy browser automation

What's Next

Once your assistant is running, you can:

  • Personalize it — Give it a name, set preferences, configure its personality via the Web UI
  • Install skills — Browse available skills with openclaw skills search on the EC2
  • Add more tools — MCP servers for GitHub, Notion, Google Drive, and more
  • Connect messaging — Add WhatsApp, Telegram, Discord, or Signal channels
  • Try different models — Switch between Claude Opus 4.5 and Sonnet 4.5 in the config

For more, see the official docs at https://docs.openclaw.ai/

Content is user-generated and unverified.
    Deploy OpenClaw on AWS EC2: Complete Setup Guide | Claude