Containerization ¶
How to build and run Stario apps in containers using Docker or Podman. Both use OCI-compatible images, so the same Containerfile (or Dockerfile) works with either runtime.
Multi-Stage Build ¶
Separate dependency installation from your source code to maximize layer caching. Stario requires Python 3.14+.
# Stage 1: Builder - install dependencies
FROM python:3.14-slim-bookworm AS builder
WORKDIR /app
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/
# Install build deps for C extensions (brotli)
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc build-essential && \
rm -rf /var/lib/apt/lists/*
# Install Python dependencies (cached layer)
COPY pyproject.toml uv.lock ./
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-install-project --no-dev
# Copy source and sync the project
COPY . .
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev
# Stage 2: Runtime - no build tools
FROM python:3.14-slim-bookworm
ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY --from=builder /app /app
ENV PATH="/app/.venv/bin:$PATH"
CMD ["python", "main.py"]
Build and run ¶
# Docker
docker build -t myapp .
docker run -p 8000:8000 myapp
# Podman
podman build -t myapp .
podman run -p 8000:8000 myapp
Tip: Using
uvin Docker gives you dramatically faster builds (up to 10x vs pip), proper lockfile support, and smart layer caching with--mount=type=cache.
Binding: Ports vs Unix Sockets ¶
Port binding ¶
The simplest approach - bind the container's port to the host:
# main.py
await app.serve(host="0.0.0.0", port=8000, workers=4)
docker run -p 8000:8000 myapp
podman run -p 8000:8000 myapp
Use host="0.0.0.0" inside the container so the server listens on all interfaces, not just loopback.
Unix sockets ¶
For production behind a reverse proxy, Unix sockets avoid TCP overhead and port management. Mount a host directory into the container and have the app write its socket there:
# main.py
await app.serve(unix_socket="/sockets/app.sock")
mkdir -p /tmp/sockets
# Docker
docker run -v /tmp/sockets:/sockets myapp
# Podman
podman run -v /tmp/sockets:/sockets:z myapp
The reverse proxy (Caddy, Nginx) then connects to /tmp/sockets/app.sock on the host. See the Reverse Proxy (Caddy) how-to for a complete setup.
Note: The
:zflag on Podman volumes is needed for SELinux-enabled hosts to allow the container to write to the mounted directory.
Docker Compose ¶
services:
web:
build: .
ports:
- "8000:8000"
environment:
- WORKERS=4
- HOST=0.0.0.0
Or with Unix sockets:
services:
web:
build: .
volumes:
- sockets:/sockets
environment:
- WORKERS=4
volumes:
sockets:
Environment-Based Configuration ¶
A common pattern is to switch between TCP (development) and Unix socket (production) based on the environment:
import asyncio
import os
import sys
from stario import Stario, RichTracer, JsonTracer
async def main():
if sys.stdout.isatty():
tracer = RichTracer()
host, port, workers, unix_socket = "127.0.0.1", 8000, 1, None
else:
tracer = JsonTracer()
host, port, workers = "0.0.0.0", 8000, 4
unix_socket = os.environ.get("SOCKET_PATH", "/sockets/app.sock")
with tracer:
app = Stario(tracer)
app.get("/", home)
await app.serve(
host=host, port=port,
workers=workers, unix_socket=unix_socket,
)
if __name__ == "__main__":
asyncio.run(main())
Resource Limits ¶
Set memory and CPU limits to prevent runaway containers:
# Docker
docker run --memory 512m --cpus 1 -p 8000:8000 myapp
# Podman
podman run --memory 512m --cpus 1 -p 8000:8000 myapp
Production Tips ¶
- Use
--restart on-failure:3to automatically recover from crashes. - Pin your base image to a specific Python version (e.g.,
python:3.14-slim-bookworm) for reproducible builds. - Use
.dockerignoreto exclude.git,__pycache__, docs, and dev files from the build context. - Layer ordering matters - copy
pyproject.tomlanduv.lockbefore your source code so dependency installation is cached.
