This page collects complete deployment-oriented examples. Use it together with the Configuration Reference, which remains the canonical schema and semantics document.
For distributed quota examples and migration guidance, see Distributed Quota.
How To Use These Examples
Use these examples as starting points, not as copy-paste truth for every environment.
- start with the smallest example that matches your deployment shape
- change addresses, certificates, and admin credentials first
- validate operational implications before promoting a local example into production
Example Selection Guide
| If you need... | Start with... |
|---|---|
| a local first run | Example 1 |
| one upstream in production | Example 2 |
| multiple upstreams with different routing | Example 3 |
| multiple listeners with different bind identities | Example 4 |
| downstream client certificate auth | Example 5 |
| a private CA for upstream trust | Example 6 |
| static asymmetric JWT verification | Example 7 |
| remote JWKS validation | Example 8 |
Example 1: Minimal Local Development
version: 1
listen:
address: "127.0.0.1"
port: 9889
tls:
cert: certs/proxy-fullchain.pem
key: certs/proxy-key-pkcs8.pem
upstream:
default:
route:
path_prefix: "/"
backends:
- id: "backend1"
address: "http://127.0.0.1:8080"
upstream_tls:
verify_certificates: false
strict_sni: false
Use this shape for local iteration only. It opts into cleartext upstream traffic explicitly with http://.
Common mistake:
- copying this example into production without restoring upstream TLS verification and stronger admin-surface protection
Example 2: Single-Upstream Production
version: 1
listen:
protocol: http3
address: "0.0.0.0"
port: 443
tls:
cert: /etc/spooky/certs/fullchain.pem
key: /etc/spooky/certs/privkey.pem
upstream_tls:
verify_certificates: true
strict_sni: true
upstream:
default:
load_balancing:
type: round-robin
route:
path_prefix: "/"
backends:
- id: "app-1"
address: "app.internal.example:8443"
weight: 100
health_check:
path: "/health"
interval: 5000
timeout_ms: 1000
failure_threshold: 3
success_threshold: 2
cooldown_ms: 5000
security:
privileges:
enabled: true
user: "spooky"
group: "spooky"
observability:
metrics:
enabled: true
address: "127.0.0.1"
port: 9901
path: "/metrics"
control_api:
enabled: true
address: "127.0.0.1"
port: 9902
auth_token: "replace-with-strong-token"
Use this when:
- one upstream handles most traffic
- you want the simplest host deployment that is still production-oriented
Example 3: Multi-Upstream Production
version: 1
listen:
protocol: http3
address: "0.0.0.0"
port: 443
tls:
cert: /etc/spooky/certs/fullchain.pem
key: /etc/spooky/certs/privkey.pem
upstream_tls:
verify_certificates: true
strict_sni: true
upstream:
api:
load_balancing:
type: consistent-hash
key: "header:x-user-id"
route:
host: "api.example.com"
path_prefix: "/"
backends:
- id: "api-1"
address: "api-a.internal.example:8443"
weight: 100
- id: "api-2"
address: "api-b.internal.example:8443"
weight: 100
web:
load_balancing:
type: latency-aware
route:
host: "www.example.com"
path_prefix: "/"
backends:
- id: "web-1"
address: "web-a.internal.example:8443"
weight: 100
- id: "web-2"
address: "web-b.internal.example:8443"
weight: 100
load_balancing:
type: round-robin
Use this when:
- different hosts or paths must route to different upstreams
- the application needs different load-balancing strategies per upstream
Example 4: Multi-Listener Deployment
version: 1
listen:
protocol: http3
address: "0.0.0.0"
port: 443
tls:
cert: /etc/spooky/certs/public-fullchain.pem
key: /etc/spooky/certs/public-privkey.pem
listeners:
- protocol: http3
address: "0.0.0.0"
port: 443
tls:
cert: /etc/spooky/certs/public-fullchain.pem
key: /etc/spooky/certs/public-privkey.pem
- protocol: http3
address: "10.0.0.10"
port: 8443
tls:
cert: /etc/spooky/certs/internal-fullchain.pem
key: /etc/spooky/certs/internal-privkey.pem
upstream:
default:
route:
path_prefix: "/"
backends:
- id: "backend1"
address: "backend.internal.example:8443"
The top-level listen field is always required by the schema. When listeners[] is non-empty, runtime normalization uses listeners[] and the top-level listen block is superseded.
Common mistake:
- expecting the top-level
listenblock to stay active alongsidelisteners[]
Example 5: Bootstrap Listener Client Auth
version: 1
listen:
protocol: http3
address: "0.0.0.0"
port: 443
tls:
cert: /etc/spooky/certs/fullchain.pem
key: /etc/spooky/certs/privkey.pem
client_auth:
enabled: true
require_client_cert: true
ca_file: /etc/spooky/certs/client-ca.pem
upstream:
default:
route:
path_prefix: "/"
backends:
- id: "backend1"
address: "backend.internal.example:8443"
This is the right shape when bootstrap TLS clients must authenticate with certificates.
Example 6: Private CA Upstream Trust
version: 1
listen:
protocol: http3
address: "0.0.0.0"
port: 443
tls:
cert: /etc/spooky/certs/fullchain.pem
key: /etc/spooky/certs/privkey.pem
upstream_tls:
verify_certificates: true
strict_sni: true
ca_file: /etc/spooky/certs/private-root-ca.pem
upstream:
default:
route:
path_prefix: "/"
backends:
- id: "backend1"
address: "backend.private.example:8443"
Use this when:
- the upstream certificate chain is not rooted in the public Web PKI
- one deployment needs stricter trust control than public default CA bundles
Example 7: Static RS256 And ES256 JWT Keys
Pin verification to public keys you manage yourself. secret stays empty because
HS256 is not in the allowlist — configuring both is rejected at startup.
version: 1
listen:
protocol: http3
address: "0.0.0.0"
port: 443
tls:
cert: /etc/spooky/certs/fullchain.pem
key: /etc/spooky/certs/privkey.pem
upstream:
api_pool:
route:
path_prefix: "/"
auth:
jwt:
secret: ""
issuer: "https://issuer.example.com/"
audience: "payments-api"
allowed_algorithms: ["RS256", "ES256"]
require_kid: true
static_keys:
- kind: pem
kid: "rsa-2026-01"
alg: "RS256"
public_key_pem: |
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...
-----END PUBLIC KEY-----
- kind: pem
kid: "ec-2026-01"
alg: "ES256"
public_key_pem: |
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----
clock_skew_secs: 30
backends:
- id: "backend1"
address: "backend.internal.example:8443"
Keys may also be supplied as JWK documents with kind: jwk and a jwk string field
instead of public_key_pem. RSA keys shorter than 2048 bits are rejected.
Example 8: JWKS-Backed Validation With Strict Policy
Fetch signing keys from the issuer and enforce a strict issuer/audience/algorithm
policy. Multiple issuers and audiences use the plural fields; the singular
issuer/audience fields remain supported but cannot be combined with them.
version: 1
listen:
protocol: http3
address: "0.0.0.0"
port: 443
tls:
cert: /etc/spooky/certs/fullchain.pem
key: /etc/spooky/certs/privkey.pem
upstream:
api_pool:
route:
path_prefix: "/"
auth:
jwt:
secret: ""
issuers:
- "https://issuer.example.com/"
- "https://issuer-eu.example.com/"
audiences:
- "payments-api"
- "payments-api-internal"
allowed_algorithms: ["RS256", "ES256"]
require_kid: true
jwks_url: "https://issuer.example.com/.well-known/jwks.json"
jwks_refresh_interval_secs: 300
jwks_request_timeout_ms: 2000
jwks_cache_ttl_secs: 900
jwks_stale_if_error_secs: 3600
jwks_startup_behavior: require_ready
clock_skew_secs: 30
required_scopes:
- "payments:read"
backends:
- id: "backend1"
address: "backend.internal.example:8443"
jwks_url must be an absolute https URL. Omitting issuers/audiences entirely
disables those checks — signature and expiry are still enforced, but any issuer's
token signed by a trusted key is accepted, so set them in production.
Startup Behavior Choices
jwks_startup_behavior decides what happens when the initial fetch fails:
| Value | Behavior |
|---|---|
require_ready (default) |
Startup fails with an error naming the endpoint and cache state. Use when the upstream must never serve traffic with unverifiable tokens. |
allow_degraded |
The process boots and retries in the background. JWT requests are rejected until keys load. Use when availability of other upstreams matters more than this one being immediately ready. |
Both reject tokens while keys are missing; they differ only in whether the process starts at all.
Example 9: Runtime Activation And Reload Posture
Spooky supports generation-based validation, preview, activation, rollback, and certificate-only reload. When planning operations:
- use
POST /admin/runtime/validateto check a candidate configuration - use
POST /admin/runtime/previewto see the staged diff without touching the running runtime - use
POST /admin/runtime/activateto commit a runtime-managed config change - use
POST /admin/runtime/rollbackto return to a retained runtime generation - use
POST /admin/runtime/reload-certsfor listener certificate replacement on new handshakes - use the legacy
POST /admin/runtime/reloadshortcut only when you intentionally want the older direct-apply behavior without preview - plan a drain-and-restart workflow only for log format/file settings, tracing config, control-plane thread counts, and listener removal or bind-address changes