Skip to content

Self-Hosting Guide

This guide walks you through deploying Simple Agent Manager to your own infrastructure. Deployment is automated via GitHub Actions + Pulumi — push to main and everything is provisioned.

RequirementPurposeTier
Cloudflare accountAPI hosting, DNS, storageFree tier
GitHub accountAuthentication, CI/CDFree tier
Domain on CloudflareWorkspace URLsAny registrar

You do not need a shared Hetzner account. Users provide their own Hetzner tokens through the Settings UI.

Fork simple-agent-manager on GitHub.

In Cloudflare Dashboard → My Profile → API Tokens → Create Custom Token:

Permission TypeResourceAccess
AccountCloudflare Workers: D1Edit
AccountWorkers KV StorageEdit
AccountWorkers R2 StorageEdit
AccountWorkers ScriptsEdit
AccountWorkers ObservabilityRead
AccountCloudflare PagesEdit
ZoneDNSEdit
ZoneWorkers RoutesEdit
ZoneZoneRead

Set Zone Resources to your specific domain and Account Resources to your account.

Go to GitHub App Settings → New GitHub App:

Basic settings:

  • Homepage URL: https://app.yourdomain.com
  • Callback URL: https://api.yourdomain.com/api/auth/callback/github
  • Setup URL: https://api.yourdomain.com/api/github/callback

Permissions:

  • Repository → Contents: Read and write
  • Repository → Metadata: Read-only
  • Account → Email addresses: Read-only

Webhook:

  • URL: https://api.yourdomain.com/api/github/webhook
  • Active: checked

After creation, note the App ID and Client ID, generate a Client Secret and Private Key.

Separate from the main API token, this is for Pulumi state storage:

  1. Cloudflare Dashboard → R2 → Manage R2 API Tokens
  2. Create token with Object Read & Write permissions
  3. Note the Access Key ID and Secret Access Key
Terminal window
openssl rand -base64 32

Save this passphrase — you’ll need it for all future deployments.

In your fork: Settings → Environments → New environment → name it production.

Environment variables:

VariableDescriptionExample
BASE_DOMAINYour domainexample.com
RESOURCE_PREFIXCloudflare resource prefix (optional)sam

Environment secrets:

SecretDescription
CF_API_TOKENCloudflare API token
CF_ACCOUNT_IDCloudflare account ID (32-char hex)
CF_ZONE_IDDomain zone ID (32-char hex)
R2_ACCESS_KEY_IDR2 API token access key
R2_SECRET_ACCESS_KEYR2 API token secret key
PULUMI_CONFIG_PASSPHRASEGenerated passphrase
GH_CLIENT_IDGitHub App client ID
GH_CLIENT_SECRETGitHub App client secret
GH_APP_IDGitHub App ID
GH_APP_PRIVATE_KEYGitHub App private key (PEM or base64)
GH_APP_SLUGGitHub App URL slug

Push any commit to main, or go to Actions → Deploy → Run workflow.

The workflow:

  1. Validates configuration
  2. Provisions infrastructure via Pulumi (D1, KV, R2, DNS)
  3. Deploys API Worker and Web UI
  4. Runs database migrations
  5. Builds and uploads VM Agent binaries
  6. Runs health check

After deployment completes:

Terminal window
# API health check
curl https://api.yourdomain.com/api/health
# Should return: {"status":"ok"}

Open https://app.yourdomain.com — you should see the login page.

To remove all resources: Actions → Teardown → Run workflow → type DELETE to confirm.

ComponentFree TierPaid Overage
Cloudflare Workers100K req/day$0.15/million
Cloudflare D15M rows read/day$0.001/million
Cloudflare KV100K reads/day$0.50/million
Cloudflare R210GB storage$0.015/GB/month
Cloudflare PagesUnlimitedFree

A typical SAM deployment stays within the free tier for small to medium usage.

VMs are billed to each user’s Hetzner account:

SizeSpecsHourlyMonthly
Small (CX22)2 vCPU, 4GB RAM~$0.007~$4.15
Medium (CX32)4 vCPU, 8GB RAM~$0.012~$7.50
Large (CX42)8 vCPU, 16GB RAM~$0.030~$18

Your PULUMI_CONFIG_PASSPHRASE doesn’t match the one used when state was created. Use the original passphrase or delete the stack in R2 and start fresh.

Check that your GitHub App’s Callback URL matches exactly: https://api.yourdomain.com/api/auth/callback/github

Migrations haven’t been applied. The deploy workflow runs them automatically, but you can also run manually:

Terminal window
wrangler d1 migrations apply workspaces --remote

Check Hetzner console for VM status. If the VM is running, SSH in and check systemctl status vm-agent.

See the full troubleshooting section in the repository for more scenarios.