Production checklist
A pre-flight list before you put Octopus in front of real traffic. Everything here maps to a feature that actually works today; where a config field exists but is not yet wired, it's called out rather than recommended. For the full picture of what's parsed versus enforced, see the configuration implementation status.
Transport security (TLS)01
Terminate TLS — either on the gateway itself (
gateway.tlswithcert_file/key_file) or at a load balancer / ingress in front of it. Don't serve plaintext to the internet. See TLS configuration.Enable certificate hot-reload if you terminate on the gateway, so rotated certs are picked up without a restart (
enable_cert_reload).Consider mTLS for service-to-service or zero-trust setups (
require_client_certplus aclient_ca_file). See mTLS.Set a minimum TLS version (
min_tls_version, e.g.1.3) appropriate to your clients.On Kubernetes, TLS via cert-manager and Gateway listener Secrets is supported when the operator's
terminate_tlsis enabled — see TLS & cert-manager.
By default Octopus rejects requests whose Host/:authority disagrees with the negotiated TLS
SNI (gateway.enforce_sni_check: true). Only disable this if TLS is terminated by an upstream
proxy and Host legitimately differs from the SNI.
Authentication & access control02
Protect routes that need it with an auth provider — JWT, OIDC, API key, forward-auth, or mTLS. See Authentication.
Protect the admin surface. Set
admin.auth_providerto an auth provider; with none set, the admin dashboard is reachable without authentication.Keep secrets out of the config file. Use
${VAR}substitution and inject JWT secrets, API keys, etc. from the environment (Kubernetes Secrets, your secret manager).
Both admin.auth_provider and admin.allowed_ips gate the admin surface: the allowlist returns
403 to clients whose IP doesn't match an IP / CIDR / range pattern (static assets still bypass
the auth check). Combine them with network-layer controls for defense in depth. See
Admin configuration.
Health probes03
Wire all three probes to the gateway listen port. They're enabled by default and served on the listen port:
startupProbe→/startupz(200 once the listener has bound)livenessProbe→/livez(200 while the process is alive; stays 200 while draining)readinessProbe→/readyz(200 only when ready; flips to 503 on SIGTERM)
Don't point liveness at
/readyz— a draining or not-yet-ready Pod would be killed instead of drained. The Helm chart and bundled manifests already wire these correctly.
See Health probes and, on Kubernetes, Probes & drain.
Graceful shutdown04
Send SIGTERM, never SIGKILL, to trigger draining.
Tune
gateway.pre_stop_delay(default5s) so deregistration propagates before the listener closes.Tune
gateway.shutdown_timeout(default30s) to cover your longest in-flight request.On Kubernetes, keep
terminationGracePeriodSeconds >= pre_stop_delay + shutdown_timeout(the chart and manifests default to45s). Full detail in Graceful shutdown.
Resource limits & runtime hardening05
Set CPU and memory requests and limits. The Helm chart defaults to requests
100m/128Miand limits1/512Mi— review these against your traffic.Run as non-root. The image already runs as
uid 1000; the chart setsrunAsNonRoot, drops all capabilities, disables privilege escalation, and uses a read-only root filesystem.Set
gateway.workersto match the cores you've actually allocated when you impose a CPU limit — see Scaling.Set
gateway.max_body_sizeto bound request bodies (default 10 MB).
Availability06
Run at least two replicas behind a load balancer (the chart defaults to 2). A single instance is a single point of failure.
Configure a shared state backend if you use rate limiting, shared caching, or circuit breaking across replicas — the default in-memory store is per-process. See State and Scaling.
On Kubernetes, keep the PodDisruptionBudget (enabled by default) and spread replicas across nodes/zones with
topologySpreadConstraints.Consider the HorizontalPodAutoscaler for elastic capacity — see Scaling.
Observability07
Scrape
/metricson the gateway listen port (e.g.:8080/metrics). On Kubernetes, enable the chart'sServiceMonitor(metrics.serviceMonitor.enabled=true), which scrapes thehttpport at/metrics.Configure structured logging (
observability.logging, JSON in production) and ship logs to your aggregator. See Logging.Enable tracing if you run a Jaeger/OpenTelemetry collector (
observability.tracing). See Tracing.
Known-inert: the /metrics path is fixed and served on the gateway listen port. The
observability.metrics.endpoint field (the 0.0.0.0:9090 you may see in examples) is not
wired — it does not move or open a second listener — and observability.metrics.enabled does
not gate the endpoint. Treat both as informational and scrape /metrics on the listen
port. See Metrics.
Configuration hygiene08
Validate before rollout:
octopus validate -c config.yaml.Pin the image tag (e.g.
ghcr.io/xraph/octopus:0.1.0), not:latest.Mount config read-only and roll Pods when it changes (the chart annotates Pods with a config checksum so updates trigger a rollout automatically).