Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

10. Deploying to Production

The bookstore runs cleanly on SQLite + go run . for development. Production needs three additional things: a real database, configuration from the environment, and a sensible operational contract — health probes, structured logs, graceful shutdown. Code changes are minimal; the framework was already built for this.

Swapping SQLite for PostgreSQL

Add the satellite:

go get github.com/xaleel/maniflex/db/postgres

Change one import in main.go:

- import "github.com/xaleel/maniflex/db/sqlite"
+ import "github.com/xaleel/maniflex/db/postgres"

…and the adapter open call:

db, err := postgres.Open(postgres.Options{
    WriteURL:        os.Getenv("DB_WRITE_URL"),
    ReadURL:         os.Getenv("DB_READ_URL"), // optional
    MaxOpenConns:    25,
    MaxIdleConns:    5,
    ConnMaxLifetime: 30 * time.Minute,
}, server.Registry())

Models, middleware, actions, and tests all carry over unchanged. The shared db/sqlcore adapter means SQL emitted by AutoMigrate is portable between the two backends. See PostgreSQL in Production for pool tuning, read replicas, and migration choices.

Configuration from environment

A single Config populated from os.Getenv:

// config.go
func loadConfig() maniflex.Config {
    return maniflex.Config{
        Port:        envInt("PORT", 8080),
        PathPrefix:  envStr("PATH_PREFIX", "/api"),
        ServiceName: envStr("SERVICE_NAME", "bookstore"),

        AutoMigrate:     envStr("AUTO_MIGRATE", "true") == "true",
        QueryTimeout:    envDuration("QUERY_TIMEOUT", 30*time.Second),
        ShutdownTimeout: envDuration("SHUTDOWN_TIMEOUT", 30*time.Second),

        HealthCheckDB: true,
        HealthTimeout: 3 * time.Second,

        Logger: slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
            Level: slog.LevelInfo,
        })),
    }
}

maniflex also ships a helper, maniflex.ConfigFromEnv(), that reads a conventional set of environment variables (PORT, PATH_PREFIX, DB_WRITE_URL, …). Pick whichever style fits your team — both produce the same maniflex.Config.

Production-safe migrations

AutoMigrate: true is convenient in development. In production, the prevailing pattern is:

  1. Disable AutoMigrate on every instance.
  2. Run schema changes through a dedicated migration tool (golang-migrate, Atlas, sqlc-migrate) executed as a separate one-shot step in your deploy pipeline.
  3. Roll out the new application code afterwards.
AUTO_MIGRATE=false

The framework’s auto-migrator never drops columns, but it isn’t aware of your release strategy — splitting “deploy” and “migrate” into two steps lets you stage them deliberately.

Health probes

HealthCheckDB: true enables a real probe — GET /health calls db.Ping() with a HealthTimeout budget. Kubernetes:

livenessProbe:
    httpGet:
        path: /health
        port: 8080
    periodSeconds: 10
    timeoutSeconds: 5
readinessProbe:
    httpGet:
        path: /health
        port: 8080
    periodSeconds: 5
    timeoutSeconds: 5

Set HealthTimeout (default 3s) shorter than the probe’s timeoutSeconds so the handler can return a clean 503 before the probe gives up.

terminationGracePeriodSeconds on the pod should be longer than Config.ShutdownTimeout, otherwise Kubernetes will SIGKILL the process before in-flight requests have finished. With the defaults (30s shutdown), 60s grace is a comfortable buffer.

Logging and tracing

The JSON handler above turns every line into a structured record that an aggregator can index. ctx.Logger() automatically adds request_id and trace_id per request, so a single trace can be reconstructed end-to-end.

Set Config.ServiceName so every log line and every audit record carries the service identifier — invaluable when a single aggregator collects logs from several services.

For a debugging spike, enable pipeline tracing:

TRACE=1
if envStr("TRACE", "") != "" {
    cfg.Trace = maniflex.PipelineTrace{Enabled: true, Skips: true}
}

Steps, Timings, and Aborts produce DEBUG-level records that show every middleware enter/exit and the file:line of every Abort call. Disable in normal operation — they are high-volume.

The Dockerfile

A typical Dockerfile for the binary:

FROM golang:1.25-alpine AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -o /out/bookstore ./

FROM gcr.io/distroless/static-debian12
COPY --from=build /out/bookstore /bookstore
COPY static /static
EXPOSE 8080
USER 65532:65532
ENTRYPOINT ["/bookstore"]

CGO_ENABLED=0 works because maniflex/db/postgres uses lib/pq (pure Go) and nothing else in the framework requires a C toolchain.

static/ is copied so the Scalar OpenAPI viewer is reachable at /static/openapi.html.

Production checklist

Setting
Databasemaniflex/db/postgres with WriteURL and optional ReadURL
MigrationsAutoMigrate: false + external migration tool
LoggerJSON handler
Config.ServiceNamethe service name
Config.QueryTimeoutbounded (e.g. 30s)
Config.ShutdownTimeoutmatches the slowest legitimate request
Config.HealthCheckDBtrue
K8s terminationGracePeriodSecondslarger than ShutdownTimeout
TLSterminated at the load balancer
Authauth.JWTAuth with an asymmetric key from your IdP
Rate limitsdb.RateLimit on password-reset / sign-up / login
Audit logdb.AuditLog on OpCreate / OpUpdate / OpDelete
File storageswap LocalStorage for S3 / R2 / GCS
Outbox workerrun alongside the API, or as a separate deployment

Where to go from here

The tutorial finishes here. From this point, the reference pages cover everything in more depth, and the code base is small enough to grow in any direction:

The shape of main.go has not changed in ten parts. Add models, add middleware, add actions — the wiring is the same.