Deployment

One process speaks HTTP. Put TLS in front of it and tell it where to log.

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.main

Use 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 8000
CMD ["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.

WhatVariableDefaultOn violation
Request line and headersSTARIO_REQUESTS_MAX_HEADER_BYTES64 KiB431
Headers read stallSTARIO_REQUESTS_HEADER_TIMEOUT5 sconnection closed
Request bodySTARIO_REQUESTS_MAX_BODY_BYTES10 MiB413
Body read stallSTARIO_REQUESTS_BODY_TIMEOUT30 s408
Keep-alive idleSTARIO_REQUESTS_KEEP_ALIVE_TIMEOUT5 sconnection closed
Pipelined requestsSTARIO_REQUESTS_MAX_PIPELINED_REQUESTS8 queued429

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 bootstrap your tests run.

  • STARIO_HOST=0.0.0.0 inside the container; the proxy is the only thing on the public port.

  • STARIO_TRACER=json or sqlite, 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.main command is written down next to the image tag.