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:
docker pull ghcr.io/kymrapro-del/stratum:0.1.0Verify it before you run it
| Evidence | Proves | Check |
|---|---|---|
| GitHub build attestation (Sigstore) | Built by the release workflow, at this commit | gh attestation verify oci://ghcr.io/kymrapro-del/stratum:0.1.0 --repo kymrapro-del/stratum |
| SLSA Build Level 3 provenance | Signed by an isolated generator the build cannot influence | slsa-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 image | docker 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.
docker run --rm -p 3000:3000 -e BRUNEL_DEMO_MODE=1 ghcr.io/kymrapro-del/stratum:0.1.0Run it for real
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_URLto the publichttps://address: session cookies then use the__Host-prefix and theSecureflag. - Client addresses: rate limits and IP allowlists read
X-Real-IP, then the firstX-Forwarded-Forentry. Your reverse proxy must set these itself and overwrite any value from the client, and must overwriteX-Forwarded-Host. Never expose the container directly. - Health:
GET /api/healthanswers{"ok":true,…}; the image declares a DockerHEALTHCHECKon it.
Hardening checklist
| Measure | How |
|---|---|
| Non-root user | Built 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 image | Deploy by digest after verifying the attestation |
| Outbound traffic | GitHub, your AI provider and, if set, Stripe and Upstash; set BRUNEL_OUTBOUND_ALLOWLIST to restrict user-configured destinations |