Deployment: Containers, TLS, and safe releases

This guide walks from running Stario with the stock CLI to a minimal container and a TLS front so you can ship without inventing a bespoke stack on day one.

Mental model: Stario listens for plain HTTP on the address you configure. TLS termination (HTTPS to clients, HTTP to the app) and trust headers live at the edge (Caddy, nginx, a load balancer). You still point stario serve at the same MODULE:bootstrap and the same in-process request limits as in staging—only the network path changes.

Stario is not ASGI: there is no uvicorn main:app-style ASGI callable in the framework. You run the Stario CLI (stario serve / stario watch) or construct Server in code—the built-in asyncio HTTP/1.1 stack, not Hypercorn/Uvicorn workers.

1. Start with stario serve and a tracer

The supported path is the same entry you use in development: stario serve MODULE:bootstrap. The CLI builds Server, runs your bootstrap, and blocks until shutdown.

Pick the tracer with STARIO_TRACER:

  • json — NDJSON lines on stdout (typical in containers and log aggregators; optional STARIO_TRACERS_JSON_* tuning when STARIO_TRACER=json).

  • sqlite — local SQLite for ad hoc inspection (STARIO_TRACERS_SQLITE* optional overrides; see Getting insights from SQLite tracer).

  • tty — interactive span tree on a terminal (fine on a laptop; rarely what you want in production).

  • auto, or omit STARIO_TRACER — TTY span tree when stdout is a TTY, otherwise JsonTracer (what you usually get under Docker without a TTY).

You can also pass a custom callable as module:callable (zero-argument function returning a Tracer) — see Telemetry — Custom tracers.

bash
STARIO_HOST=127.0.0.1 STARIO_PORT=8000 STARIO_TRACER=json stario serve main:bootstrap

For day-to-day local work, stario watch is still the right loop; for anything you intend to release, prove the same bootstrap under serve with the tracer you plan to run in production (often json or a custom sink, not tty).

Run stario serve --help for the full STARIO_* list (listen, compression, request limits, graceful shutdown). Stario does not load .env files.

2. Why TLS lives in front of Stario

Stario’s HTTP stack serves plain HTTP/1.1 on the listen socket you configure. It does not terminate TLS for you: no automatic HTTPS, no certificates inside the framework. TLS termination means decrypting HTTPS from browsers at the edge and speaking plain HTTP to Stario on a trusted path. That usually belongs at the edge (Caddy, nginx, a cloud load balancer) or on a sidecar.

Put another way: run Stario behind something that speaks HTTPS to clients and forwards HTTP to your process (or connect over a Unix socket on the same host). That edge also gives you stable Host headers, HTTP/2 or HTTP/3 toward the browser while the upstream to Stario stays HTTP/1.1.

3. Minimal Docker image

The shape is: install your app and dependencies, expose the port you bind, and CMD invokes stario serve. Set STARIO_HOST=0.0.0.0 inside the container so traffic from the bridge network reaches the process (the default 127.0.0.1 only accepts local connections).

dockerfile
FROM python:3.14-slim
 
WORKDIR /app
COPY . .
RUN pip install uv && uv sync --frozen
 
ENV PYTHONUNBUFFERED=1
ENV STARIO_HOST=0.0.0.0
ENV STARIO_PORT=8000
ENV STARIO_TRACER=json
 
EXPOSE 8000
CMD ["uv", "run", "stario", "serve", "main:bootstrap"]

Adjust COPY / RUN to match your layout. The important part for safety is: one clear CMD, no shell wrapper unless you need it, and the same MODULE:bootstrap you test with TestClient.

4. Caddy in front (TLS + reverse proxy)

Caddy can obtain and renew certificates automatically. Stario stays on HTTP on localhost or a Docker network; Caddy terminates TLS and reverse_proxy to that upstream.

TCP upstream (Stario listening on 127.0.0.1:8000 on the host or another container):

text
example.com {
    reverse_proxy 127.0.0.1:8000
}

Unix socket upstream (Stario started with STARIO_UNIX_SOCKET=/run/stario/app.sock — see Server):

text
example.com {
    reverse_proxy unix//run/stario/app.sock
}

Tune proxy timeouts if you use long-lived streams (SSE, Datastar)—aggressive idle cuts break long GET streams. In-process graceful shutdown drain is STARIO_GRACEFUL_SHUTDOWN_TIMEOUT (default 5 seconds; total shutdown may exceed that by up to ~1 s during force-close — see Runtime — Shutdown sequence).

5. Request limits on the Stario side

The HTTP stack enforces size caps on every connection—you do not need to reimplement them in each handler.

ControlEnvironment variableDefault (order of magnitude)On violation
Request line + headers (total)STARIO_REQUESTS_MAX_HEADER_BYTES64 KiB431 Request Header Fields Too Large
Headers read stallSTARIO_REQUESTS_HEADER_TIMEOUT5 sConnection closed (no HTTP status)
Request body (total bytes)STARIO_REQUESTS_MAX_BODY_BYTES10 MiB413 Payload Too Large
Body read stalls between chunksSTARIO_REQUESTS_BODY_TIMEOUT30 s408 Request Timeout (when handler reads body)
Keep-alive idleSTARIO_REQUESTS_KEEP_ALIVE_TIMEOUT5 sConnection closed
Pipelining queue depthSTARIO_REQUESTS_MAX_PIPELINED_REQUESTS8 queued (+ 1 in-flight)503 Pipeline queue full

Example:

bash
STARIO_HOST=0.0.0.0 STARIO_PORT=8000 STARIO_TRACER=json \
  STARIO_REQUESTS_MAX_HEADER_BYTES=65536 \
  STARIO_REQUESTS_MAX_BODY_BYTES=10485760 \
  stario serve main:bootstrap

Response security headers (CSP, HSTS, X-Frame-Options, …) are your responsibility in handlers or middleware—Stario does not inject a default security header bundle.

6. Limits, headers, and timeouts on the reverse proxy

Stario does not rewrite Request.host or scheme from X-Forwarded-* automatically—your app sees the Host header the edge sent. Configure proxies so they forward the host and scheme you expect for URL generation and routing.

Putting Caddy (or nginx, Envoy, a cloud load balancer, …) in front is how you add defense in depth: many teams set stricter limits at the edge than inside the app so garbage never reaches Python.

  • Request body size — Caddy can cap bodies before they hit Stario (request_body with max_size, e.g. 10MB). Align max_size with STARIO_REQUESTS_MAX_BODY_BYTES.

  • Timeouts — Configure read, write, and idle timeouts on reverse_proxy / transport. Keep SSE/Datastar routes in mind: long GET streams need generous read-side timeouts.

  • Security headers at the edge — You can set Strict-Transport-Security, X-Content-Type-Options, Referrer-Policy, and related headers in Caddy’s header directive.

7. What is not built in (rate limits, DDoS, WAF)

Stario is an application server, not an edge firewall. By default there is no request rate limiting, per-IP quotas, bot detection, or DDoS mitigation in the framework—those belong in front of Stario (Caddy modules or plugins, your CDN / WAF, cloud load balancer rules, or a dedicated reverse proxy tier).

8. Configuration and environment

Stario does not mandate a single config file format. Common patterns:

  • App environment — read in bootstrap (os.environ, pydantic-settings, etc.) for database URLs, API keys, and feature flags. Pass them with docker run -e, Compose environment:, or your orchestrator’s secrets.

  • Server runtime (STARIO_*) — host, port, tracer, loop, Unix socket (STARIO_UNIX_SOCKET, STARIO_UNIX_SOCKET_MODE), listen backlog (STARIO_BACKLOG), graceful shutdown (STARIO_GRACEFUL_SHUTDOWN_TIMEOUT), request limits, compression. See Configuration and stario serve --help.

  • Bootstrap — keep secrets and clients in bootstrap lifetime; avoid globals that ignore configuration.

Document the exact stario serve … command and STARIO_* values next to your image tag so releases are reproducible.