AI Decision System
Replayable, offline-verifiable records for AI-assisted go/no-go decisions: experiment promotions, typed model answers and human-approved actions.
Open-source hosting infrastructure built for AI agents. 100% JSON REST API, gVisor sandboxing, circuit breakers. Runs on your server.
Developer toolsGo · ShellGitHub @dennisonbertramOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Agentic-first self-hosted PaaS — Docker + gVisor + Traefik + Nixpacks + SQLite, operated by AI agents
Translation pending
Taken from the repository README
Loading the full README.
Read the full README ↗Real ah --dev loop (not a mock): register a tenant, Nixpacks-build from git, tail logs, service running. Source recording: docs/assets/demo.cast.
A self-hosted PaaS for bare metal servers, designed to be operated by AI agents via REST API. Includes an optional supervisory dashboard for humans.
docs-site/)ahGet an Ubuntu 24.04 server (Hetzner is one option). Open ports 80 and 443. Point api and *.apps at that server.
git clone https://github.com/dennisonbertram/agentic-hosting && cd agentic-hosting
sudo scripts/setup-linux.sh --domain example.com --acme-email [email protected] -y
export AH_URL=https://api.example.com
export AH_BOOTSTRAP_TOKEN="$(sudo sed -n 's/^AH_BOOTSTRAP_TOKEN=//p' /etc/default/ah)"
ahc register my-team [email protected] --save # api_key is shown once
ahc deploy https://github.com/org/repo my-app
# prints URL: https://my-app.apps.example.com
ahc wraps POST /v1/tenants/register (name, email → tenant_id, api_key), POST /v1/services with image: "pending" (route port defaults to 8000; pass --port if the app listens elsewhere), then POST /v1/services/{id}/builds with source_type / source_url (source_ref defaults to main) which returns build_id, not id. Need a database? ahc db provision my-app postgres sets DATABASE_URL from GET /v1/databases/{id}/connection-string (host ah-db-<id>, port 5432 / 6379) and restarts so the service joins that network.
Give this to your agent: llms.txt · operator skill.
| You have | Run | Runbook |
|---|---|---|
| A fresh Ubuntu 24.04 server (amd64/arm64) | sudo scripts/setup-linux.sh --domain example.com --acme-email [email protected] | setup-linux |
| An existing Linux install to upgrade or repair | sudo scripts/setup-linux.sh | operations |
| A Mac for local development | scripts/setup-macos.sh then make sandbox | setup-macos |
| Any machine with Docker, for a throwaway instance | make sandbox / make sandbox-down | local-sandbox |
Every setup script accepts --dry-run (check prerequisites, print the plan, change nothing) and --help. The macOS setup and the sandbox use the runc runtime when gVisor is unavailable; that is a dev-only fallback with no syscall sandbox, ah refuses it without --dev, and it logs a warning banner at startup. Production always runs workloads under gVisor (runsc). Day-2 tasks (upgrade, backup and restore, master key rotation, logs, troubleshooting) are in the operations runbook.
One shareable link is enough for an agent to install the control plane, register a tenant, deploy from git, add Postgres/Redis, and read logs.
| What | URL (after merge to main) |
|---|---|
| llms.txt (llmstxt.org index) | https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/llms.txt |
| llms-full.txt (all docs) | https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/llms-full.txt |
| Operator skill | https://raw.githubusercontent.com/dennisonbertram/agentic-hosting/main/skills/agentic-hosting/SKILL.md |
Docs-site copies: /llms.txt and /llms-full.txt (GitHub Pages: https://agentic.hosting/docs/llms.txt). The Claude Code skill and /ah-* slash commands in .claude/ are the same API, split for interactive use. Details: Give this to your agent.
{dns_label}.{base-domain}) are first-come across the whole server, not per tenant. Rate limits are per-tenant and global. Internet
│
Traefik (80/443)
│
┌──────────────┼──────────────┐
│ │ │
ah API Service A Service B
(127.0.0.1:8080) (gVisor) (gVisor)
│
SQLite DBs
/var/lib/ah/
traefik.enable=false on containers so image labels cannot hijack routing)ah.db (state) and ah-metering.db (metering), WAL modeAH_BOOTSTRAP_TOKEN environment variable (min 32 chars; authorizes tenant registration)AH_ADMIN_TOKEN environment variable (min 32 chars per token; authorizes recover, reactivate, suspend and quota update)/var/lib/ah/master.key — hex-encoded 32-byte keyEnvironment variables read by ah:
| Variable | Purpose |
|---|---|
AH_BOOTSTRAP_TOKEN | Tenant registration token (min 32 chars, comma-separated for rotation) |
AH_ADMIN_TOKEN | Operator token for POST /v1/auth/recover, POST /v1/tenants/{id}/reactivate, POST /v1/tenants/{id}/suspend and PATCH /v1/tenants/{id}/quotas (min 32 chars per token, comma-separated for rotation). If unset, bootstrap tokens are accepted for those routes with a deprecation warning |
AH_METER_DB_PATH | Metering database path. Defaults to ah-metering.db next to the state database |
AH_BASE_DOMAIN | Base domain for service URLs (https://{name}.{base-domain}) |
AH_EXTERNAL_TLS_TERMINATED | true when TLS is terminated in front of Traefik |
AH_CORS_ORIGINS | Comma-separated browser Origin allowlist for the dashboard. Default: https://agentic.hosting, https://www.agentic.hosting, https://dennisonbertram.github.io. * is ignored |
AH_CONTAINER_RUNTIME | OCI runtime for tenant workloads: runsc (default, gVisor) or runc. runc is a dev-only fallback for hosts without gVisor (macOS); ah refuses it unless started with --dev |
AH_WARM_POOL_ENABLED | false disables the pre-started environment containers (default true). The local sandbox turns it off so it does not pull every template image |
AH_GC_PRUNE_DOCKER | false stops GC from running daemon-wide dangling-image and unused anonymous-volume prunes (default true). Set it when the Docker daemon is shared with unrelated work |
For a Mac mini demo, use the Mac mini first-run guide for the isolated Lima VM, local health check, ngrok setup, and first deployment. It runs the same Go application with Docker and gVisor inside Ubuntu; the existing Hetzner/server install remains separate in the fresh-install guide.
The quickest path is scripts/setup-linux.sh (see Quick start); on a fresh host it runs scripts/bootstrap-stack.sh and then installs ahc, and on an existing host it upgrades in place. For a fresh Ubuntu 24.04 amd64 host, scripts/bootstrap-stack.sh installs and configures everything below in one run, including generating the master key, bootstrap token and admin token (see the fresh-install guide). The manual steps that follow are for reference or customized setups.
The OpenRouter agent guide records one successful local ARM64 hosted-model request (HTTP 200, calculator result 42) and the manual app/relay setup. It is not a one-command installer; the relay invocation was reconstructed from inspected settings and not replayed verbatim on a fresh VM. The temporary QA VM was deleted, and ngrok, public DNS, and TLS were not tested.
# Docker
curl -fsSL https://get.docker.com | sh
# gVisor
curl -fsSL https://gvisor.dev/archive.key | sudo gpg --dearmor -o /usr/share/keyrings/gvisor-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/gvisor-archive-keyring.gpg] https://storage.googleapis.com/gvisor/releases release main" | sudo tee /etc/apt/sources.list.d/gvisor.list
sudo apt-get update && sudo apt-get install -y runsc
# Configure Docker to use gVisor
cat > /etc/docker/daemon.json <<'EOF'
{
"runtimes": {
"runsc": {
"path": "/usr/bin/runsc"
}
},
"default-address-pools": [{"base":"172.20.0.0/14","size":24}]
}
EOF
systemctl restart docker
# Go
wget -q https://go.dev/dl/go1.25.0.linux-amd64.tar.gz -O /tmp/go.tar.gz
tar -C /usr/local -xzf /tmp/go.tar.gz
echo 'export PATH=$PATH:/usr/local/go/bin' >> /etc/profile
# Nixpacks
curl -sSL https://nixpacks.com/install.sh | bash
# Create Docker network
docker network create --driver bridge traefik-public
# Start Traefik
docker run -d --name paas-traefik --restart unless-stopped \
--network traefik-public \
-p 80:80 -p 443:443 -p 8090:8080 \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-v /etc/traefik:/etc/traefik \
traefik:latest \
--api.dashboard=true \
--providers.docker=true \
--providers.docker.network=traefik-public \
--providers.file.filename=/etc/traefik/dynamic/ah.yml \
--entrypoints.web.address=:80 \
--entrypoints.websecure.address=:443
# Start local registry
docker run -d --name paas-registry --restart unless-stopped \
-p 127.0.0.1:5000:5000 \
-v /var/lib/ah/registry:/var/lib/registry \
registry:2
# Clone repo
git clone https://github.com/dennisonbertram/agentic-hosting /agentic-hosting
cd /agentic-hosting
# Build (CGO required for SQLite)
CGO_ENABLED=1 go build -o bin/ah ./cmd/ah
cp bin/ah /usr/local/bin/ah
# Create data directory
mkdir -p /var/lib/ah/builds /var/lib/ah/backups
# Generate master key (hex-encoded 32 bytes)
head -c 32 /dev/urandom | xxd -p -c 64 > /var/lib/ah/master.key
chmod 600 /var/lib/ah/master.key
# Generate the bootstrap token and the admin token (run twice; use a different value for each)
openssl rand -hex 32
openssl rand -hex 32
# Set bootstrap and admin tokens
cat > /etc/default/ah <<'EOF'
AH_BOOTSTRAP_TOKEN=<your-32+-char-token-here>
AH_ADMIN_TOKEN=<a-different-32+-char-token-here>
EOF
chmod 600 /etc/default/ah
# Install service
cp deploy/ah.service /etc/systemd/system/ah.service
systemctl daemon-reload
systemctl enable ah
systemctl start ah
# Verify from the host (the API only trusts forwarded HTTPS headers from loopback)
curl -H 'X-Forwarded-Proto: https' http://127.0.0.1:8080/v1/system/health
# → {"status":"ok"}
Tenant routes require a Bearer key. Public (no API key): GET /v1/system/health, POST /v1/tenants/register, POST /v1/auth/recover, POST /v1/tenants/{tenantID}/reactivate, POST /v1/system/bootstrap-token/validate, PATCH /v1/tenants/{tenantID}/quotas. Register sends X-Bootstrap-Token. Recover, reactivate, and quota patch send X-Admin-Token when AH_ADMIN_TOKEN is set (bootstrap tokens then get 403 on those routes); if unset, X-Bootstrap-Token still works (deprecated). Bootstrap validate takes {"token":"…"} in the JSON body and always returns 200 {"valid":true|false}.
Authorization: Bearer <api_key>
API key format: keyID.secret (returned on registration and key creation).
# Register a tenant (requires bootstrap token)
curl -X POST http://localhost:8080/v1/tenants/register \
-H 'Content-Type: application/json' \
-H 'X-Bootstrap-Token: <bootstrap-token>' \
-d '{"name": "my-tenant", "email": "[email protected]"}'
# Response
{
"tenant_id": "ten_abc123",
"api_key": "keyid.secret"
}
Recovery needs the admin token, not the bootstrap token. The response contains a new API key in the key field, shown once:
curl -X POST http://localhost:8080/v1/auth/recover \
-H 'Content-Type: application/json' \
-H 'X-Admin-Token: <admin-token>' \
-d '{"email": "[email protected]"}'
# Response
{
"id": "<key-id>",
"key": "keyid.secret",
"name": "...",
"created_at": <unix-seconds>
}
GET /v1/system/health # Public — {"status":"ok"}
GET /v1/system/health/detailed # Auth required — disk, docker, gVisor status
GET /v1/tenant # Get tenant info
PATCH /v1/tenant # Update name: {"name": "new-name"}
DELETE /v1/tenant # Soft-delete (suspends tenant, stops all services)
POST /v1/auth/keys # Create key: {"name":"ci-key","expires_in":2592000}
GET /v1/auth/keys # List keys (secrets not shown)
DELETE /v1/auth/keys/{keyID} # Revoke key
Services are Docker containers run with gVisor isolation.
Images may come from Docker Hub or the local loopback registry on 127.0.0.1:5000 / localhost:5000.
# Create and deploy a service
POST /v1/services
{
"name": "my-app",
"image": "127.0.0.1:5000/tenant/my-app:latest",
"port": 3000,
"env": {"NODE_ENV": "production"}
}
# Returns immediately with status "deploying"
GET /v1/services # List services
GET /v1/services/{serviceID} # Get service (includes status, URL)
GET /v1/services/{serviceID}/logs?follow=true&tail=100 # Stream runtime logs
DELETE /v1/services/{serviceID} # Delete service and container
POST /v1/services/{serviceID}/start # Start
POST /v1/services/{serviceID}/stop # Stop
POST /v1/services/{serviceID}/restart # Restart
POST /v1/services/{serviceID}/reset # Reset circuit breaker
GET /v1/services/{serviceID}/env # List env vars (masked by default)
GET /v1/services/{serviceID}/env?reveal=true # Show values
POST /v1/services/{serviceID}/env # Set vars: {"KEY": "value"}
DELETE /v1/services/{serviceID}/env/{key} # Delete one var
Build from a git repository. Supports GitHub, GitLab, Bitbucket, sr.ht, Codeberg (HTTPS only).
# Start a build
POST /v1/services/{serviceID}/builds
{
"source_type": "git",
"source_url": "https://github.com/org/repo",
"source_ref": "main"
}
# source_ref defaults to "main" if omitted
# Returns: {"build_id": "...", "status": "pending", "image": "127.0.0.1:5000/..."}
GET /v1/services/{serviceID}/builds # List builds
GET /v1/services/{serviceID}/builds/{buildID} # Get build status
GET /v1/services/{serviceID}/builds/{buildID}/logs?follow=true # Stream logs
DELETE /v1/services/{serviceID}/builds/{buildID} # Cancel build
Managed Postgres and Redis containers with encrypted connection strings.
# Create database (takes ~10-30s)
POST /v1/databases
{
"name": "my-db",
"type": "postgres" # or "redis"
}
GET /v1/databases # List databases
GET /v1/databases/{dbID} # Get database
GET /v1/databases/{dbID}/connection-string # Get connection string (decrypted)
DELETE /v1/databases/{dbID} # Delete database and data (not limited to 30s; stops the container, wipes and removes the volume)
Connection strings use the DNS alias ah-db-<databaseID> and the engine port (5432 / 6379). That name resolves only inside containers attached to ah-dbnet-<tenant>-<db> (Internal=true, ICC=true). Set DATABASE_URL / REDIS_URL from GET /v1/databases/{id}/connection-string, then restart so the service joins that network. Host-loopback publish remains for control-plane health probes only — do not curl the URL from the host. Other tenants are never attached.
/var/lib/ah/
├── ah.db # State database (tenants, services, builds, databases)
├── ah-metering.db # Metering database
├── master.key # AES-256 master key (hex-encoded, chmod 600)
├── builds/ # Nixpacks build workdirs (GC after build)
├── backups/ # SQLite backups
└── registry/ # Docker registry data
POST /v1/services/{id}/reset, then POST .../redeploy (/start returns 409 because there is no container).{"error":"insufficient disk space; try again later"} with Retry-After: 10; service create returns 201 then fails async. A full deploy queue is also 503 with Retry-After.tenant_quotas. Fallback 512 MB / 1 CPU only if the quota row cannot be loaded. Limits are per-tenant and still bounded by the host.ah backup
# Creates timestamped gzip backups in /var/lib/ah/backups/
# Keeps the newest 10 state and 10 metering backups
# Uses VACUUM INTO for WAL-safe snapshots
--runtime=runsc (syscall interception)/tmp, /var/run, /var/tmp, /runkeyID.secret using the master key (constant-time compare). bcrypt is used for passwords, not API keys.tenant_quotas.api_rate_limit) + global (500 req/s burst 1000)Idempotency-Key applies to POST and PUT only (not DELETE/PATCH; no PUT routes exist). Scoped tenant + method + path + body hash. TTL is 10 minutes. POST /v1/auth/keys and POST /v1/environments/{id}/exec are excluded.--base-domain: http://{serviceID}.localhost--base-domain example.com: https://{dns_label}.example.comah; containers set traefik.enable=false# Build
make build
# Run locally (requires Docker + gVisor)
AH_BOOTSTRAP_TOKEN=dev-bootstrap-token-32-chars-minimum AH_ADMIN_TOKEN=dev-admin-token-32-chars-minimum make run
# Run in dev mode (no HTTPS enforcement, open registration)
./bin/ah --dev --open-registration --port 8080
# Throwaway local instance with a demo tenant and service (macOS or Linux)
make sandbox
make sandbox-down
# Dry-run checks for the setup scripts (no sudo, nothing installed)
make test-scripts
# Backup databases
./bin/ah backup
agentic-hosting ships with a Claude Code skill, slash commands, and bash scripts for operating the API without writing curl commands manually.
Copy the skill to your Claude Code skills directory:
curl -fsSL https://agentic.hosting/install-skill.sh | bash
# or copy from the repo:
mkdir -p ~/.claude/skills/agentic-hosting
cp .claude/skills/agentic-hosting/SKILL.md ~/.claude/skills/agentic-hosting/SKILL.md
cp .claude/commands/ah-*.md ~/.claude/commands/
Claude will automatically load the skill and know how to operate agentic-hosting: authentication, deployment flows, database provisioning, error handling, and all API limits.
Files in .claude/commands/ are named ah-*.md. There are no /deploy /status /db /logs command files.
| Command | Description |
|---|---|
/ah-deploy <git-url-or-image> <name> [port] | Deploy from a git URL (Nixpacks build) or Docker image |
/ah-status | Full dashboard — disk, services, databases, circuit breakers |
/ah-db <service> <postgres|redis> [name] | Provision a database and wire it to a service |
/ah-logs <service> [build-id] | Stream runtime logs, or build logs when a build-id is given |
/ah-register <name> <email> | Register a tenant (needs bootstrap token) |
/ah-snapshot <service> [name] | Snapshot a service for forking |
Example usage in Claude Code:
/ah-deploy https://github.com/org/my-app web 3000
/ah-status
/ah-db web postgres
/ah-logs web
Standalone scripts that work without Claude:
# Set credentials
export AH_URL="https://<your-domain>"
export AH_KEY="keyid.secret"
# Register a new tenant (one-time)
AH_BOOTSTRAP_TOKEN=<token> ./scripts/register.sh my-tenant [email protected]
For deploys, status, build logs and databases, use the ahc CLI:
ahc deploy https://github.com/org/repo my-app
ahc deploy nginxinc/nginx-unprivileged my-site --port 8080
ahc status
ahc db provision my-app postgres
ahc build logs my-app
See scripts/README.md for full documentation.
--dev single-tenant box; blocked for stock production install and unattended signupThe public docs match the agentic.hosting landing page (Instrument Serif + Inter, stone palette) and are generated from docs-site/ (Astro). They describe only what the Go API actually implements — see internal/api/server.go. Stale-vs-correct claims from the alignment pass are listed in docs-site/CORRECTIONS.md.
cd docs-site
npm ci
npm run dev # http://127.0.0.1:4321/
npm run build # GitHub Pages + agentic.hosting (base /docs/)
GitHub Pages deploys on push to main via .github/workflows/docs.yml. The combined artifact serves the homepage from website/ at https://agentic.hosting/ and the Astro docs at https://agentic.hosting/docs/. Custom domain and DNS steps: docs/website/github-pages.md. The static dashboard at /dashboard/ defaults its API base to https://api.agentic.hosting on that host (?api= overrides). The API reflects Access-Control-Allow-Origin for that allowlist (AH_CORS_ORIGINS / --cors-origins); it never uses * and does not set credentials.
The Hetzner website container still builds from the repository root (used for www.agentic.hosting, which 301s to the GitHub repo):
docker build -f website/Dockerfile -t website .
See GitHub Issues and the docs roadmap. Known gaps include HKDF key separation, billing (metering is query-only), Prometheus export, and multi-node SQLite. Audit log lines already exist for sensitive reads.
This project is licensed under the MIT License. Copyright © 2026 Dennison Bertram.