Self-hosting

Run Brunel yourself

Run Brunel from its container image. Verify the image's provenance, start it in demo mode or for real, harden the container, and put it behind TLS.

Brunel ships as a container image built from the repository's Dockerfile: the Next.js standalone server and nothing else, running as the unprivileged node user (uid 1000) with read-only application files.

Get the image

Each release publishes the image to GitHub Packages, only after lint, typecheck and the test suite pass:

bash
docker pull ghcr.io/kymrapro-del/stratum:0.1.0

Verify it before you run it

EvidenceProvesCheck
GitHub build attestation (Sigstore)Built by the release workflow, at this commitgh attestation verify oci://ghcr.io/kymrapro-del/stratum:0.1.0 --repo kymrapro-del/stratum
SLSA Build Level 3 provenanceSigned by an isolated generator the build cannot influenceslsa-verifier verify-image ghcr.io/kymrapro-del/stratum@sha256:<digest> --source-uri github.com/kymrapro-del/stratum --source-tag v0.1.0
SBOM (SPDX)Every package in the imagedocker buildx imagetools inspect ghcr.io/kymrapro-del/stratum:0.1.0 --format '{{ json .SBOM }}'

Deploy by digest (@sha256:…), not by tag, so the image you verified is the image that runs.

Try it in demo mode

No database and no AI provider: the in-memory store and the deterministic architect answer, and a banner says so.

bash
docker run --rm -p 3000:3000 -e BRUNEL_DEMO_MODE=1 ghcr.io/kymrapro-del/stratum:0.1.0

Run it for real

bash
docker run -d --name brunel -p 3000:3000 --read-only --tmpfs /tmp \
  --tmpfs /app/.next/cache:uid=1000,gid=1000 \
  --cap-drop ALL --security-opt no-new-privileges \
  --env-file brunel.env \
  ghcr.io/kymrapro-del/stratum@sha256:<digest>

brunel.env holds the configuration; see Configuration reference. Keep it out of version control and readable only by the account that starts the container.

Notes

  • Database migrations run on first use.
  • TLS belongs in front of the container. Set AUTH_URL to the public https:// address: session cookies then use the __Host- prefix and the Secure flag.
  • Client addresses: rate limits and IP allowlists read X-Real-IP, then the first X-Forwarded-For entry. Your reverse proxy must set these itself and overwrite any value from the client, and must overwrite X-Forwarded-Host. Never expose the container directly.
  • Health: GET /api/health answers {"ok":true,…}; the image declares a Docker HEALTHCHECK on it.

Hardening checklist

MeasureHow
Non-root userBuilt in
Read-only root filesystem--read-only with tmpfs for /tmp and /app/.next/cache
No Linux capabilities--cap-drop ALL (port 3000 needs none)
No privilege escalation--security-opt no-new-privileges
Pinned imageDeploy by digest after verifying the attestation
Outbound trafficGitHub, your AI provider and, if set, Stripe and Upstash; set BRUNEL_OUTBOUND_ALLOWLIST to restrict user-configured destinations
Run Brunel yourself · Brunel Docs