Deployment
GoFastr apps compile to a single static binary with templates, runtime
JS, and (optionally) embedded migrations baked in. That makes deployment
boring in the good way: build one binary, run it with a few env vars.
The single-binary model
go build your main package → one executable. It serves HTTP, runs
auto-migrations on Start, and embeds the UI runtime — no Node, no asset
pipeline, no sidecar.
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o app ././app # listens on :8080 (or $PORT)
Go version.
go.moddeclaresgo 1.26. The framework core only needs
Go 1.24 (generic type aliases); the 1.26 floor comes from the optional
battery/print/chromepdfPDF dependency (chromedp/cdproto). If you don't
use that battery, an older Go works in practice — but the declared floor is
what the toolchain enforces.
SQLite vs Postgres. The bundled
gofastrCLI uses SQLite. For a
Postgres deployment, import a Postgres driver in your app and pass a
*sql.DBviaframework.WithDB.CGO_ENABLED=0works with the pure-Go
jackc/pgxstdlib driver; themattn/go-sqlite3driver needs CGO, so
choose your base image accordingly (see the Dockerfile note below).
Production Dockerfile
Multi-stage, distroless runtime, non-root, pure-Go build (Postgres):
# ---- build ----FROM golang:1.26 AS buildWORKDIR /srcCOPY go.mod go.sum ./RUN go mod downloadCOPY . .RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/app ./# ---- runtime ----FROM gcr.io/distroless/static-debian12:nonrootCOPY --from=build /out/app /appENV PORT=8080EXPOSE 8080USER nonroot:nonrootENTRYPOINT ["/app"]
Using the CGO SQLite driver (
mattn/go-sqlite3) instead? Build with
CGO_ENABLED=1ongolang:1.26and run ongcr.io/distroless/base-debian12
(has libc) rather thanstatic.
Configuration (env)
GoFastr reads config from the environment (with .env auto-loaded in
development — see dotenv; real env always wins). Common vars:
| Var | Purpose |
|---|---|
PORT | Listen port. A bare value like 8080 is normalized to :8080, so PaaS-injected $PORT works. |
DATABASE_URL | Your app reads this and passes the connection to WithDB. |
APP_ENV | Selects .env.<APP_ENV> in development. |
| auth secrets | If you use battery/auth, set its JWT/session secret explicitly in production — do not rely on the dev auto-generated secret (it rotates per process and silently invalidates sessions). See auth. |
Secrets
GoFastr reads secrets from the process environment — it does not bundle a
secrets manager, and .env files are a development convenience only
(never commit them, never ship them in the image). In production, inject
secrets as env vars from your platform's secret store:
- Kubernetes: a
Secretmounted as env vars (or via the CSI secrets
store driver). - AWS: Secrets Manager / SSM Parameter Store → env at task start
(ECS task definitionsecrets:, or fetch-on-boot). - Vault: the Vault Agent injector or
vault kv getin an init step.
The one secret every auth-enabled app must set is AuthConfig.JWTSecret
(typically from env). With DevMode=false and no JWTSecret, the auth
battery fails closed: Init returns an error and the app refuses to
start — an empty signing key yields forgeable, restart-unstable
sessions. In dev, a per-process secret is auto-minted (with a WARN log)
so the boilerplate never ships a literal change-me.
Migrations
App.Start auto-migrates on boot: it creates missing tables and adds
missing columns (additive only — it never drops, renames, or retypes;
those need a reviewed versioned migration from gofastr migrate generate,
applied with gofastr migrate up). For controlled rollouts, run migrations
as a separate step with the CLI instead of on every replica's boot:
gofastr migrate up --db-url="$DATABASE_URL"gofastr migrate status --db-url="$DATABASE_URL"
See Migrations for the production-hardening details
(locking, checksums, dirty-state, destructive-change gating).
TLS & graceful shutdown
Terminate TLS at your ingress/load balancer (the common setup) and run the
app over plain HTTP behind it. App.Start installs signal handling and
drains in-flight requests on SIGINT/SIGTERM via App.Shutdown, so
rolling deploys don't cut active requests.
Health & metrics
- Readiness/liveness: auto-registered probes (plus a DB readiness check
when a DB is configured) — point your orchestrator's probes at them. See
Health checks. - Metrics: enable
framework.WithMetrics()to expose Prometheus
/metrics; enableframework.WithTracing()for OpenTelemetry. See
Observability. Scrape/metricsfrom inside the
cluster — it is unauthenticated by design.
Checklist
- [ ]
CGO_ENABLEDmatches your DB driver (0 for pgx, 1 for go-sqlite3). - [ ] Auth/session secret set explicitly (not the dev default).
- [ ] Migrations run as a deploy step (or accepted on-boot for single-replica).
- [ ] Readiness/liveness probes wired.
- [ ]
/metricsscraped from inside the network only. - [ ] TLS terminated at ingress.
Common mistakes
- Shipping
.envfiles in the image. They are a development
convenience only. In production, inject secrets as real env vars
from your platform's secret store — real env always wins over file
values anyway, so a stowaway.envis at best dead weight and at
worst a leaked secret. - Expecting boot auto-migrate to handle every schema change. It
creates tables and adds columns — additive only. Drops, renames, and
type changes need a reviewed versioned migration (gofastr migrate generate <name>thengofastr migrate up); booting won't do them. - CGO flag and base image out of sync.
CGO_ENABLED=0with
mattn/go-sqlite3fails at build; a CGO build on
distroless/staticfails at runtime (no libc — use
base-debian12). Pure-Go pgx is what makes the static image work. - Booting production auth without a
JWTSecret. With
DevMode=falseand noJWTSecret, the battery refuses to start
(Initerrors,App.Startfails). There is no auto-minted fallback
in production — the dev fallback rotates per process, which would
silently invalidate sessions on every deploy. Set the secret
explicitly from env.