Skip to content

Deploy: one static binary

A bri app AOT-compiles to a single static CGO_ENABLED=0 binary, byte-identical to the interpreter path (ADR 0071). The dev loop is a REPL (cljgo dev, live re-def, nREPL); the deploy artifact is one file with no runtime to distribute — the design bet the benchmarks measure (~20 MB image, ~30 ms cold-start, ~35 MB RSS under load — and ~2x fewer req/s than JVM http-kit, which the benchmarks page reports too).

Terminal window
cljgo build -o server src/app/main.cljg # AOT-compile to one static binary
CGO_ENABLED=0 GOOS=linux cljgo build -o server src/app/main.cljg # for a Linux image
cljgo dist --target linux/amd64,linux/arm64 # or cross-compile a whole matrix at once (ADR 0077)

The binary links the compiled core (never the interpreter), so it starts in single-digit milliseconds. It embeds nothing it doesn’t need — an app that never requires bri.core.telemetry never links the OpenTelemetry SDK (ADR 0074), and a db-less app never links SQLite/pgx (ADR 0076). For shipping binaries to many platforms at once (releases, Homebrew), see cljgo dist.

cljgo new --template web ships a multi-stage Dockerfile that AOT-compiles the app to a static binary and copies it into a scratch image — typically single-digit MB plus your config and static assets:

# syntax=docker/dockerfile:1
ARG CLJGO_VERSION=v0.8.9
FROM golang:1.26 AS build
ARG CLJGO_VERSION
RUN go install github.com/muthuishere/cljgo/cmd/cljgo@${CLJGO_VERSION}
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux cljgo build -o /server src/app/main.cljg
FROM scratch
COPY --from=build /server /server
COPY --from=build /app/conf.edn /conf.edn
COPY --from=build /app/public /public
ENV APP_PROFILE=prod
EXPOSE 3000
ENTRYPOINT ["/server"]

The build stage go installs the cljgo compiler at a pinned CLJGO_VERSION and fetches the matching runtime from the Go module proxy — no repo checkout. Bump CLJGO_VERSION to move to a newer cljgo.

Terminal window
docker build -t app .
docker run -p 3000:3000 app

conf.edn defaults :port to 3000; any key is overridden by an APP_* env var (the secrets-are-env doctrine), so the same image runs in every environment — see bri.core.config:

Terminal window
docker run -p 3000:3000 \
-e APP_PROFILE=prod \
-e APP_DB_URL=postgres://… \
-e APP_AUTH__SECRET=… \
app

Secrets never live in the image — they arrive as env at run time. APP_DB_URL flips cljg.data.cast from the zero-install SQLite default to Postgres with no code change; APP_AUTH__SECRET is the bri.core.security signing key that prod must set.

(db/migrate! conn "migrations") is idempotent and forward-only — run it on boot (the delay pattern) or as a one-shot before rollout. It is the same call in dev and prod; running it twice is a no-op. See cljg.data.cast.

The API stack serves ops endpoints by default: GET /healthz (liveness), GET /readyz (runs :ready-checks → 200/503), and GET /metrics (Prometheus text) — wire them straight into your orchestrator’s probes. Guard /metrics in prod with :metrics-guard (auth/admin-only).

Questions or feedback on this page? Comment below with your GitHub account — comments are public and live in the project's GitHub Discussions.