| name | agentic-hosting |
| description | This skill should be used when the user asks to "deploy a service", "deploy an app", "add a domain", "provision a database", "check service status", "view logs", "restart a service", "set environment variables", "reset circuit breaker", "register a tenant", "add a kanban board", "take a snapshot", "redeploy a service", "recover an API key", "check tenant usage", "view deployment history", "fork a service", or mentions operating an agentic-hosting instance. Also trigger when the user says "use agentic-hosting", "spin up on my PaaS", or "deploy to my server". |
agentic-hosting Operator Skill
agentic-hosting (ah) is an agentic-first self-hosted PaaS (v0.4.0). Operate it entirely via REST API — no GUI required. Every action is a curl command. The system builds and runs containerized apps using Nixpacks (from Git) or Docker images, with full gVisor sandbox isolation.
Prerequisites
Set these before issuing any commands:
export AH_URL="https://agentic.hosting"
export AH_KEY="keyid.secret"
Verify connectivity:
curl -s $AH_URL/v1/system/health
Don't have an API key? See Register a Tenant or Recover an API Key below.
Slash Commands
Six slash commands are available when the Claude Code skill is installed:
| Command | Purpose |
|---|
/ah-deploy <git-url-or-image> <name> [port] | Deploy a service from git URL or Docker image |
/ah-status | Full health dashboard — disk, services, databases, circuits |
/ah-db <service-name> <postgres|redis> [db-name] | Provision a database and wire it to a service |
/ah-logs <service-name> [build-id] | Stream build logs for a service |
/ah-register <tenant-name> <email> | Register a new tenant and get an API key |
/ah-snapshot <service-name> [snapshot-name] | Take a snapshot for instant environment forking |
Register a Tenant (one-time)
Requires the bootstrap token. On the server, read it with:
ssh root@<server> "grep AH_BOOTSTRAP_TOKEN /etc/default/ah"
Multiple bootstrap tokens are supported — set AH_BOOTSTRAP_TOKEN to a comma-separated list (e.g. token1,token2). Any valid token in the list is accepted.
To verify a bootstrap token without registering, use POST /v1/auth/bootstrap/validate with the X-Bootstrap-Token header — returns 200 if valid.
curl -s -X POST $AH_URL/v1/tenants/register \
-H "X-Bootstrap-Token: $AH_BOOTSTRAP_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "my-agent", "email": "agent@example.com"}'
Recover an API Key
Lost all API keys? Recover access using the bootstrap token and your tenant email:
curl -s -X POST $AH_URL/v1/auth/recover \
-H "X-Bootstrap-Token: $AH_BOOTSTRAP_TOKEN" \
-H "Content-Type: application/json" \
-d '{"email": "agent@example.com"}'
Deploy a Service
From a Docker image (fast path)
SVC=$(curl -s -X POST $AH_URL/v1/services \
-H "Authorization: Bearer $AH_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"my-app","image":"nginx:alpine","port":80,"memory_mb":256,"cpu_count":1}')
SERVICE_ID=$(echo $SVC | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')
echo "Service ID: $SERVICE_ID"
echo "URL: $(echo $SVC | python3 -c 'import sys,json; print(json.load(sys.stdin)["url"])')"
From a snapshot (instant fork)
SVC=$(curl -s -X POST "$AH_URL/v1/services?from_snapshot=$SNAPSHOT_ID" \
-H "Authorization: Bearer $AH_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"my-fork"}')
Build from Git (Nixpacks — zero config)
Supported: GitHub, GitLab, Bitbucket, sr.ht, Codeberg (HTTPS URLs only). Supports branch names and full SHA commit refs.
SVC=$(curl -s -X POST $AH_URL/v1/services \
-H "Authorization: Bearer $AH_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"my-app","port":3000}')
SERVICE_ID=$(echo $SVC | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')
BUILD=$(curl -s -X POST $AH_URL/v1/services/$SERVICE_ID/builds \
-H "Authorization: Bearer $AH_KEY" \
-H "Content-Type: application/json" \
-d '{"git_url":"https://github.com/org/repo","branch":"main"}')
BUILD_ID=$(echo $BUILD | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')
curl -sN -H "Authorization: Bearer $AH_KEY" \
"$AH_URL/v1/services/$SERVICE_ID/builds/$BUILD_ID/logs?follow=true"
Redeploy (rebuild from same git source)
curl -s -X POST $AH_URL/v1/services/$SERVICE_ID/redeploy \
-H "Authorization: Bearer $AH_KEY"
Poll until running
Large images can take up to 10 minutes. Always use a 10-minute timeout (120 × 5s):
for i in $(seq 1 120); do
STATUS=$(curl -s -H "Authorization: Bearer $AH_KEY" \
$AH_URL/v1/services/$SERVICE_ID \
| python3 -c 'import sys,json; print(json.load(sys.stdin)["status"])')
echo "[$i/120] $STATUS"
[ "$STATUS" = "running" ] && { echo "RUNNING"; break; }
[ "$STATUS" = "failed" ] && { echo "FAILED — check logs"; break; }
[ "$STATUS" = "circuit_open" ] && { echo "CIRCUIT OPEN — fix app, then POST .../reset"; break; }
sleep 5
done
Deployment History
curl -s -H "Authorization: Bearer $AH_KEY" \
$AH_URL/v1/services/$SERVICE_ID/deployments | python3 -m json.tool
Service URLs
The URL a service receives depends on whether the platform was started with --base-domain:
Without --base-domain (default):
http://{dns-label}.localhost
When running locally, Traefik file-provider routes are auto-generated for each service. Access via curl -H "Host: {dns-label}.localhost" http://localhost.
With --base-domain apps.example.com:
https://{dns-label}.apps.example.com
The dns_label is derived from the service name: lowercased, non-alphanumeric characters replaced with hyphens. Example: service named My App gets label my-app → URL https://my-app.apps.example.com.
Check which mode the platform is in:
Create any service and inspect the url field in the response. If it contains .localhost, the platform is in localhost mode. If it contains a real domain, subdomain mode is active.
Service name constraints when base-domain is set:
- Must produce a valid DNS label (max 63 chars after conversion)
- Reserved names are blocked:
api, admin, dashboard, traefik, www, auth, login, registry — using one returns 422
- DNS labels are globally unique across all tenants (first-come-first-served) — if another tenant has
blog.apps.example.com, your service named blog will be rejected with 422
The URL is stable. Once a service is deployed its URL does not change, even if the daemon restarts with a different --base-domain value. The dns_label is stored in the database at creation time.
See references/custom-domains.md for DNS wildcard setup and the full procedure for server operators.
Provision a Database
Database creation takes up to 30 seconds. Always use an idempotency key so retries are safe:
DB=$(curl -s -X POST $AH_URL/v1/databases \
-H "Authorization: Bearer $AH_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"name":"mydb","type":"postgres"}')
DB_ID=$(echo $DB | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')
CONN=$(curl -s -H "Authorization: Bearer $AH_KEY" \
$AH_URL/v1/databases/$DB_ID/connection-string \
| python3 -c 'import sys,json; print(json.load(sys.stdin)["connection_string"])')
curl -s -X POST $AH_URL/v1/services/$SERVICE_ID/env \
-H "Authorization: Bearer $AH_KEY" -H "Content-Type: application/json" \
-d "{\"DATABASE_URL\": \"$CONN\"}"
curl -s -X POST $AH_URL/v1/services/$SERVICE_ID/restart \
-H "Authorization: Bearer $AH_KEY"
Common Day-to-Day Operations
curl -s $AH_URL/v1/system/health
curl -s -H "Authorization: Bearer $AH_KEY" $AH_URL/v1/system/health/detailed | python3 -m json.tool
curl -s -H "Authorization: Bearer $AH_KEY" "$AH_URL/v1/system/health/detailed?fresh=true" | python3 -m json.tool
curl -s -H "Authorization: Bearer $AH_KEY" $AH_URL/v1/services | python3 -m json.tool
curl -s -H "Authorization: Bearer $AH_KEY" $AH_URL/v1/databases | python3 -m json.tool
curl -s -X POST $AH_URL/v1/services/$SERVICE_ID/stop -H "Authorization: Bearer $AH_KEY"
curl -s -X POST $AH_URL/v1/services/$SERVICE_ID/start -H "Authorization: Bearer $AH_KEY"
curl -s -X POST $AH_URL/v1/services/$SERVICE_ID/restart -H "Authorization: Bearer $AH_KEY"
curl -s -X DELETE $AH_URL/v1/services/$SERVICE_ID -H "Authorization: Bearer $AH_KEY"
curl -s -X PATCH $AH_URL/v1/services/$SERVICE_ID \
-H "Authorization: Bearer $AH_KEY" -H "Content-Type: application/json" \
-d '{"name":"new-name"}'
curl -s -X POST $AH_URL/v1/services/$SERVICE_ID/redeploy -H "Authorization: Bearer $AH_KEY"
curl -s -X POST $AH_URL/v1/services/$SERVICE_ID/reset -H "Authorization: Bearer $AH_KEY"
curl -s -H "Authorization: Bearer $AH_KEY" "$AH_URL/v1/services/$SERVICE_ID/env?reveal=true"
curl -s -X POST $AH_URL/v1/services/$SERVICE_ID/env \
-H "Authorization: Bearer $AH_KEY" -H "Content-Type: application/json" \
-d '{"KEY":"value","OTHER":"value2"}'
curl -s -X DELETE $AH_URL/v1/services/$SERVICE_ID/env/KEY -H "Authorization: Bearer $AH_KEY"
curl -s -X POST $AH_URL/v1/auth/keys \
-H "Authorization: Bearer $AH_KEY" -H "Content-Type: application/json" \
-d '{"name":"agent-key"}'
curl -s -H "Authorization: Bearer $AH_KEY" $AH_URL/v1/auth/keys | python3 -m json.tool
curl -s -X DELETE $AH_URL/v1/auth/keys/$KEY_ID -H "Authorization: Bearer $AH_KEY"
curl -s -H "Authorization: Bearer $AH_KEY" $AH_URL/v1/tenant | python3 -m json.tool
curl -s -H "Authorization: Bearer $AH_KEY" $AH_URL/v1/tenant/usage | python3 -m json.tool
curl -s -X PATCH $AH_URL/v1/tenant \
-H "Authorization: Bearer $AH_KEY" -H "Content-Type: application/json" \
-d '{"name":"new-name"}'
curl -s -H "Authorization: Bearer $AH_KEY" "$AH_URL/v1/activity?limit=50" | python3 -m json.tool
curl -s -X POST $AH_URL/v1/tenant/reactivate \
-H "X-Bootstrap-Token: $AH_BOOTSTRAP_TOKEN"
Snapshots — fork a service instantly
curl -s -X POST $AH_URL/v1/services/$SERVICE_ID/snapshots \
-H "Authorization: Bearer $AH_KEY" -H "Content-Type: application/json" \
-d '{"name":"before-migration"}' | python3 -m json.tool
curl -s -H "Authorization: Bearer $AH_KEY" $AH_URL/v1/snapshots | python3 -m json.tool
curl -s -X POST "$AH_URL/v1/services?from_snapshot=$SNAPSHOT_ID" \
-H "Authorization: Bearer $AH_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"forked-service"}'
curl -s -X DELETE $AH_URL/v1/snapshots/$SNAPSHOT_ID -H "Authorization: Bearer $AH_KEY"
Kanban — per-tenant Vikunja board
Provisioning is async — the POST returns immediately and the kanban URL becomes available once the container is ready. Poll GET /v1/kanban until the url field is populated.
curl -s -X POST $AH_URL/v1/kanban \
-H "Authorization: Bearer $AH_KEY" \
-H "Idempotency-Key: $(uuidgen)" | python3 -m json.tool
curl -s -H "Authorization: Bearer $AH_KEY" $AH_URL/v1/kanban | python3 -m json.tool
curl -s -H "Authorization: Bearer $AH_KEY" $AH_URL/v1/kanban/admin-token | python3 -m json.tool
curl -s -X DELETE $AH_URL/v1/kanban -H "Authorization: Bearer $AH_KEY"
Operations / Admin
Master key rotation: Rotate the encryption key used for secrets at rest:
ah rotate-key --new-key-file /path/to/new.key
Snapshot retention: Configure automatic cleanup of old snapshots via daemon flags:
--snapshot-max-per-service N — keep at most N snapshots per service (oldest pruned first)
--snapshot-max-age DURATION — delete snapshots older than the given duration (e.g. 720h for 30 days)
Kanban port range: Control the host port range allocated to kanban containers:
--kanban-port-start PORT and --kanban-port-end PORT
Quick Error Reference
| Error | Fix |
|---|
401 Unauthorized | Check key format: keyid.secret |
422 Unprocessable Entity | Name/email already exists, missing required field, reserved subdomain (api, admin, etc.), or DNS label too long |
429 Too Many Requests | Back off; respect Retry-After header |
503 Service Unavailable | Disk >90% or Docker down — check /v1/system/health/detailed |
Service stuck deploying >10 min | Server marks it failed at 10 min; check health, delete and retry |
circuit_open | Fix the app, then POST .../reset, then POST .../start |
Build failed | Stream build logs with ?follow=true for the error |
| All API keys lost | Use POST /v1/auth/recover with bootstrap token + tenant email |
For idempotency, limits, and advanced operations see references/operations.md.
Known Gaps
- No runtime log streaming (#11) — build logs work; container stdout/stderr via API not yet available
Additional Resources
references/api-reference.md — Full endpoint listing with request/response shapes
references/operations.md — Idempotency, rate limits, circuit breaker, disk management
references/custom-domains.md — How to expose a service on a real domain via Traefik
references/server-setup.md — Fresh server installation from scratch