Cookbook
Docker and Environment Config
Read settings from environment variables with defaults, build a small container image, and shut down cleanly when the container stops.
This program reads its port, environment name, log level and shutdown timeout from environment variables, falling back to defaults when they’re unset. It ships in a small container image and finishes in-flight requests when the container is stopped. You’d use this shape for any service deployed as a container, where settings come from the platform rather than from flags or files.
The program
Section titled “The program”package main
import ( "context" "fmt" "log/slog" "os" "os/signal" "syscall" "time"
"github.com/0mjs/zinc" "github.com/0mjs/zinc/middleware/logger")
// config holds every setting the service reads from the environment.type config struct { Addr string Env string LogLevel slog.Level ShutdownTimeout time.Duration}
// getenv returns the variable's value, or fallback when it's unset or empty.func getenv(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback}
func loadConfig() (config, error) { cfg := config{ Addr: ":" + getenv("PORT", "8080"), Env: getenv("APP_ENV", "development"), } if err := cfg.LogLevel.UnmarshalText([]byte(getenv("LOG_LEVEL", "info"))); err != nil { return cfg, fmt.Errorf("LOG_LEVEL: %w", err) } timeout, err := time.ParseDuration(getenv("SHUTDOWN_TIMEOUT", "10s")) if err != nil { return cfg, fmt.Errorf("SHUTDOWN_TIMEOUT: %w", err) } cfg.ShutdownTimeout = timeout return cfg, nil}
func main() { cfg, err := loadConfig() if err != nil { slog.Error("invalid configuration", "error", err) os.Exit(1) }
log := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: cfg.LogLevel}))
app := zinc.New(zinc.Config{ShutdownTimeout: cfg.ShutdownTimeout}) app.Use(logger.New(logger.Config{Logger: log}))
app.Get("/", func(c *zinc.Context) error { return c.JSON(zinc.Map{"env": cfg.Env}) })
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop()
log.Info("starting", "addr", cfg.Addr, "env", cfg.Env) if err := app.ListenContext(ctx, cfg.Addr); err != nil { log.Error("shutdown", "error", err) os.Exit(1) } log.Info("stopped")}The Dockerfile
Section titled “The Dockerfile”Put this next to main.go, go.mod and go.sum:
# Build stage: compile a static binary.FROM golang:1.25 AS buildWORKDIR /srcCOPY go.mod go.sum ./RUN go mod downloadCOPY . .RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/server .
# Run stage: only the binary, running as a non-root user.FROM gcr.io/distroless/static-debian12:nonrootCOPY --from=build /out/server /serverEXPOSE 8080ENTRYPOINT ["/server"]Try it
Section titled “Try it”Run it without Docker first. Settings come from the environment:
PORT=3000 APP_ENV=production go run .curl http://localhost:3000/# {"env":"production"}Stop it with kill -TERM <pid> or Ctrl-C, and it logs the request and a clean stop:
{"time":"2026-09-28T01:22:51.416903+01:00","level":"INFO","msg":"starting","addr":":3000","env":"production"}{"time":"2026-09-28T01:22:52.070532+01:00","level":"INFO","msg":"REQUEST","method":"GET","uri":"/","route":"/","status":200,"latency":262958,"host":"localhost:3000","bytes_in":"","bytes_out":21,"user_agent":"curl/8.7.1","remote_ip":"::1","request_id":""}{"time":"2026-09-28T01:22:52.092439+01:00","level":"INFO","msg":"stopped"}A value that doesn’t parse stops the program before it starts serving, with exit status 1:
SHUTDOWN_TIMEOUT=30 go run .# 2026/09/28 01:24:57 ERROR invalid configuration error="SHUTDOWN_TIMEOUT: time: missing unit in duration \"30\""
LOG_LEVEL=loud go run .# 2026/09/28 01:24:57 ERROR invalid configuration error="LOG_LEVEL: slog: level string \"loud\": unknown name"Then build and run the image:
docker build -t zinc-app .docker run --rm -p 8080:8080 -e APP_ENV=staging zinc-appcurl http://localhost:8080/# {"env":"staging"}docker stop <container>How it works
Section titled “How it works”getenvreturns a default when a variable is unset or empty, so the program runs locally with no setup.loadConfigparses every value once at startup and returns an error naming the variable. A typo fails the deploy immediately instead of on the first request that needs the value.Addris":" + PORT, which listens on every interface. Inside a container,localhost:8080would only accept connections from the container itself.signal.NotifyContextcancels onSIGTERM, whichdocker stopsends.app.ListenContextthen stops accepting connections and waits up toShutdownTimeoutfor requests in progress.- The exec form,
ENTRYPOINT ["/server"], runs the binary directly, so it receivesSIGTERMitself. The shell form (ENTRYPOINT /server) would start/bin/sh, which doesn’t pass signals on, and the distroless image has no shell at all.
Before production
Section titled “Before production”docker stopwaits 10 seconds before sendingSIGKILL, the same as Zinc’s defaultShutdownTimeout. SetSHUTDOWN_TIMEOUTa little lower, or give Docker longer withdocker stop -t 30or--stop-timeout.- Add a
.dockerignorelisting.gitand local build output, soCOPY . .doesn’t send them to the build. - Pass secrets as environment variables from your platform’s secret store, and don’t log the loaded config.
CGO_ENABLED=0makes a static binary that the distroless image can run. A driver that needs cgo, such asmattn/go-sqlite3, needs a base image with a C library instead.
See also
Section titled “See also”- Graceful Shutdown: watch a request drain during shutdown.
- Configuration: every
zinc.Configfield, including the server timeouts. - Health and Readiness Checks: endpoints for your orchestrator’s probes.