Chapter 06

Deployment

RuleForge is a stateless ASP.NET Core service. No databases of its own, no leader election, no bootstrap dance. Two env vars and a rule source.

Environment variables

VariablePurposeDefault
RULEFORGE_RULE_SOURCElocal or dflocal
RULEFORGE_FIXTURES_DIRLocal rule directory (when source = local)./fixtures/rules
RULEFORGE_REFS_DIRLocal reference-set directory (when source = local)./fixtures/refs
RULEFORGE_DF_BASE_URLDocumentForge base URL (when source = df)https://documentforge.onrender.com
RULEFORGE_DF_API_KEYDocumentForge bearer token(required for df source)
RULEFORGE_ENVDF environment to read bindings fromstaging
RULEFORGE_API_KEYCaller-side X-AERO-Key shared secret(unset = open dev mode)
ASPNETCORE_URLSListening URL(s)http://localhost:5000

Docker

A multi-stage Dockerfile is the simplest path. Builds in 30 seconds on a warm cache, produces a ~110MB self-contained runtime image.

# Dockerfile
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish src/RuleForge.Api -c Release -o /out

FROM mcr.microsoft.com/dotnet/aspnet:9.0
WORKDIR /app
COPY --from=build /out ./
COPY fixtures ./fixtures
EXPOSE 8080
ENV ASPNETCORE_URLS=http://+:8080
ENTRYPOINT ["dotnet", "RuleForge.Api.dll"]

Render

A minimal render.yaml at the repo root pairs the engine with a co-located DocumentForge node, so cold-paths are loopback rather than cross-region.

# render.yaml
services:
  - type: web
    name: ruleforge
    runtime: docker
    plan: starter
    envVars:
      - key: RULEFORGE_RULE_SOURCE
        value: df
      - key: RULEFORGE_DF_BASE_URL
        value: http://documentforge:5000
      - key: RULEFORGE_ENV
        value: prod
      - key: RULEFORGE_API_KEY
        sync: false               # set in Render dashboard

  - type: pserv
    name: documentforge
    runtime: docker
    repo: https://github.com/tailwind-retailing/documentforge
    disk:
      name: data
      mountPath: /data
      sizeGB: 1

Security

API auth

Setting RULEFORGE_API_KEY activates middleware that checks every request for either:

Comparison is constant-time. Unauthenticated requests return 401 with WWW-Authenticate: AeroKey realm="aero-engine". /health is always allowed (so liveness probes don't need to ship the secret).

When the env var is unset, every request is allowed — useful for local dev and integration tests.

Network

The engine never originates outbound traffic except to DocumentForge. Lock down egress to whatever DF endpoint you've configured. No external SaaS dependencies.

Secrets

Health checks

GET /health returns 200 {"ok": true} regardless of rule-source state. It does not probe DocumentForge — health is "the engine is up", not "every dependency is healthy". Wire DocumentForge into a separate probe if your orchestrator needs that signal.

GET /admin/bindings dumps the current binding list (auto-router state). Useful for ops sanity but, like every other endpoint, gated by the API key when configured.

Logging

Standard ASP.NET Core logging — stdout with structured fields. The auto-router logs one line per bound endpoint at boot:

info: Bound POST /v1/ancillary/bag-policy → rule-bag-policy@7
info: Bound POST /v1/ancillary/tier-bonus → rule-tier-bonus@1
info: Now listening on: http://localhost:5050

Per-request tracing is opt-in via ?debug=true on the URL or X-Debug: true on the request. Production mode skips trace allocation entirely; debug mode adds ~10× overhead but emits per-node start times, durations, ctx reads/writes and sub-rule run IDs.

Scaling

Pure horizontal scale — add pods, no coordination required. Each pod independently caches rule snapshots; on publish, restart pods (or wait the 30-second env-binding TTL) for the new version to roll out. The benchmarks show a single pod can sustain ~73K req/s on 16 cores; multi-pod scales linearly.

Co-located DocumentForge keeps the cold path ~600× faster than cross-region. If you don't want a dfdb sidecar, the next-best option is putting DF in the same VPC as the engine.