Docs
Deploy from git
Two deploy paths exist:
- Image —
POST /v1/serviceswith"image"(Docker Hub name or127.0.0.1:5000/.../localhost:5000/...). Deploy starts asynchronously (10 minute timeout). - Git — create a service with a placeholder image (
ahcsends"pending"—imageis required), thenPOST /v1/services/{id}/buildswithsource_type: "git".
There is no git_url / branch field on the build API. The struct is:
{
"source_type": "git",
"source_url": "https://github.com/org/repo",
"source_ref": "main"
}
source_type must be git. source_ref defaults to main. SHA refs are allowed (sanitized, then clone + checkout).
Allowed git hosts
From allowedGitHosts in internal/builds:
github.comgitlab.combitbucket.orgsr.htcodeberg.org
Also enforced:
- HTTPS only
- No userinfo in the URL
- URL max 2048 chars
- Ref max 256 chars, charset
A-Za-z0-9._/-
This is SSRF / DNS-rebinding protection, not a preference list.
Sequence
# 1. Create the service (`image` is required; "pending" is the ahc placeholder)
curl -s -X POST "$AH_URL/v1/services" \
-H "Authorization: Bearer $AH_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"name":"web","image":"pending","port":3000}'
# 2. Start the build
curl -s -X POST "$AH_URL/v1/services/$SERVICE_ID/builds" \
-H "Authorization: Bearer $AH_KEY" \
-H "Content-Type: application/json" \
-d '{"source_type":"git","source_url":"https://github.com/org/repo","source_ref":"main"}'
# 202 {"build_id":"...","status":"pending","image":"127.0.0.1:5000/..."}
# 3. Stream logs (no 30s request timeout on this route)
curl -sN -H "Authorization: Bearer $AH_KEY" \
"$AH_URL/v1/services/$SERVICE_ID/builds/$BUILD_ID/logs?follow=true"
On success the build manager calls DeployImage and the service moves to running (or failed). Poll GET /v1/services/{id}.
Stuck deploying longer than 10 minutes is marked failed by the reconciler.
Images ah will accept
services.ValidateImage:
- Docker Hub:
name,name:tag,org/name:tag(lowercase path rules) - Local registry:
127.0.0.1:5000/...orlocalhost:5000/...with at least two path segments after the host
Arbitrary registries are rejected.
CLI and skill shortcuts
ahc deploy https://github.com/org/repo web --port 3000
# or
ahc deploy nginx:alpine web --port 80
ahc deploy treats http:// or https:// as git. The Claude command /ah-deploy does the same, then streams build logs.
Redeploy
POST /v1/services/{id}/redeploy
Starts a new build from the last git source_url / source_ref recorded for that service. Returns the new build object.
Cancel
DELETE /v1/services/{id}/builds/{buildID}
Tenant suspend also runs CancelAllForTenant for pending/running builds.
Concurrency and disk
- In-flight build slots: 10 process-wide. A full queue fails the new build with 409
build queue full; try again later. - Disk: warn 80%, block 90% on
/var/lib/ahand/var/lib/dockerbefore the build starts. - Logs: 5 MB then last 500 lines kept.
List
GET /v1/services/{id}/builds
GET /v1/builds?limit=100 # tenant-wide, max 200
GET /v1/services/{id}/builds/{id}
GET /v1/services/{id}/deployments
POST /v1/services/{id}/deployments/{deploymentID}/cancel
URLs after deploy
| Mode | URL |
|---|---|
No --base-domain | http://{serviceID}.localhost |
--base-domain apps.example.com | https://{dns_label}.apps.example.com |
dns_label is stored at create time and does not change if you later restart ah with a different --base-domain. Reserved names are rejected (see Architecture). Labels are globally unique across tenants.
Snapshots (fork, not a build)
POST /v1/services/{id}/snapshots {"name":"before"}
POST /v1/services?from_snapshot={snapshotID} {"name":"fork"}
Copies image, port, and env. No Nixpacks run. Still goes through the async deploy path.