# 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

```sh
make build                                              # go build ./..., CI check
go build -o myapp ./cmd/myapp                           # one artifact per service
GOOS=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:

```sh
gh attestation verify dist/zever-linux-amd64 --repo zenta-dev/zever
gh attestation verify sbom/cmd_zever.json --repo zenta-dev/zever
```

## 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](https://zenta-dev.github.io/zever/guides/operate/configuration/).

```sh
DB_ADAPTER=postgres
DB_DSN=postgres://app:secret@db:5432/app
DB_MAX_CONNS=20
DB_MIN_CONNS=2
CACHE_ADAPTER=redis
CACHE_ADDR=redis:6379
LOG_ADAPTER=slog
OBSERVABILITY_ADAPTER=otlp
OBSERVABILITY_ENDPOINT=otel:4317
OBSERVABILITY_SERVICE_NAME=myapp
```

```sh
DB_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](https://zenta-dev.github.io/zever/guides/security/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.

```go title="main.go"
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 SIGTERM
ctx, 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

Build one container per process, resolve services in handlers, and close on SIGINT or SIGTERM:

```go title="main.go"
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 SIGTERM
ctx, 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

Wire `/healthz` and `/readyz`:

```go
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

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

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

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.

Do not use a `memory` cache, queue, session, lock, or rate-limit adapter with more than one replica: each pod holds its own state. Point them at Redis or your database. See [Choosing batteries](https://zenta-dev.github.io/zever/guides/extend/choosing-batteries/).

## 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

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:

```sh
kubectl port-forward deploy/myapp-server 6060:6060
go tool pprof http://127.0.0.1:6060/debug/pprof/heap
```

See [Profiling with pprof](https://zenta-dev.github.io/zever/guides/operate/observability/#profiling-with-pprof). Never run `zever tinker` against production.

## 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)
- [ ] `preStop` delay + `terminationGracePeriodSeconds`
- [ ] Read-only rootfs, drop ALL capabilities
- [ ] NetworkPolicy: deny all egress except db, redis, otel, allowed APIs
- [ ] `PodDisruptionBudget` with `minAvailable`
- [ ] Rolling restart for config changes (SIGHUP only reloads log/ratelimit)
- [ ] Migrations as separate Job before rollout

- [Kubernetes](https://zenta-dev.github.io/zever/guides/operate/kubernetes/): Manifests for server, worker, PDB, HPA.
- [Configuration](https://zenta-dev.github.io/zever/guides/operate/configuration/): Env-only, `.env`, `.env.local`, defaults.
- [Secrets](https://zenta-dev.github.io/zever/guides/operate/security/secrets/): Vault, env, rotation.
- [FAQ and troubleshooting](https://zenta-dev.github.io/zever/guides/operate/faq-troubleshooting/): Common problems.
