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.
Prerequisites
Section titled “Prerequisites”| Requirement | Purpose | Tier |
|---|---|---|
| Cloudflare account | API hosting, DNS, storage | Free tier |
| GitHub account | Authentication, CI/CD | Free tier |
| Domain on Cloudflare | Workspace URLs | Any registrar |
You do not need a shared Hetzner account. Users provide their own Hetzner tokens through the Settings UI.
Step 1: Fork the Repository
Section titled “Step 1: Fork the Repository”Fork simple-agent-manager on GitHub.
Step 2: Create Cloudflare API Token
Section titled “Step 2: Create Cloudflare API Token”In Cloudflare Dashboard → My Profile → API Tokens → Create Custom Token:
| Permission Type | Resource | Access |
|---|---|---|
| Account | Cloudflare Workers: D1 | Edit |
| Account | Workers KV Storage | Edit |
| Account | Workers R2 Storage | Edit |
| Account | Workers Scripts | Edit |
| Account | Workers Observability | Read |
| Account | Cloudflare Pages | Edit |
| Zone | DNS | Edit |
| Zone | Workers Routes | Edit |
| Zone | Zone | Read |
Set Zone Resources to your specific domain and Account Resources to your account.
Step 3: Create GitHub App
Section titled “Step 3: Create GitHub App”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.
Step 4: Create R2 API Token
Section titled “Step 4: Create R2 API Token”Separate from the main API token, this is for Pulumi state storage:
- Cloudflare Dashboard → R2 → Manage R2 API Tokens
- Create token with Object Read & Write permissions
- Note the Access Key ID and Secret Access Key
Step 5: Generate Pulumi Passphrase
Section titled “Step 5: Generate Pulumi Passphrase”openssl rand -base64 32Save this passphrase — you’ll need it for all future deployments.
Step 6: Configure GitHub Environment
Section titled “Step 6: Configure GitHub Environment”In your fork: Settings → Environments → New environment → name it production.
Environment variables:
| Variable | Description | Example |
|---|---|---|
BASE_DOMAIN | Your domain | example.com |
RESOURCE_PREFIX | Cloudflare resource prefix (optional) | sam |
Environment secrets:
| Secret | Description |
|---|---|
CF_API_TOKEN | Cloudflare API token |
CF_ACCOUNT_ID | Cloudflare account ID (32-char hex) |
CF_ZONE_ID | Domain zone ID (32-char hex) |
R2_ACCESS_KEY_ID | R2 API token access key |
R2_SECRET_ACCESS_KEY | R2 API token secret key |
PULUMI_CONFIG_PASSPHRASE | Generated passphrase |
GH_CLIENT_ID | GitHub App client ID |
GH_CLIENT_SECRET | GitHub App client secret |
GH_APP_ID | GitHub App ID |
GH_APP_PRIVATE_KEY | GitHub App private key (PEM or base64) |
GH_APP_SLUG | GitHub App URL slug |
Step 7: Deploy
Section titled “Step 7: Deploy”Push any commit to main, or go to Actions → Deploy → Run workflow.
The workflow:
- Validates configuration
- Provisions infrastructure via Pulumi (D1, KV, R2, DNS)
- Deploys API Worker and Web UI
- Runs database migrations
- Builds and uploads VM Agent binaries
- Runs health check
Verification
Section titled “Verification”After deployment completes:
# API health checkcurl https://api.yourdomain.com/api/health# Should return: {"status":"ok"}Open https://app.yourdomain.com — you should see the login page.
Teardown
Section titled “Teardown”To remove all resources: Actions → Teardown → Run workflow → type DELETE to confirm.
Cost Estimation
Section titled “Cost Estimation”Platform Costs
Section titled “Platform Costs”| Component | Free Tier | Paid Overage |
|---|---|---|
| Cloudflare Workers | 100K req/day | $0.15/million |
| Cloudflare D1 | 5M rows read/day | $0.001/million |
| Cloudflare KV | 100K reads/day | $0.50/million |
| Cloudflare R2 | 10GB storage | $0.015/GB/month |
| Cloudflare Pages | Unlimited | Free |
A typical SAM deployment stays within the free tier for small to medium usage.
User VM Costs
Section titled “User VM Costs”VMs are billed to each user’s Hetzner account:
| Size | Specs | Hourly | Monthly |
|---|---|---|---|
| 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 |
Troubleshooting
Section titled “Troubleshooting””error: failed to decrypt state”
Section titled “”error: failed to decrypt state””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.
”OAuth callback failed”
Section titled “”OAuth callback failed””Check that your GitHub App’s Callback URL matches exactly: https://api.yourdomain.com/api/auth/callback/github
”D1_ERROR: no such table”
Section titled “”D1_ERROR: no such table””Migrations haven’t been applied. The deploy workflow runs them automatically, but you can also run manually:
wrangler d1 migrations apply workspaces --remote“Workspace stuck in provisioning”
Section titled ““Workspace stuck in provisioning””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.