- Docs
- Operate
- Deployment
Deployment
Ship Zever to production with one checklist for build, config, secrets, shutdown, health, scaling, and hardening.
The production checklist for a tested checkout. Build once, configure per environment, run one container per process, and close it on SIGTERM. There is no build-time codegen: the .zen compiler runs explicitly (zever compile, zever generate) during development, never at container start.
Build one binary
Section titled “Build one binary”make build # go build ./..., CI checkgo build -o myapp ./cmd/myapp # one artifact per serviceGOOS=linux GOARCH=amd64 go build -o myapp ./cmd/myapp # cross-compile| Target | Shape | Pick it for |
|---|---|---|
| Systemd | ExecStart=/usr/local/bin/myapp with EnvironmentFile |
VMs and bare metal. |
| Kubernetes | Image plus env: secrets and .env ConfigMap. |
Orchestrated rollouts. |
| Container image | docker build -f examples/showcase/Dockerfile . |
Multi-stage: golang:1.27-trixie builder (CGO_ENABLED=0, -trimpath, -ldflags="-s -w"), final gcr.io/distroless/static-debian13:nonroot with an absolute ENTRYPOINT ["/server"]. |
| Compose | DB_PASSWORD=... docker compose up --build |
Scaffolded compose.yaml when zever new picks db=postgres or *=redis. Health-gated startup. |
Static builds work because sqlite is pure Go (no cgo) and the distroless static base carries CA certificates for Stripe and cloud TLS. Build from the repo root; the builder runs with GOWORK=off. The worker follows the same pattern with ./cmd/worker. Build from a clean tree at a tag and keep the SBOM. Tagged releases build static binaries and SBOMs with SLSA provenance; verify before deploy:
gh attestation verify dist/zever-linux-amd64 --repo zenta-dev/zevergh attestation verify sbom/cmd_zever.json --repo zenta-dev/zeverConfigure and inject secrets
Section titled “Configure and inject secrets”Ship an .env file for structure and set secrets and per-environment deltas through env. Layering is defaults < .env < .env.local < process env, and config.Default() never reaches production because its crypto key is deterministic and public. See Configuration.
DB_ADAPTER=postgresDB_DSN=postgres://app:secret@db:5432/appDB_MAX_CONNS=20DB_MIN_CONNS=2CACHE_ADAPTER=redisCACHE_ADDR=redis:6379LOG_ADAPTER=slogOBSERVABILITY_ADAPTER=otlpOBSERVABILITY_ENDPOINT=otel:4317OBSERVABILITY_SERVICE_NAME=myappDB_DSN='postgres://app:${DB_PASSWORD}@db:5432/app'AUTH_JWT_SECRET='prod-secret-at-least-32-bytes-long'Resolve with an explicit env (config.FromEnv()) so a wrong working directory cannot fall back to sqlite. Keep secrets in env or mounted files, and prefer Secrets with Vault for payment credentials (SOC2 and PCI expect a real secrets manager). The env adapter cannot rotate at runtime: rotate by restarting with new values. Print only cfg.RedactedServices() at boot.
env, err := config.FromEnv()if err != nil { log.Fatal(err)}cfg, err := config.Resolve(env)if err != nil { log.Fatal(err)}c := container.New(cfg, app.Providers()...)
srv := &http.Server{Addr: ":8080", Handler: routes(c)}go func() { if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatal(err) }}()
<-sigCh // SIGINT or SIGTERMctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)defer cancel()_ = srv.Shutdown(ctx)if err := c.Close(ctx); err != nil { log.Fatal(err)}Shut down in order
Section titled “Shut down in order”Build one container per process, resolve services in handlers, and close on SIGINT or SIGTERM:
env, err := config.FromEnv()if err != nil { log.Fatal(err)}cfg, err := config.Resolve(env)if err != nil { log.Fatal(err)}c := container.New(cfg, app.Providers()...)
srv := &http.Server{Addr: ":8080", Handler: routes(c)}go func() { if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatal(err) }}()
<-sigCh // SIGINT or SIGTERMctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)defer cancel()_ = srv.Shutdown(ctx)if err := c.Close(ctx); err != nil { log.Fatal(err)}Health probes
Section titled “Health probes”Wire /healthz and /readyz:
r.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK)})r.HandleFunc("/readyz", func(w http.ResponseWriter, r *http.Request) { if err := c.DB().Ping(r.Context()); err != nil { http.Error(w, "db", http.StatusServiceUnavailable) return } w.WriteHeader(http.StatusOK)}Configure liveness on /healthz (no deps) and readiness on /readyz (with 3s db timeout). In Kubernetes, the readinessProbe timeout must exceed the db timeout so a slow query doesn’t flip the probe.
Graceful shutdown
Section titled “Graceful shutdown”On SIGTERM the generated server flips gRPC health to NOT_SERVING, calls GracefulStop(), shuts the HTTP server down, and closes the container, all within a 10 second grace. Keep terminationGracePeriodSeconds above that plus any preStop delay.
A short preStop delay lets endpoint removal propagate before SIGTERM. The reference image is distroless and has no shell; the example uses the native sleep lifecycle handler (Kubernetes 1.30+). On older clusters, add a tiny static sleep binary.
Config reload
Section titled “Config reload”Log level and the rate limiter swap on SIGHUP; everything else needs a rollout. A rolling restart (kubectl rollout restart deployment/myapp-server) is the supported way to change anything else.
Scaling
Section titled “Scaling”Run more than one replica only with shared-state adapters (redis, db) for cache, queue, session, lock, rate-limit. With more than one scheduler replica, wire a locker so a cron slot fires once; otherwise run the scheduler in a single replica. Scale workers on queue depth (queue.Length) with an autoscaler such as KEDA rather than on CPU.
Migrations
Section titled “Migrations”Run zever db migrate as a Kubernetes Job (or an init step in your pipeline) before rolling out a schema change, not inside every pod. Use a distinct Job per release so a failed migration blocks the rollout instead of racing replicas.
Profiling and debugging
Section titled “Profiling and debugging”The generated server can expose pprof on a loopback-only listener (--pprof-addr). In a cluster, set PPROF_ADDR=127.0.0.1:6060 on the pod and reach it with a port-forward rather than a Service:
kubectl port-forward deploy/myapp-server 6060:6060go tool pprof http://127.0.0.1:6060/debug/pprof/heapSee Profiling with pprof. Never run zever tinker against production.
Hardening checklist
Section titled “Hardening checklist”- Distroless, non-root, read-only rootfs, drop all capabilities
- Secrets in Secret (not ConfigMap), rotate by restart
- Readiness probe with db timeout
- Liveness on
/healthz(no deps) -
preStopdelay +terminationGracePeriodSeconds - Read-only rootfs, drop ALL capabilities
- NetworkPolicy: deny all egress except db, redis, otel, allowed APIs
-
PodDisruptionBudgetwithminAvailable - Rolling restart for config changes (SIGHUP only reloads log/ratelimit)
- Migrations as separate Job before rollout