Skip to content

Deployment

jero is the ASGI app, not the server. Production is: pick an ASGI server, put a reverse proxy in front of it, tell jero its public URL, and configure logging. This page covers each.

Running the app

Any ASGI server works. granian is the recommended default (it's what the benchmarks run), and the granian extra installs both in one step:

uv add "jero[granian]"
granian --interface asgi --workers 4 myapp:app

Each worker is a separate process: wire runs in every one, so every worker builds its own services, connection pools, and background-task queue. Anything that must be shared across workers (a cache, a queue) belongs in an external service, not in process memory.

Host, port, worker count, and request timeouts are all server configuration — see granian's docs. jero adds nothing to configure on the request path.

Behind a reverse proxy

Reversed URLs (Location / Link headers) are relative by default — always RFC-valid and free of proxy-host ambiguity. Behind a reverse proxy or load balancer you usually want absolute public URLs instead: the client sees a different scheme/host than your app does, and the proxy may strip a path prefix. Reversed URLs become absolute when either environment variable is set (read once when the app is constructed — no code change):

Variable Effect
JERO_BASE_URL A static public origin (e.g. https://api.example.com, may include a prefix). Absolute against it, with no header trust — safest when your origin is fixed.
JERO_TRUST_FORWARDED Truthy (1/true/yes/on). Rebuild the origin per request from X-Forwarded-Proto / -Host / -Port, and restore the stripped path with X-Forwarded-Prefix.

They're mutually exclusive — setting both is a startup WiringError (one source for the base). With JERO_TRUST_FORWARDED=1, a create that reverses to /widgets/w1 — for a request carrying X-Forwarded-Proto: https, X-Forwarded-Host: api.example.com, X-Forwarded-Prefix: /api — emits:

Location: https://api.example.com/api/widgets/w1

(X-Forwarded-For is the client IP — it never shapes a URL, so it plays no part here.) Operation, ref, and from_path(...) links are rewritten against the base; from_url(...) links are left exactly as you wrote them.

Only trust headers behind a proxy you control

Both variables default off, and that matters for JERO_TRUST_FORWARDED: honoring X-Forwarded-* when you are not behind a trusted proxy lets any client spoof X-Forwarded-Host and poison your Location URLs. Setting it is your explicit statement that everything reaching the app comes through a proxy you control. JERO_BASE_URL has no such risk — it's a constant you set, never client input.

JERO_BASE_URL also makes the startup docs-URL log line (OpenAPI & docs) a full, clickable URL.

What belongs in the proxy

Response-body transformation — compression, caching, ETags — is server/proxy work. jero's middleware can answer requests, add headers, and observe; it never rewrites bodies, because that costs the buffering jero refuses to pay. Configure gzip/brotli, caching, and TLS at granian or your reverse proxy.

Graceful shutdown

On lifespan shutdown, jero closes everything you opened through _enter / _aenter in reverse order — even if wire failed partway. Queued background tasks drain best-effort for up to their drain_timeout, then anything left is cancelled and logged; there is no "wait forever", so shutdown can't hang. How long the server waits for in-flight requests before killing the worker is its own grace-period setting.

Logging

jero logs to the jero logger (background-task failures under jero.background). It never configures logging itself — attach handlers like for any library:

import logging

logging.basicConfig(level=logging.INFO)   # or your dictConfig

What arrives there: the docs-URL line at startup (INFO), errors raised inside a stream after the response started (with traceback), failed or unhandled background items, and dropped queued work at shutdown. Request access logs are the server's job — granian has its own.