Skip to content

Environment Reference

Daniel Hokanson edited this page Aug 30, 2026 · 1 revision

A Forge box's configuration is a single .env file next to the compose files, created from .env.example by setup.sh. .env.example is the authoritative list — it ships with the tree you are running, carries the current defaults, and explains each setting inline. This page does not restate it. It describes the classes of setting, which of them the tooling owns and which are yours, and the handful whose behaviour is surprising enough to state twice.

What is deliberately not here: integration credentials. Those are entered in the admin settings screens and stored as encrypted rows in the database, which is why they survive an image upgrade and why a boot-time hydrator treats the database as authoritative. See Configuration and Integrations. The security-relevant defaults and the order to change them in are on Hardening a Production Install.

Who writes what

Written by Settings
setup.sh, on .env creation Everything from the example, plus a generated JWT_KEY, the resolved deployment target, the compose project name, and the overlay list
forge-deploy The image pins, SCHEMA_IMAGE_TAG, ENABLE_SCHEMA_RECONCILE, the component scope, and the remote-database and remote-API wiring
You Everything else — and the security defaults, which nothing changes for you

Hand-editing a pin is how .env and the deploy record diverge. forge-deploy --status reads the deploy state file in preference to .env precisely so it can flag that. Change versions through the CLI.

.env is never committed and never appears in a backup. On a split install every box has its own. Back it up separately — see Backup and Restore.

The classes

Hosting mode

QBE_HOSTING_MODE is standalone or cohost and decides the compose overlay set, not just the documentation. Changing it by hand without rebuilding the stack produces the drift forge-preflight warns about. See Topologies.

Image tags and pins

SERVER_IMAGE_TAG, UI_IMAGE_TAG, TEST_IMAGE_TAG, DEMO_IMAGE_TAG and SCHEMA_IMAGE_TAG name what runs. The example file ships them as latest because that is what the local development compose path wants; production must be pinned to an immutable tag, and both the deploy path and --up refuse latest outright.

Alongside them sit the two settings that decide whether a live database moves forward at all — ENABLE_SCHEMA_RECONCILE, which ships false, and SCHEMA_IMAGE_TAG, which must be pinned in lockstep with the application when it is on. A semver deploy sets both for you; a build-tag deploy does not. SUPPORT_CONTACT, when set, marks the install as customer-operated: a reconcile that would drop data halts and names the contact instead of offering the destructive bypass, and the bypass flag is refused. The full reasoning is on the hub at Upgrades and Rollback.

HEALTHCHECK_TIMEOUT_SECS is how long a deploy waits for a service to report healthy before rolling back. Raise it on slow hosts and large databases: first boot on a populated database warms the API's composite health check before it answers, and a premature rollback is worse than a slow gate.

Binds and ports

Every published service takes a *_BIND and a *_PORT pair, and the compose defaults are all loopback. Standalone setup then widens most of them so LAN workflows work out of the box; --cohost leaves them alone. The table of which service takes which pair is on Operations and Troubleshooting; the rule that matters is that widening is per service and per interface, and that widening the database is almost never the answer.

Two extras belong to the UI tier rather than to exposure: UI_PORT_ALT is the standby port the blue/green cutover uses, and UI_BLUE_GREEN turns that path off when set false.

If setup finds a port already taken by something else, it moves that mapping to the next free port and records the change — except 80 and 443, which are deliberately excluded because they are the addresses people are told to browse to.

Database and storage

POSTGRES_DB, POSTGRES_USER and POSTGRES_PASSWORD configure the local database; DB_HOST and DB_PORT, when present, point the API at a remote one instead. On the storage side, STORAGE_PROVIDER picks between object storage and a local path, MINIO_ROOT_USER/MINIO_ROOT_PASSWORD are its credentials, MINIO_PUBLIC_ENDPOINT is what a browser is handed for a pre-signed URL, and MINIO_ENDPOINT (with the mirrored MINIO_HOST/MINIO_PORT the backup sidecar reads) points at a remote store.

Two of these are one-shot. The object store accepts root credentials only on its first initialisation, so changing them after buckets hold data leaves the API authenticating with values the store no longer expects. The rotation protocol is in docs/TROUBLESHOOTING.md, which carries its own warning that it is not battle-tested. Change both storage credentials and the database password while the install is empty.

MINIO_PUBLIC_ENDPOINT is the one people forget on a LAN or public install: it is baked into the URLs handed to browsers, so leaving it on localhost breaks every attachment for everyone but you.

Authentication

JWT_KEY, JWT_ISSUER and JWT_AUDIENCE. Setup generates a random key and the API refuses to start on one shorter than 32 characters, so a clean install is already correct; an .env carried forward from an older tree or assembled by hand is the case to check. Changing the key invalidates every issued session. The SSO settings live beside them, but the domain allow-list deliberately is not wired to a plain environment variable — a present-but-empty entry would bind as a single empty string and reject every sign-in.

CORS and public URLs

FRONTEND_BASE_URL is where the application believes it lives; CORS_ORIGINS is a comma-separated list, no spaces, scheme included, merged with a built-in list of local origins rather than replacing it. Because the API's policy allows credentials, a wildcard is not an option, and the symptom of a missing entry is usually broken real-time updates rather than a blocked request — SignalR's negotiate step checks the origin even on same-host paths. The hub's Hardening page covers the policy; the practical advice is to list the www. form and every hostname anyone actually types.

Environment posture

ASPNETCORE_ENVIRONMENT ships as Development, and the API reads that as permission to behave like a developer's box: the rate limiter is a no-op, developer endpoints are mapped, and the interactive API reference is served. Setting it to Production is the first item on the hardening checklist. Beside it sit the data-shaping switches — RECREATE_DB, SEED_DEMO_DATA, SEED_USER_PASSWORD, MOCK_INTEGRATIONS — which decide whether the box comes up with demo data and canned integration responses. Verify the posture from inside the running container, not from the file: an edit that never reached a recreate is the usual reason a "hardened" box is still in Development.

SETUP_ACTIVATION_CODE_HASH gates the first-run wizard behind an activation code and is left blank on a self-hosted install.

Backups

BACKUP_SCHEDULE is a UTC cron expression, BACKUP_RETENTION_DAYS prunes old snapshot directories, and BACKUP_RUN_ON_START takes an immediate snapshot when the sidecar starts — useful on a first install or straight after a restore, and worth turning off again. Snapshots are local-volume only; there is no off-site destination yet. Backup and Restore has the restore procedure and the disaster-recovery checklist, and docs/backup-restore.md has the commands.

Topology and self-service upgrade

FORGE_DEPLOY_SERVICES declares which versioned components this box deploys, FORGE_SCOPED_OUT records which compose services it must not run, and DB_HOST/MINIO_ENDPOINT point a split API box at its data tier. DEPLOY_AGENT_URL and DEPLOY_AGENT_TOKEN connect the API to the host agent that performs upgrades; blank is a supported state — the in-app Updates screen then reports that upgrades are not available from this box and points at the command line. FORGE_PEER_AGENTS and FORGE_SCHEMA_AGENT are the coordinator's fan-out list. All of these are covered in Topologies.

Optional services

The AI, TTS, signing, logging and crash-reporting blocks configure containers that are off unless the box opts in through COMPOSE_PROFILES. Their settings are inert otherwise. The mobile block (MOBILE_INSTANCE_NAME, MOBILE_CERT_SHA256, MOBILE_MIN_APP_VERSION, MOBILE_CRASH_DSN) configures what phones see and pin — rotate the certificate fingerprint before the certificate changes, or enrolled devices stop trusting the box. See Mobile and Offline.

Settings the example file does not list

setup.sh and the CLI write several keys that are not in .env.example, because they are computed rather than configured. Expect to see them in a real .env:

Key Meaning
COMPOSE_PROJECT_NAME Pinned on creation so volumes follow the install rather than the directory it lives in. Do not add it to an existing install — that repoints every volume and reads as total data loss
COMPOSE_FILE The resolved overlay list, so a bare compose sees the same files the CLI does. When it is set, compose stops auto-loading docker-compose.override.yml, so that file must be listed explicitly
FORGE_SCOPED_OUT The per-box scope roster
DB_HOST, DB_PORT, MINIO_ENDPOINT, MINIO_HOST, MINIO_PORT Written by --remote-db; absent on a single box
ALLOW_DEMO_DATA_IN_PRODUCTION Set when a box that already opted into demo data is upgraded, so the upgrade does not fail closed on it

Missing keys are healed, not diffed. forge-deploy --recover compares your .env against .env.example and adds any setting the example has and yours does not, at the example's default. That is how a tree that moved on since setup ran catches up — and it is a reason to read the changelog rather than assume a new release changed nothing about configuration.

Generated files that are not .env

Several files in the tree are written by the tooling and carry a "do not edit by hand" banner, because the next run overwrites them:

  • docker-compose.override.yml — public and TLS port bindings, owned by setup.sh. Never park a version hold here.
  • docker-compose.scope.yml — the per-box component scope.
  • docker-compose.remote-api.yml and forge-api-proxy.conf — the UI box's forwarder.
  • The generated nginx vhost under the host's config directory, written by --edge.

Deploy state and the event log live outside the tree, under the host state directory and log path, and both can be relocated by environment variable so the whole deploy path can be exercised without root.