Deployment

Running a generated ForgeDB app in production — the Docker/compose path, the FORGEDB_* environment, graceful shutdown, and health checks.

A ForgeDB-generated app is a single self-contained axum binary. forgedb init emits a blessed container path (multi-stage Dockerfile, .dockerignore, docker-compose.yml) and a 12-factor main.rs configured entirely from the environment. There is no runtime config file; the binary reads its settings from FORGEDB_* env vars.

The environment#

The generated src/main.rs reads all config from the environment, opens its data directory, and serves the REST + WebSocket API plus the operational routes.

VarDefaultPurpose
FORGEDB_HOST127.0.0.1Bind host — set 0.0.0.0 in a container.
FORGEDB_PORT3000Bind port.
FORGEDB_DATAdataData directory (the per-tenant root).
FORGEDB_TENANT(unset)Tenant this process serves → opens <FORGEDB_DATA>/<tenant>.
RUST_LOGinfoLog level filter (tracing-subscriber env-filter).
FORGEDB_LOG_FORMAT(text)Set json for machine-parseable log lines.

JWT tenant guard#

Verify-only — ForgeDB verifies tokens your IdP issues, it never issues them. The guard is enabled when FORGEDB_JWT_PUBKEY (or FORGEDB_JWKS_URL) is set:

VarDefaultPurpose
FORGEDB_JWT_PUBKEY(unset)Path to the IdP's PEM public (verification) key.
FORGEDB_JWKS_URL(unset)JWKS endpoint to fetch verification keys.
FORGEDB_JWT_ALGRS256Signature algorithm (asymmetric only).
FORGEDB_JWT_ISSUER(unset)Expected iss.
FORGEDB_JWT_AUDIENCE(unset)Expected aud.
FORGEDB_TENANT_CLAIMtenantClaim carrying the tenant id.
FORGEDB_JWT_LEEWAY60Clock-skew leeway (seconds).

When the guard is on, FORGEDB_TENANT is required — the token's tenant claim is cross-checked against it (wrong tenant → 403). These env vars are the runtime bridge for the [auth] config table; forgedb tenant create prints the exact exports.

Operational routes (no auth)#

Three ops routes sit outside the JWT guard so load balancers and Kubernetes probes reach them without a token:

RouteResponseUse
GET /health{"status":"ok"}Liveness — never touches the DB.
GET /ready{"status":"ready"}Readiness — acquires a read lock.
GET /metricsper-model row countsMinimal JSON scrape (not Prometheus text in v1).

Point your liveness probe at /health and your readiness probe at /ready. Full route list: Generated REST API.

Containers#

forgedb init writes a ready-to-build image. Generate before building — the build context expects the generated code present:

forgedb generate all --output ./generated
docker build -t myblog .
docker run -p 3000:3000 -v myblog-data:/data myblog

The generated Dockerfile is multi-stage: a rust:1-slim builder running cargo build --release --locked, then a debian:bookworm-slim runtime carrying only ca-certificates

  • curl. That runtime runs as a non-root forgedb user, mounts /data as a VOLUME, bakes FORGEDB_HOST=0.0.0.0 as a default, and HEALTHCHECKs by curling /health.

Compose#

docker compose up --build

The generated docker-compose.yml maps 3000:3000, mounts a named forgedb-data volume at /data, and includes the same /health healthcheck. Tenancy, JSON logs, and JWT knobs are present as commented environment entries — uncomment what you need.

Graceful shutdown#

The server installs a tracing subscriber and drains in-flight requests on SIGINT/SIGTERM (axum::serve(..).with_graceful_shutdown), so a container stop or Ctrl-C never severs a connection mid-write.

The single-writer contract#

One writer per data directory

The process takes an advisory directory lock on open; a second writer against the same directory refuses to start (it does not corrupt). Do not point two server processes at the same FORGEDB_DATA/<tenant> dir. Concurrent readers are fine.

Scale a single tenant vertically (one process) and scale across tenants horizontally (one process per tenant). Manage tenant directories with forgedb tenant {create,list,drop}.

forgedb tenant create acme --root ./data          # make ./data/acme
FORGEDB_TENANT=acme FORGEDB_DATA=./data ./myblog  # serve tenant acme

Reverse proxy / TLS#

Terminate TLS and route hosts/subdomains with nginx or Caddy in front of the bound port. Forward the Upgrade/Connection headers so the change-feed / live-query / replication WebSocket routes work, and forward Authorization for the JWT guard:

location / {
    proxy_pass http://127.0.0.1:3000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Authorization $http_authorization;
}

Logs#

Structured logging is the standard stack — tracing + tracing-subscriber (honors RUST_LOG) + a tower-http request span per request. Text by default; FORGEDB_LOG_FORMAT=json emits one JSON object per line for a log aggregator.

On-host (systemd)#

forgedb init also emits deploy/<name>.service, deploy/<name>.env, and a deploy/README.md for running under systemd (non-root DynamicUser, managed StateDirectory, SIGTERM drain). Other init systems (OpenRC, runit, s6, launchd, supervisord, Windows services) follow the same shape and are documented, not emitted.

Prebuilt CLI binaries#

Distributing the CLI

Tagging a release drives a cargo-dist workflow that cross-builds the forgedb CLI (plus the bundled forgedb-lsp) for Linux (x86_64 glibc/musl, aarch64) and macOS (Intel/ARM), attaches the archives + a shell installer to a GitHub Release, and fans them out to Homebrew, npm, and PyPI (a maturin sidecar) — all live as of v0.2.0. Windows isn't supported yet (the binary is Unix-only); run under WSL2 in the meantime. See installation for every channel. This ships the CLI; your generated app is built from its own Dockerfile.

Search documentation

Find pages across the ForgeDB docs