Skip to content

About

Agentic-first self-hosted PaaS — Docker + gVisor + Traefik + Nixpacks + SQLite, operated by AI agents

Resources

Stars

32 stars

Watchers

1 watching

Forks

Repository files navigation

agentic-hosting

License: MIT

Register an agent, deploy from git, tail logs, and see it running

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.

  • Repo: github.com/dennisonbertram/agentic-hosting
  • Docs: Documentation site (source in docs-site/)
  • Stack: Go binary + Docker + gVisor + Traefik + Nixpacks + SQLite
  • Language: Go 1.25 (requires CGO for SQLite)
  • Binary: ah
  • Default port: 8080 (loopback only, behind Traefik)
  • License: MIT

Quick start

You have Run Runbook
A fresh Ubuntu 24.04 server (amd64/arm64) sudo scripts/setup-linux.sh --domain example.com --acme-email ops@example.com 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.

Give this to your agent

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.

Design Principles

  1. Agentic-first — AI agents use the REST API directly. The dashboard is optional and supervisory, not the primary operating surface.
  2. One happy path per primitive — One database engine per type (Postgres, Redis), one build system (Nixpacks), one router (Traefik).
  3. Multi-tenant — Full tenant isolation via API keys, namespaced resources, per-tenant rate limits.
  4. Secure by default — gVisor syscall interception, ReadonlyRootfs, dropped capabilities, AES-256-GCM encrypted secrets.

Architecture

                    Internet
                       │
                    Traefik (80/443)
                       │
        ┌──────────────┼──────────────┐
        │              │              │
   ah API         Service A      Service B
   (127.0.0.1:8080)  (gVisor)       (gVisor)
        │
   SQLite DBs
   /var/lib/ah/
  • ah — control plane (single Go binary, systemd service)
  • Traefik — reverse proxy and TLS. Service routes are written as file-provider config (traefik.enable=false on containers so image labels cannot hijack routing)
  • gVisor (runsc) — container runtime, syscall interception for tenant isolation
  • Nixpacks — zero-config source-to-image builds
  • SQLite — two databases: ah.db (state) and ah-metering.db (metering), WAL mode

Server Requirements

  • Linux (Ubuntu 22.04+ recommended)
  • Docker 29+ with gVisor runtime configured
  • Go 1.25+ with CGO enabled
  • Nixpacks installed
  • Traefik running as Docker container
  • AH_BOOTSTRAP_TOKEN environment variable (min 32 chars; authorizes tenant registration)
  • AH_ADMIN_TOKEN environment variable (min 32 chars per token; authorizes recover, reactivate and quota update)
  • /var/lib/ah/master.key — hex-encoded 32-byte key

Environment 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 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

Installation

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.

Run the OpenRouter example on the Mac mini

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.

1. Install dependencies

# 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

2. Start infrastructure

# 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

3. Build and install ah

# 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

4. Configure and start systemd service

# 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"}

API Reference

Authentication

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).

Registration

# 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": "admin@example.com"}'

# Response
{
  "tenant_id": "ten_abc123",
  "api_key": "keyid.secret"
}

Key recovery

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": "admin@example.com"}'

# Response
{
  "id": "<key-id>",
  "key": "keyid.secret",
  "name": "...",
  "created_at": <unix-seconds>
}

Health

GET /v1/system/health           # Public — {"status":"ok"}
GET /v1/system/health/detailed  # Auth required — disk, docker, gVisor status

Tenant Management

GET    /v1/tenant   # Get tenant info
PATCH  /v1/tenant   # Update name: {"name": "new-name"}
DELETE /v1/tenant   # Soft-delete (suspends tenant, stops all services)

API Keys

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

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

Builds (Nixpacks)

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

Databases

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.

Operational Details

File Layout

/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

Background Processes

  • Reconciler (30s interval): Syncs DB state to Docker reality. Detects crashes, circuit breakers.
  • GC (5min interval): Cleans orphaned containers, volumes, images (including public images ah pulled for image-sourced sandboxes once nothing uses them), build directories.
  • Circuit breaker: 5 crashes in 10 minutes stops the container and removes it. Reset with POST /v1/services/{id}/reset, then POST .../redeploy (/start returns 409 because there is no container).
  • Disk watermarks: Warns at 80% usage, blocks new deploys at 90%. Build, database create, kanban create, sandbox create, deploy and restart/redeploy return 503 {"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 resource defaults: New tenants get 2048 MB / 2 CPUs from 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.

Backups

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

Security Model

  • gVisor: All tenant containers run with --runtime=runsc (syscall interception)
  • ReadonlyRootfs: Container root filesystem is read-only; tmpfs mounts for /tmp, /var/run, /var/tmp, /run
  • CapDrop=ALL: No Linux capabilities
  • no-new-privileges: Containers cannot escalate privileges
  • Encrypted secrets: Env vars and database credentials encrypted with AES-256-GCM using the server master key. There is no per-tenant key separation yet.
  • API keys: HMAC-SHA256 of keyID.secret using the master key (constant-time compare). bcrypt is used for passwords, not API keys.
  • Rate limiting: Per-tenant (default 100 req/s burst 200, from tenant_quotas.api_rate_limit) + global (500 req/s burst 1000)
  • Idempotency: 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.

Networking

  • Without --base-domain: http://{serviceID}.localhost
  • With --base-domain example.com: https://{dns_label}.example.com
  • Traefik file-provider config is written by ah; containers set traefik.enable=false
  • ah API is loopback-only by default (Traefik reverse proxies it)

Development

# 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

Claude Code Integration

agentic-hosting ships with a Claude Code skill, slash commands, and bash scripts for operating the API without writing curl commands manually.

Install the Skill

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.

Slash Commands

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

Bash Scripts

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 me@example.com

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.

Comparisons

Documentation site

The 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 .

Planned Work

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.

License

This project is licensed under the MIT License. Copyright © 2026 Dennison Bertram.

About

Agentic-first self-hosted PaaS — Docker + gVisor + Traefik + Nixpacks + SQLite, operated by AI agents

Resources

Stars

32 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages