A Stario app in production is one process running python -m app.main — asyncio.run(serve(bootstrap)) inside. It listens for plain HTTP on the address you give it. TLS, certificates, and the public port usually belong to a reverse proxy in front of it — Stario can also serve TLS itself with STARIO_SSL_CERTFILE and STARIO_SSL_KEYFILE, but certificates, redirects, and the public port are one job, and a proxy does it well. There is no ASGI server to choose and no worker model to tune.
That leaves four decisions: how to build and run the process, what sits in front of it, where the traces go, and what limits to set. Yard makes all four for you. The rest of this page is the manual version, for when you want to see the pieces.
With Yard#
Yard is the host tool built beside Stario. You push a repository; it builds the image, runs it on your server behind Caddy, handles TLS and the environment, and rolls back if the new version does not answer. If you have a VM and a domain, start there. The decisions below are the ones Yard has already made.
1. Run the process#
STARIO_HOST=127.0.0.1 STARIO_PORT=8000 STARIO_TRACER=json python -m app.mainUse the same bootstrap that watchfiles restarted in development and that TestClient runs in your tests. Everything else is environment variables; Configuration lists them, and Stario does not read .env files.
STARIO_TRACER picks where spans go. json writes one line per span to stdout, which is what a container platform collects. sqlite writes to a file you can query; see Traces in SQLite. tty draws a tree and is for a terminal. Left unset, Stario picks tty on a terminal and json otherwise.
2. Build an image#
The image installs the project with uv, sets the server variables, and runs the module. STARIO_HOST=0.0.0.0 is the one setting that differs from a laptop: the default 127.0.0.1 would only accept connections from inside the container.
FROM python:3.14-slim-bookworm AS builder COPY --from=ghcr.io/astral-sh/uv:0.9.24 /uv /uvx /bin/ ENV UV_COMPILE_BYTECODE=1 \ UV_LINK_MODE=copy \ UV_PYTHON_DOWNLOADS=0 WORKDIR /app RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ uv sync --locked --no-install-project --no-dev COPY . /app RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --locked --no-dev FROM python:3.14-slim-bookworm WORKDIR /app COPY --from=builder /app /app ENV PATH="/app/.venv/bin:$PATH" \ PYTHONUNBUFFERED=1 \ STARIO_HOST=0.0.0.0 \ STARIO_PORT=8000 \ STARIO_TRACER=json EXPOSE 8000CMD ["python", "-m", "app.main"]Two stages keep the final image free of the build cache. The dependency layer is built before COPY . /app, so a code change does not reinstall packages.
3. Put a proxy in front#
The proxy speaks HTTPS to the world and HTTP, or a Unix socket, to Stario. Caddy obtains and renews certificates by itself, which makes the whole configuration two lines:
example.com { reverse_proxy 127.0.0.1:8000}With a Unix socket (STARIO_UNIX_SOCKET=/run/stario/app.sock):
example.com { reverse_proxy unix//run/stario/app.sock}Three things to know about the proxy. Stario does not rewrite c.req.host or the scheme from X-Forwarded-* headers, so configure the proxy to forward the host you expect. Live pages hold a connection open for minutes, so raise the proxy's idle timeout above the default or SSE will be cut mid-stream. And the proxy can refuse large bodies before they reach Stario; if it does, keep its limit in step with STARIO_REQUESTS_MAX_BODY_BYTES.
4. Set the limits#
Stario ships with limits that suit a public server. Change them only when you have a reason.
| What | Variable | Default | On violation |
|---|---|---|---|
| Request line and headers | STARIO_REQUESTS_MAX_HEADER_BYTES | 64 KiB | 431 |
| Headers read stall | STARIO_REQUESTS_HEADER_TIMEOUT | 5 s | connection closed |
| Request body | STARIO_REQUESTS_MAX_BODY_BYTES | 10 MiB | 413 |
| Body read stall | STARIO_REQUESTS_BODY_TIMEOUT | 30 s | 408 |
| Keep-alive idle | STARIO_REQUESTS_KEEP_ALIVE_TIMEOUT | 5 s | connection closed |
| Pipelined requests | STARIO_REQUESTS_MAX_PIPELINED_REQUESTS | 8 queued | 429 |
Shutdown drains open requests for STARIO_GRACEFUL_SHUTDOWN_TIMEOUT seconds, 5 by default, before the process exits; Runtime has the order of events. Rate limiting, bot detection, and security headers are not Stario's job; put them at the edge or in middleware you write.
5. Keep two kinds of configuration apart#
STARIO_* variables configure the server: address, tracer, socket, limits, compression. Your own variables configure the app: database URL, API keys, feature flags. Read yours in bootstrap, once, and pass what they produce into handlers as Passing dependencies describes. Configuration lists every STARIO_* variable.
Before you ship#
The image runs the same
bootstrapyour tests run.STARIO_HOST=0.0.0.0inside the container; the proxy is the only thing on the public port.STARIO_TRACER=jsonorsqlite, and you know where that output ends up.The proxy's idle timeout is longer than your longest stream.
Secrets arrive as environment variables, not files in the image.
The
python -m app.maincommand is written down next to the image tag.