AI Server serves chat, embeddings, image generation, speech and vision from hardware you own
339
Runs the aisuite-server daemon (net10.0 head) as a cloud container. Everything is configured by
environment (12-factor); the image sits behind a load balancer / ingress / the AI Gateway and drains
cleanly on rolling upgrades. Background + rationale:
docs/v2/architecture/ai-server-deployment-forward-compat.md.
# Build from the REPO ROOT (the daemon's project graph spans shared v2/*). Needs BuildKit (Docker 23+).
docker build -f deploy/docker/aiserver/Dockerfile -t aisuite-server:local .
# Minimal run. NOTE: 0.0.0.0 serving requires a Pro entitlement + ≥1 API key (see "Licensing" below),
# so mount a prepared data dir:
docker run --rm -p 8080:8080 -v aisuite-data:/data aisuite-server:local
| Var | Default (image) | Meaning |
|---|---|---|
AISUITE_URLS | http://0.0.0.0:8080 | Bind address(es). Non-loopback ⇒ Pro + API keys required (fail-closed). ASPNETCORE_URLS also honored. |
AISUITE_DATA | /data | State root (keys, registry, audit, licensing). Mount a volume. |
AISUITE_ENGINE | stub | stub | real | gateway. real pulls multi-GB models on first chat — opt in deliberately. |
AISUITE_BIND | loopback | Policy view of the bind (loopback/lan/all). A non-loopback AISUITE_URLS auto-promotes this. |
AISUITE_TRUST_PROXY | (unset) | 1 to trust X-Forwarded-For/Proto/Host. Set only behind a trusted ingress/LB. |
AISUITE_DRAIN_SECONDS | 8 | On SIGTERM: seconds to hold with /readyz=503 so the LB deregisters before shutdown. |
AISUITE_SHUTDOWN_SECONDS | 30 | Graceful-shutdown budget for in-flight requests (fit inside the orchestrator's grace period). |
AISUITE_LOG_JSON | 1 | One JSON log object per line on stdout (for Fluent Bit / Loki / CloudWatch). |
AISUITE_SERVICE_MODE | (unset) | 1 for supervisor-friendly logging (rarely needed in a container). |
XDG_DATA_HOME | /data | Do not unset. See below — the daemon will not start as a non-root user without it. |
XDG_DATA_HOME is load-bearing (read before changing the user or data dir)The daemon keeps a survival root that is intentionally independent of AISUITE_DATA, and on Linux
it resolves from XDG_DATA_HOME (else $HOME/.local/share). This image runs as the non-root app
user, whose ~/.local/share does not exist — and .NET's Unix GetFolderPath returns an empty
string for a directory that doesn't exist. The daemon then builds a relative path and tries to
create it under its working directory, which app cannot write:
Unhandled exception. System.UnauthorizedAccessException: Access to the path '/app/AISuite' is denied.
XDG_DATA_HOME=/data fixes that and keeps the survival root inside your mounted volume — without
it the data would land in the container's ephemeral layer and be lost on docker rm. The directory it
points at must exist (the image creates /data). The same applies outside Docker: running the
daemon under a non-root Linux account with no ~/.local/share (systemd DynamicUser=, distroless,
K8s runAsUser) needs XDG_DATA_HOME set to a writable, existing directory.
Distinct endpoints, all anonymous and excluded from usage accounting:
GET /livez — liveness. 200 while the process runs, even while draining. Use it to decide
whether to restart the container. Never gate routing on it.GET /readyz — readiness. 200 when accepting traffic, 503 once draining. Use it to decide
whether to route to the container. This is what makes rolling upgrades zero-downtime.GET /api/health — legacy wire-compat probe (kept for the local subprocess backend); prefer /livez.livenessProbe:
httpGet: { path: /livez, port: 8080 }
periodSeconds: 10
readinessProbe:
httpGet: { path: /readyz, port: 8080 }
periodSeconds: 5
# Give the drain hook time to flip /readyz→503 and let the LB deregister before SIGKILL.
terminationGracePeriodSeconds: 40 # ≥ AISUITE_DRAIN_SECONDS + AISUITE_SHUTDOWN_SECONDS
On kubectl rollout, the new pod only receives traffic once /readyz is 200; the old pod flips to 503 on
SIGTERM, is pulled from the Service endpoints, drains in-flight requests, then exits — no dropped requests.
services:
aiserver:
build: { context: ../../.., dockerfile: deploy/docker/aiserver/Dockerfile }
ports: ["8080:8080"]
volumes: ["aisuite-data:/data"]
environment:
AISUITE_ENGINE: "real"
AISUITE_TRUST_PROXY: "1" # only if a proxy terminates in front
stop_grace_period: 40s
healthcheck:
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8080/readyz"]
interval: 10s
timeout: 3s
retries: 3
volumes: { aisuite-data: {} }
Binding to 0.0.0.0 is network serving, which fail-closes unless BOTH are present under AISUITE_DATA:
<data>/licensing/server-entitlement.json (issued by the licensed desktop app or
your enrollment flow).<data>/credentials/server-keys.json (only SHA-256 hashes are stored). Issue
with the server CLI, then bake the resulting file into the mounted volume.Without them the daemon logs the reason and exits non-zero (by design — never serve inference unauthenticated on a network bind). Loopback-only runs need neither, but a loopback bind isn't reachable across the container network, so it's only useful for in-container smoke tests.
Point the Gateway (or any reverse proxy) at these containers; set AISUITE_TRUST_PROXY=1 so per-client rate
limiting sees the real client IP via X-Forwarded-For. The Gateway health-checks /readyz and routes around
draining pods — the same contract shipped clients already speak (retry + idempotency headers), so no client
change is needed.
Content type
Image
Digest
sha256:fd3389355…
Size
706.1 MB
Last updated
1 day ago
docker pull softwaretailor/aiserver