Skip to content

Deploy

One agent instance runs one project, typically on a dedicated VM. There are two install paths: the older compile-on-the-VM make deploy (for developers and the dogfood host), and the release-binary steelframe agent install (for operators without a compiler).

The agent reads these from the process environment:

VariableDefaultDescription
STEELFRAME_HOST0.0.0.0Bind address; set 127.0.0.1 behind nginx
STEELFRAME_PORT7700Bind port
STEELFRAME_DB_URLfile:steelframe.duckdbDuckDB file URL (file: prefix optional)
STEELFRAME_QUACK_PORT9494Localhost Quack listen port (127.0.0.1 only)
STEELFRAME_QUACK_TOKEN—Overrides the Quack token; prefer the 0600 file
STEELFRAME_QUACK_TOKEN_FILEsibling quack.tokenToken file (mode 0600). Production default /opt/steelframe/data/quack.token
STEELFRAME_QUACK_EXTENSION—Path to a matching quack.duckdb_extension. Setting it also opens DuckDB with allow_unsigned_extensions, because a local build is unsigned (this pin cannot INSTALL from the public CDN). Restart to rebind.
STEELFRAME_MASTER_KEY—Master key for credential encryption and session signing
STEELFRAME_TMP—Sandbox root (disk-backed on a systemd unit)
STEELFRAME_LOG_FILE—Log file; self-heal scans it for panics

Everything else (repo, labels, timeouts, App id, coding runtime) lives in the DB config table and is edited via the TUI, GUI, or API.

On the VM, as the steelframe user (create it first if needed):

Terminal window
cp infra/secrets.mk.example infra/secrets.mk && $EDITOR infra/secrets.mk
make deploy

make deploy is idempotent — the same command handles first-time setup and updates.

infra/secrets.mk holds everything per-VM (gitignored):

HOST := agent.example.com
STEELFRAME_MASTER_KEY := …
OAUTH2_PROXY_CLIENT_ID := …
OAUTH2_PROXY_CLIENT_SECRET := …
OAUTH2_PROXY_COOKIE_SECRET := …

The OAuth credentials come from a GitHub OAuth App with callback https://<HOST>/oauth2/callback.

The orchestrator runs three sub-makes:

Sub-makeWhat it installs
infra/steelframeHost tools (gcx, mbx), RustFS sidecar, the agent binary, the systemd unit
infra/oauth2-proxyoauth2-proxy binary + unit in front of the agent
infra/nginxReverse proxy + TLS for HOST

infra/steelframe targets:

  • deploy — install-mbx install-tools install-rustfs install-bin install-unit restart status.
  • deploy-source — the same without install-tools, for a source-profile host with no Grafana/gcx.
  • install-mbx — installs mr-boxington, the shared Cargo build cache; make build uses it when present, so host rebuilds reuse compiler outputs.
  • install-rustfs — installs the RustFS sidecar (session-memory WAL on 127.0.0.1:9000, console 127.0.0.1:9001, data at /opt/steelframe/data/rustfs).
  • install-unit — renders steelframe.service.tpl with the master key and installs it to /etc/systemd/system/.
  • status — fails the deploy if the unit is not active or crash-looping.

The unit runs as user steelframe, WorkingDirectory=/opt/steelframe, DB at /opt/steelframe/data/steelframe.duckdb, Delegate=yes, Restart=always.

Localhost Quack (CALL quack_serve on 127.0.0.1:9494) is best-effort on a dedicated connection. Bind failure is non-fatal. The token is /opt/steelframe/data/quack.token (mode 0600), never config, never worker env. Do not reverse-proxy port 9494. Coding-runtime workers never receive the token.

This pin cannot INSTALL quack from the public CDN (git-hash 404). Build a matching quack.duckdb_extension from the pin and set STEELFRAME_QUACK_EXTENSION to its path, then restart. Because a local build is unsigned, setting the variable also opens DuckDB with allow_unsigned_extensions: the extension file is trusted like the binary itself, so keep it operator-owned and out of the repo. Without the extension, inspect with the DuckDB 2.0 CLI on a stopped agent.

Do not promote DuckDB 2.0-alpha to dogfood or production without an explicit ask. Staging must be green first. Merge sf/duckdb to main only after that ask.

On a host with the release binary and no compiler:

Terminal window
steelframe setup
sudo STEELFRAME_MASTER_KEY=… steelframe agent install

steelframe setup installs host tools (git, gh, gcx, kubectl, bun, duckdb, rustfs) and the Cloudflare security-audit skill and one coding runtime, then persists coding_runtime to the DB.

steelframe agent install:

  • Refuses to run without STEELFRAME_MASTER_KEY.
  • Creates the steelframe user and /opt/steelframe (data, tmp, logs).
  • Installs the binary to /opt/steelframe/steelframe.
  • Writes /opt/steelframe/agent.env (mode 0600) with the master key and env.
  • Renders a unit with ExecStartPre=-/opt/steelframe/steelframe update, ExecStart=/opt/steelframe/steelframe agent run, and Restart=always.
  • Configures and starts the RustFS sidecar.

TLS and SSO are still make -C infra deploy (nginx + oauth2-proxy); agent install does not replace certbot or oauth2-proxy.

Production GitHub operations should use a dedicated GitHub App so commits and PRs use a consistent bot identity. Without an App, ambient gh auth on the agent host is used, which is fine for local development but not production.

The coordinator keeps running after the database is invalidated. The process logs the fatal and stays up, so Restart= does not fire, and Discord cannot be read from the database. The agent sentinel is a separate process. It watches the current service MainPID. A database-invalidation line on that PID, a unit that stays down past a grace period, or a listen port that refuses connections past that grace makes it restart the agent once. It then waits. If the agent is still unhealthy, it sends one alert and does not restart again until the agent is healthy. A later outage restarts once more, and never more often than the cooldown.

An optional public URL is alert-only. A refused connection there does not restart the agent. HTTP 200 and 401 are not treated as proof that the database is alive.

Discord is read from STEELFRAME_SENTINEL_WEBHOOK_FILE (mode 0600) or STEELFRAME_SENTINEL_WEBHOOK. It is never read from the database. Do not point the sentinel unit at agent.env; that file holds the master key. On a system unit the webhook file is /etc/steelframe/discord-webhook, root-owned, outside the directories the agent user can write. See Agent sentinel.

VariableDefaultDescription
STEELFRAME_SENTINEL_SCOPEsystem as root, otherwise usersystemctl scope of the agent unit
STEELFRAME_SENTINEL_UNITsteelframe.serviceUnit to watch and restart
STEELFRAME_SENTINEL_PORT7700Local port probed with a TCP connect
STEELFRAME_SENTINEL_INTERVAL30Seconds between checks
STEELFRAME_SENTINEL_GRACE120Seconds to ignore a down port or a down unit after a start or stop
STEELFRAME_SENTINEL_COOLDOWN900Minimum seconds between restarts of separate incidents
STEELFRAME_SENTINEL_WEBHOOK_FILE/etc/steelframe/discord-webhook or ~/opt/steelframe/data/discord-webhookWebhook URL, mode 0600
STEELFRAME_SENTINEL_PUBLIC_URLunsetAlert-only probe, for example https://host/api/status
STEELFRAME_SENTINEL_NAMEshort hostnameName used in the alert text

make -C infra/steelframe install-sentinel installs the system unit. A user-unit host installs infra/steelframe/steelframe-sentinel.user.service under the user manager. The checked-in unit does not set the public URL; add a drop-in per host.

  • make deploy must run as the steelframe user; it fails if HOST or any secret is missing.
  • Secrets stay in infra/secrets.mk (gitignored) or /opt/steelframe/agent.env (mode 0600); never commit them.
  • The agent binds 127.0.0.1:7700 and is reached through nginx. On the dogfood host the tree is a user unit at ~/opt/steelframe.
  • delivery_profile=source skips production_monitor; use deploy-source so gcx is not required.
  • RustFS must be up before the agent; the unit declares After=/Wants= on steelframe-rustfs.service.
  • DuckDB 2.0-alpha storage: before a later-2.0 bump, stop the agent and EXPORT DATABASE '/opt/steelframe/data/export' (FORMAT PARQUET); then IMPORT DATABASE after the new binary. Rehearse on staging. Do not promote 2.0-alpha without an explicit ask.