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).
Environment variables
Section titled “Environment variables”The agent reads these from the process environment:
| Variable | Default | Description |
|---|---|---|
STEELFRAME_HOST | 0.0.0.0 | Bind address; set 127.0.0.1 behind nginx |
STEELFRAME_PORT | 7700 | Bind port |
STEELFRAME_DB_URL | file:steelframe.duckdb | DuckDB file URL (file: prefix optional) |
STEELFRAME_QUACK_PORT | 9494 | Localhost Quack listen port (127.0.0.1 only) |
STEELFRAME_QUACK_TOKEN | — | Overrides the Quack token; prefer the 0600 file |
STEELFRAME_QUACK_TOKEN_FILE | sibling quack.token | Token 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.
Path A: make deploy from a checkout
Section titled “Path A: make deploy from a checkout”On the VM, as the steelframe user (create it first if needed):
cp infra/secrets.mk.example infra/secrets.mk && $EDITOR infra/secrets.mkmake deploymake deploy is idempotent — the same command handles first-time setup and updates.
infra/secrets.mk holds everything per-VM (gitignored):
HOST := agent.example.comSTEELFRAME_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-make | What it installs |
|---|---|
infra/steelframe | Host tools (gcx, mbx), RustFS sidecar, the agent binary, the systemd unit |
infra/oauth2-proxy | oauth2-proxy binary + unit in front of the agent |
infra/nginx | Reverse proxy + TLS for HOST |
infra/steelframe targets:
deploy—install-mbx install-tools install-rustfs install-bin install-unit restart status.deploy-source— the same withoutinstall-tools, for a source-profile host with no Grafana/gcx.install-mbx— installs mr-boxington, the shared Cargo build cache;make builduses it when present, so host rebuilds reuse compiler outputs.install-rustfs— installs the RustFS sidecar (session-memory WAL on127.0.0.1:9000, console127.0.0.1:9001, data at/opt/steelframe/data/rustfs).install-unit— renderssteelframe.service.tplwith 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.
Path B: steelframe agent install
Section titled “Path B: steelframe agent install”On a host with the release binary and no compiler:
steelframe setupsudo STEELFRAME_MASTER_KEY=… steelframe agent installsteelframe 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
steelframeuser and/opt/steelframe(data,tmp,logs). - Installs the binary to
/opt/steelframe/steelframe. - Writes
/opt/steelframe/agent.env(mode0600) with the master key and env. - Renders a unit with
ExecStartPre=-/opt/steelframe/steelframe update,ExecStart=/opt/steelframe/steelframe agent run, andRestart=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.
GitHub App
Section titled “GitHub App”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.
Agent sentinel
Section titled “Agent sentinel”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.
| Variable | Default | Description |
|---|---|---|
STEELFRAME_SENTINEL_SCOPE | system as root, otherwise user | systemctl scope of the agent unit |
STEELFRAME_SENTINEL_UNIT | steelframe.service | Unit to watch and restart |
STEELFRAME_SENTINEL_PORT | 7700 | Local port probed with a TCP connect |
STEELFRAME_SENTINEL_INTERVAL | 30 | Seconds between checks |
STEELFRAME_SENTINEL_GRACE | 120 | Seconds to ignore a down port or a down unit after a start or stop |
STEELFRAME_SENTINEL_COOLDOWN | 900 | Minimum seconds between restarts of separate incidents |
STEELFRAME_SENTINEL_WEBHOOK_FILE | /etc/steelframe/discord-webhook or ~/opt/steelframe/data/discord-webhook | Webhook URL, mode 0600 |
STEELFRAME_SENTINEL_PUBLIC_URL | unset | Alert-only probe, for example https://host/api/status |
STEELFRAME_SENTINEL_NAME | short hostname | Name 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.
Gotchas
Section titled “Gotchas”make deploymust run as thesteelframeuser; it fails ifHOSTor any secret is missing.- Secrets stay in
infra/secrets.mk(gitignored) or/opt/steelframe/agent.env(mode0600); never commit them. - The agent binds
127.0.0.1:7700and is reached through nginx. On the dogfood host the tree is a user unit at~/opt/steelframe. delivery_profile=sourceskipsproduction_monitor; usedeploy-sourceso gcx is not required.- RustFS must be up before the agent; the unit declares
After=/Wants=onsteelframe-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);thenIMPORT DATABASEafter the new binary. Rehearse on staging. Do not promote 2.0-alpha without an explicit ask.