Deployment
The framework repo ships reference material under deploy/: a multi-stage Dockerfile (both stages pinned by digest; it builds from the committed gen/ rather than regenerating proto inside the image, and the runtime stage is distroless/static:nonroot running as USER nonroot:nonroot) and a docker-compose with otelcol, Prometheus, Grafana and PgBouncer configuration. The three items below are deliberately left to the operator — the framework does not do them for you.
Scaffold secret preflight
Section titled “Scaffold secret preflight”A new scaffold carries local development configuration. Before the first git add, Docker build, or CI handoff, verify that every secret is externalized. Do not leave a usable JWT signing secret in version control or an image layer.
- Supply real secrets through a secret manager, mounted file, or runtime environment using GORTEXA_AUTH__JWT_SECRET.
- Keep only an unusable placeholder or a secret-free example configuration in the repository; ignore local overrides.
- Make sure both the Docker build context and runtime image exclude local secret configuration, and verify the image starts only with runtime injection. The reference
.dockerignorealready keeps.git,.envand*.local.yamlout of the build context — keep it if you fork. - Plan rotation, issuer, and audience; services sharing a secret should use different audiences.
This is a deployment gate, not optional later work. Likewise, the scaffold’s in-memory resource is only a demo; establish real storage, cache, and MQ lifecycle before treating readiness as meaningful.
Terminate TLS in front
Section titled “Terminate TLS in front”The service port is cleartext h2c; the server does no TLS itself. Bearer tokens crossing this port are unencrypted. Put the service behind a reverse proxy, service mesh or load balancer that terminates TLS. Do not expose the h2c port to the public internet.
Wire readiness to real dependencies
Section titled “Wire readiness to real dependencies”/readyz only reflects that the process is alive until you register real checks with app.Health().Register(...):
app.Health().Register("postgres", func(ctx context.Context) health.State { if err := pool.Ping(ctx); err != nil { return health.Unhealthy } return health.Healthy})Health probes can target a private ops port, separate from the public one: kernel.WithAdminListener(addr) serves only /healthz and /readyz on a separate plain-HTTP port, so an orchestrator can probe health without reaching the public h2c port (app.AdminAddr() reports the bound address). To expose pprof or a custom ops handler on a separate port, use kernel.WithExtraListener(lis, h). Both are opt-in; the main h2c port’s single-port multiplexing is unchanged.
Each check runs under a 5-second timeout, so a single slow dependency cannot stall the query.
What the server logs by default
Section titled “What the server logs by default”A metric and a span event are both dropped on the floor unless an OTLP endpoint is configured, so each of the following also writes one WARN line to your logger, with no telemetry backend required:
| Event | Message | Fields |
|---|---|---|
/readyz answers 503 |
gortexa: readiness not serving |
status, checks |
| Boot with an empty health registry | gortexa: no health checks registered; /readyz reports ready without probing anything |
— |
| Circuit breaker opens or closes | gortexa: circuit breaker state change |
method, from, to |
| MCP origin or batch denial | mcp: rejected request from a disallowed origin / mcp: rejected an oversized JSON-RPC batch |
origin / elements + cap, remote |
The empty-registry line is the one worth alerting on: it means /readyz is reporting ready while probing nothing. The breaker line appears automatically only when you build the chain with interceptor.NewSet, which back-fills CBConfig.Logger from Config.Logger; a breaker you construct directly needs it set explicitly. The MCP lines are the exception to “your logger”: the bridge writes them to slog.Default(), so call slog.SetDefault(log) if you want them in your configured format and OTLP pipeline.
Authorization is the application’s job
Section titled “Authorization is the application’s job”The framework does authentication only: it verifies the JWT signature and puts the claims into the context. Who may do what (roles, permission policy) is decided by the application, in handlers or its own interceptor:
claims, ok := auth.ClaimsFrom(ctx)if !ok || !slices.Contains(claims.Roles, "admin") { // reject}Graceful shutdown
Section titled “Graceful shutdown”On SIGTERM the server drains in-flight requests within server.shutdown_timeout (default 20s) before exiting. If the drain overruns, the remaining connections are force-closed and the gRPC server hard-stopped; the deadline is logged as a warning, not returned as an error, so the process still exits 0 and still runs the telemetry-flush hooks — an open MCP SSE or Health.Watch stream during a rolling deploy is not reported as a crash. Interruption under authenticated load is verified by example 05.
Production flags
Section titled “Production flags”- Keep
server.reflectionatfalse: do not expose the service structure. observ.otlp_insecuremust befalsefor any remote collector.- Real secrets use runtime injection and must not exist in Git or an image layer — see Configuration.