Environment variables
These are the core variables read by the SupaCloud server. The standalone
deployment sets them via .env (see
Self-host with Docker Compose). This is a
working subset, not an exhaustive list.
| Variable | Default | Purpose |
|---|---|---|
MODE |
— | dev (the default when unset) relaxes prod-only guards. Set MODE=prod (or production) for production — any other value, including unset, runs in dev mode. |
PUBLIC_URL |
— | Public base URL of the server. Required in production. |
CORS_ALLOWED_ORIGINS |
— | Comma-separated origins allowed to call the API. |
SUPACLOUD_SKIP_BOOT_MIGRATIONS |
false |
true makes the server only check at boot that every migration it was built with is applied, and refuse to start otherwise. Use it when a deploy step runs supacloud-server migrate (same environment as the server) before the new version starts, so two versions can run side by side during a switch. Unset, the server applies pending migrations itself. A server always starts against migrations newer than itself. |
DATABASE_URL |
postgresql://supacloud:supacloud@postgres:5432/supacloud |
PostgreSQL connection string. |
SERVER_PORT |
8080 |
Port the server listens on. |
Health probes and shutdown
Section titled “Health probes and shutdown”The server answers two probes outside /api. GET /livez reports that the process
runs and checks nothing else. GET /readyz returns 200 while the server should get
traffic and 503 once it is shutting down or cannot reach its database within two
seconds. Point the load balancer at /readyz and the container healthcheck at
/livez: a healthcheck on /readyz would mark every replica unhealthy during a
database outage and take the whole service down. An outage of a realtime or cache
service does not make /readyz 503.
On SIGTERM the server marks itself not ready, waits for the drain delay so the load
balancer stops routing to it, then stops accepting connections, tells every browser
WebSocket to reconnect elsewhere, and finishes open requests within the grace
period. Set the container’s stop grace period to at least the sum of both.
| Variable | Default | Purpose |
|---|---|---|
SUPACLOUD_SHUTDOWN_LB_DELAY_SECS |
5 |
Seconds between SIGTERM (readiness turns 503) and closing the listeners. 0 skips the delay. |
SUPACLOUD_SHUTDOWN_GRACE_SECS |
20 |
Upper bound for open requests to finish after the listeners close; the process then exits. |
Auth & secrets
Section titled “Auth & secrets”| Variable | Default | Purpose |
|---|---|---|
AUTH_PROVIDER |
builtin |
Authentication provider. |
SUPACLOUD_JWT_SECRET |
— | Required. Session-signing secret, ≥ 32 bytes. |
SUPACLOUD_INITIAL_ADMIN_EMAILS |
— | Email(s) provisioned as the initial owner. Required in prod for builtin/oidc (boot fails if unset); the example files ship admin@example.com only as a placeholder. |
AUTH_COOKIE_SECURE |
— | false for local HTTP; true (default in prod) for HTTPS. |
SECRET_BACKEND |
auto | openbao or env. Auto-detects OpenBao when vault auth is present. See Secret provisioning. |
SUPACLOUD_CALIBRATION_SWEEP_INTERVAL_SECS |
86400 (1 d) |
How often the unattended model-chain calibration decides. The loop polls hourly and runs a pass once the last one is at least this old; the first pass comes 30 minutes after boot, 0 turns it off. With several server replicas exactly one runs each pass — the others see the lease and skip. |
SUPACLOUD_CALIBRATION_SWEEP_MIN_AGE_SECS |
the sweep interval | Minimum age of the last pass before the hourly poll runs another one. 0 means every poll, an hourly measuring mode — set it deliberately and briefly, the benchmark sources’ budgets are shared. |
SUPACLOUD_CALIBRATION_REFRESH_INTERVAL_SECS |
21600 (6 h) |
How often the benchmark source snapshots behind the calibration are refreshed. They also serve the interactive “calibrate” button, which is why they refresh faster than the sweep decides. 0 turns the refresh off. |
SUPACLOUD_RESOURCE_SECRET_RECONCILE_INTERVAL_SECS |
21600 (6 h) |
How often the server removes workflow resource secrets that no resource row will ever read again, for example after a resource create whose database write failed. Runs only with an OpenBao session; the first pass comes 15 minutes after boot; 0 turns it off. A secret is removed only after two passes found it unclaimed, not deleted and older than 15 minutes. Removal is permanent when the OpenBao policy grants delete on supacloud/metadata/workflows/resources/*; without that grant the secret is soft-deleted instead and the server logs an error. |
AI & voice
Section titled “AI & voice”| Variable | Default | Purpose |
|---|---|---|
GROQ_API_KEY |
— | Instance-wide Groq key (note: Groq, not Grok) used for the voice-input speech-to-text (Whisper). Set it to enable dictation across the app. Resolved env-first, then from the OpenBao supacloud/ai map when SECRET_BACKEND=openbao. |
XAI_API_KEY |
— | Instance-wide xAI (Grok) API key. Backs the Grok models (grok-4.5, grok-build-0.1, …) offered through the OpenCode agent. Per-user/workspace keys are normally entered under Settings → API keys (provider xAI / Grok); this env var is the instance-wide fallback. Resolved env-first, then from the OpenBao supacloud/ai map when SECRET_BACKEND=openbao. |
Agent sandbox
Section titled “Agent sandbox”| Variable | Default | Purpose |
|---|---|---|
AGENT_NETWORK |
supacloud-agents |
Docker network agent containers join. |
AGENT_MEMORY_BYTES |
2147483648 |
Per-agent memory cap (bytes). |
AGENT_NANO_CPUS |
2000000000 |
Per-agent CPU cap (nano-CPUs). |
AGENT_PIDS_LIMIT |
512 |
Per-agent PID limit. |
AGENT_READ_ONLY_ROOTFS |
true |
Mount the agent root filesystem read-only. |
AGENT_TMPFS_TMP_MB |
512 |
Size ceiling for the agent’s /tmp tmpfs, in MiB. |
AGENT_TMPFS_RUN_MB |
64 |
Size ceiling for the agent’s /run tmpfs, in MiB. |
AGENT_TMPFS_HOME_MB |
512 |
Size ceiling for /home/agent — the CLIs’ config/state and the npm cache. |
SUPACLOUD_AGENT_TEST_DB |
false |
Wand 6 option C — instance-level CAPABILITY gate for the per-task ephemeral Postgres sidecar. With it off no sidecar is ever created. With it on, a task gets a sidecar only when it opts in (task.config.test_db = true); a non-selected task gets none. An explicit caller TEST_DATABASE_URL wins: it is never overwritten and the sidecar is suppressed. The sidecar injects TEST_DATABASE_URL, its password is registered in the run’s redaction set, and it is removed with the agent container. |
AGENT_TEST_DB_IMAGE |
postgres:18 |
Image for the SUPACLOUD_AGENT_TEST_DB sidecar. |
ATTACHMENTS_FILE_WAIT_MS |
30000 |
How long an agent container waits for its attachments file. Set INSIDE the agent container, not on the server. |
AGENT_TASK_TIMEOUT_MINUTES |
90 |
Hard timeout per agent task. |
MAX_PARALLEL_TASKS_PER_WORKSPACE |
3 |
Concurrency cap per workspace. |
Observability
Section titled “Observability”| Variable | Default | Purpose |
|---|---|---|
SUPACLOUD_GLITCHTIP_DSN |
— | DSN of a GlitchTip (Sentry-compatible) project. When set, the server pushes targeted error events to it: operational alert breaches (tick SLA, queue age, error spike, budget 80 %, weekly window low — one event per breach onset), circuit-breaker trips / provider failover (grouped per provider and error class), and terminal workflow-run failures (with a run deep link, grouped by workflow + node + error class so a repeatedly failing interval flow stays one issue), plus error!-level log events and panics. Every payload is scrubbed by the server’s redaction wall before it leaves the process. Unset ⇒ the sink is fully disabled — no client, no network attempt. Resolved env-first, then from the OpenBao supacloud/app map. |
Runner fleet (hub mode)
Section titled “Runner fleet (hub mode)”Opt-in; off by default. See Set up a runner securely and Steer & monitor runners.
On the server (hub)
Section titled “On the server (hub)”| Variable | Default | Purpose |
|---|---|---|
SUPACLOUD_HUB_MODE |
false |
Enable dispatch to remote runners. While false, every task runs locally regardless of registered runners. |
SUPACLOUD_AGENT_EXECUTION |
— | fleet_only runs agents only on remote runners: when no eligible runner takes a task, its launch fails with a message naming the cause instead of starting a container on the server. Unset, the server falls back to running the agent itself. |
SUPACLOUD_INSTANCE_ID |
hostname | This server’s stable identity. Every container it starts carries the label supacloud.instance=hub@<id> (runner@<id> on a runner), and the clean-up at boot and on shutdown removes only containers with its own label, so two servers or a server and a runner can share one Docker daemon. Keep it stable across restarts of the same replica. Containers started before this label existed are not removed automatically. |
SUPACLOUD_HUB_EVENT_RELAY |
false |
Relay realtime hub events across server replicas over Postgres LISTEN/NOTIFY, so a WebSocket client on any replica receives a signal emitted on another. Enable only when running multiple server replicas; a single replica is unaffected. |
SUPACLOUD_RUNNER_STALE_SECS |
90 |
Heartbeat staleness window before a runner is marked offline and its un-started work re-surfaced. Floor 45. |
SUPACLOUD_TLS_CERT_PATH |
— | PEM server-certificate path. Set together with SUPACLOUD_TLS_KEY_PATH to serve the hub over TLS directly (rather than terminating TLS at a proxy). Supplying one without the other is a hard boot error (fail-closed). Unset (both) ⇒ plain HTTP exactly as today (byte-identical default). |
SUPACLOUD_TLS_KEY_PATH |
— | PEM private-key path, the key half of SUPACLOUD_TLS_CERT_PATH. |
SUPACLOUD_TLS_CLIENT_CA_PATH |
— | PEM CA bundle. When set (TLS must already be on) the hub additionally requires and verifies a client certificate on every connection — mutual TLS (mTLS). A runner without a trusted client cert is rejected at the handshake. Setting this without SUPACLOUD_TLS_CERT_PATH/_KEY_PATH is a hard boot error. See Set up a runner securely. |
Installer entry points
Section titled “Installer entry points”/install-runner.sh and /install-runner.ps1 are the public entry points the web
wizard hands the operator. The hub does not carry the installer body: it redirects
to the published, stamped installer on the release feed (runner-release.yml
publishes it, version-keyed, on the public bw-public package feed). Both values are
non-secret.
| Variable | Default | Purpose |
|---|---|---|
SUPACLOUD_RUNNER_VERSION |
— | Release version the hub points at: <download base>/<version>/install-runner.{sh,ps1}. Required — unset, the entry points answer 503. |
SUPACLOUD_RUNNER_DOWNLOAD_BASE |
— | Base URL of the release feed, e.g. https://git.blockworx.tech/api/packages/bw-public/generic/supacloud-runner. Required — unset, the entry points answer 503. |
The published, stamped installer + the release binaries come from the Forgejo release pipeline (ADR 0046), not from this server. Until the release channel is configured, onboard from the Docker image or a self-built binary.
Autoscaler (experimental, local-Docker only in v1)
Section titled “Autoscaler (experimental, local-Docker only in v1)”Off by default. When off, no background loop is spawned and dispatch is unchanged. See Steer & monitor runners.
| Variable | Default | Purpose |
|---|---|---|
SUPACLOUD_RUNNER_AUTOSCALE |
off |
off or on. When on, the hub runs a built-in autoscaler loop that spawns and scales-in local-Docker runner containers from the fleet pull-queue signal. Default off ⇒ no loop, no behaviour change. |
SUPACLOUD_RUNNER_AUTOSCALE_MIN |
— | Floor on the live runner count the autoscaler maintains. |
SUPACLOUD_RUNNER_AUTOSCALE_MAX |
— | Ceiling on the runner count the autoscaler may spawn. |
SUPACLOUD_RUNNER_AUTOSCALE_RUNNER_IMAGE |
— | The container image the autoscaler launches for each new runner. Required when autoscale is on. The autoscaler also needs a runner token to hand the spawned container (see the how-to). |
SUPACLOUD_RUNNER_AUTOSCALE_COOLDOWN_SECS |
— | Minimum seconds between scale actions, damping flapping. |
SUPACLOUD_RUNNER_AUTOSCALE_NETWORK |
supacloud-agents |
Docker network the autoscaler attaches each spawned runner container to. |
On the runner (worker node, --runner mode)
Section titled “On the runner (worker node, --runner mode)”| Variable | Default | Purpose |
|---|---|---|
SUPACLOUD_HUB_URL |
— | Required. The hub base URL. Must be https:// unless the insecure flag below is set. |
SUPACLOUD_RUNNER_TOKEN |
— | The scrn_… bearer token from a system-admin registration (shown once). Provide either this or SUPACLOUD_RUNNER_ENROLLMENT_TOKEN. |
SUPACLOUD_RUNNER_ENROLLMENT_TOKEN |
— | The one-time enrollment token the runner itself redeems on boot (over the gRPC ExchangeToken RPC) for a scoped, short-lived scrnj_… JWT (then auto-refreshes). Used for third-party onboarding — see Set up a runner securely. The token is single-use and consumed on first exchange; provide EITHER this OR SUPACLOUD_RUNNER_TOKEN, never both. |
SUPACLOUD_RUNNER_LABELS |
— | Advertised capability labels, comma/space separated (folded lowercase + deduped). Sent as capabilities.labels at enrollment, so a task’s config.runner.required_labels can route to this runner. The runner’s backends/isolation capabilities are derived from SUPACLOUD_RUNNER_EXECUTION_BACKEND, never set here. |
SUPACLOUD_RUNNER_TOKEN_FILE |
$XDG_STATE_HOME/supacloud-runner/runner-token (fallback $HOME/.local/state/…; %LOCALAPPDATA%\supacloud\runner-token on Windows) |
Path of the file the runner persists its scoped scrnj_… JWT to, and reads back on boot. The enrollment token is single-use, so without this file a reboot would leave an enrolled runner with no credential. Written atomically with mode 0600; read before any exchange, so a valid file means no second ExchangeToken. An expired, malformed, or group/world-readable file is discarded and the runner re-enrols. The installers set this explicitly (runner-token under the runner’s state directory — /var/lib/supacloud-runner as root). Ignored for a static scrn_… token, which never expires. See Set up a runner securely. |
SUPACLOUD_RUNNER_TOKEN_REFRESH_SECS |
0 |
For an enrolled (scrnj_… JWT) runner only: how many seconds before expiry to re-exchange the JWT. 0 (the default) derives the cadence automatically — roughly half the token TTL (from runners.token_expires_at / the JWT exp) — so the daemon refreshes well before the token lapses. A static scrn_… token never expires and is never refreshed; this variable is a no-op for it. |
SUPACLOUD_RUNNER_NAME |
supacloud-runner |
Display name reported to the hub (unset/blank defaults to this). |
SUPACLOUD_RUNNER_HEARTBEAT_SECS |
30 |
How often the runner heartbeats the hub. |
SUPACLOUD_RUNNER_POLL_SECS |
3 |
How often the runner polls for claimable work. |
SUPACLOUD_RUNNER_ALLOW_INSECURE_TRANSPORT |
false |
Permit a plaintext http:// hub URL. Only on a trusted private network — the token and per-run secrets cross this channel. |
AGENT_NETWORK |
supacloud-agents |
Docker network the runner puts agent containers on (must exist on the worker). |
SUPACLOUD_RUNNER_EXECUTION_BACKEND |
docker |
How the runner runs a claimed task: docker (local Docker, the default), ssh-docker (remote Docker over SSH), raw-ssh-exec (degraded, unsandboxed remote process) or microvm (strongest isolation — one launcher-spawned microVM per task). The SSH backends need a binary built with the ssh-backends feature; microvm needs one built with the microvm-backend feature. Selecting a backend whose feature was not compiled in is a hard boot error (fail-closed). See SSH execution backends and Set up a runner securely. |
SUPACLOUD_RUNNER_MICROVM_CMD |
— | microvm backend only: the operator-provisioned launcher command SupaCloud invokes to start the per-task microVM (e.g. a Firecracker/Kata wrapper). Required when SUPACLOUD_RUNNER_EXECUTION_BACKEND=microvm — the runner image ships no microVM launcher. |
SUPACLOUD_RUNNER_TLS_CLIENT_CERT_PATH |
— | PEM client-certificate path. Set together with SUPACLOUD_RUNNER_TLS_CLIENT_KEY_PATH so the runner presents a client identity to a hub that requires mTLS. Supplying one without the other is a hard boot error. Unset ⇒ current one-way-TLS behaviour. |
SUPACLOUD_RUNNER_TLS_CLIENT_KEY_PATH |
— | PEM private-key path, the key half of SUPACLOUD_RUNNER_TLS_CLIENT_CERT_PATH. |
SUPACLOUD_RUNNER_TLS_CA_PATH |
— | PEM CA bundle the runner uses to pin the hub’s certificate (rather than the system trust store). Optional; unset ⇒ current behaviour. |
SUPACLOUD_SSH_HOST |
— | ssh://user@host:port — the SSH endpoint for either SSH backend. Required for ssh-docker/raw-ssh-exec. |
SUPACLOUD_SSH_KNOWN_HOSTS |
— | Path to a pinned known_hosts file. Required for either SSH backend; the runner refuses to start if it is missing or empty. For raw-ssh-exec this IS the in-code anchor (russh rejects an unknown/mismatched key — no TOFU). For ssh-docker SupaCloud cannot wire it through bollard — it is only a typo-guard; you must point your system ssh client at it and set SUPACLOUD_SSH_DOCKER_HOSTKEY_DELEGATED. |
SUPACLOUD_SSH_DOCKER_HOSTKEY_DELEGATED |
false |
ssh-docker only — required. Explicit acknowledgment that SSH host-key verification is delegated to the system ssh client (SupaCloud cannot enforce the pin through bollard). Selecting ssh-docker without this truthy flag is a hard boot error — otherwise an unknown host key would be silently accepted (accept-new TOFU). Set it only after configuring the runner host’s ssh with StrictHostKeyChecking=yes + UserKnownHostsFile=<SUPACLOUD_SSH_KNOWN_HOSTS>. |
SUPACLOUD_SSH_KEY_PATH |
— | Path to the SSH private key file. Required for raw-ssh-exec (key auth only, no agent forwarding); optional for ssh-docker. The key bytes are never logged. |
SUPACLOUD_RUNNER_ALLOW_UNSANDBOXED |
false |
GATE 1 for raw-ssh-exec. Selecting that backend without this truthy flag is a hard boot error — it runs the agent with NO container isolation and NO resource caps. |
SUPACLOUD_SSH_EXEC_AGENT_CMD |
— | raw-ssh-exec only: the operator-curated remote agent-launch command template ({image}/{agent_type} placeholders, shell-quoted). Required — the work payload carries no agent entrypoint. |
SUPACLOUD_SSH_INSECURE_HOST_KEY |
false |
raw-ssh-exec only: disable host-key verification (accept any server key). The only escape from fail-closed host-key checking; logs a loud warning. Never on an untrusted network. |