Deployment¶
The binary is one executable with no Python at run time: it reads its configuration from the environment
(the list), listens on HOST:PORT, and talks to PostgreSQL at DATABASE_URL.
This page shows how to build it in a container image and how to run it, alone or next to a Python process for
the routes left to Python (hybrid mode).
Building with Docker¶
The Docker image ghcr.io/larrymotalavigne/py2axum holds Python, py2axum, Rust and the runtime's
precompiled dependencies: use it as the first stage of a multi-stage build, and ship the binary alone (an image
of a few tens of MB on gcr.io/distroless/cc-debian12). The reference files are in
examples/docker, set up for the bookshelf example in hybrid mode:
| File | |
|---|---|
Dockerfile |
translate and compile in ghcr.io/larrymotalavigne/py2axum, then the binary alone on gcr.io/distroless/cc-debian12:nonroot |
Dockerfile.python |
the same application under uvicorn, for the routes left to Python |
compose.yaml |
PostgreSQL, the Python sidecar (not published) and the binary (port 8080, the only entry point) |
docker compose -f examples/docker/compose.yaml up --build # from the repository root
curl -s localhost:8080/health
Adapting the Dockerfile to your application¶
The Dockerfile takes build arguments, so it can often be used as is:
docker build -f Dockerfile \
--build-arg APP_DIR=. --build-arg PACKAGE=api \
--build-arg PY2AXUM_IMAGE=ghcr.io/larrymotalavigne/py2axum:0.8.0 \
--build-arg PY2AXUM_FLAGS="--python-side auto" -t myapp .
APP_DIRis the directory you run uvicorn from (the import root,--root),PACKAGEthe package holding the FastAPI application.PY2AXUM_FLAGS:--python-side autoleaves every route that does not translate to Python; list them explicitly (--python-side '/reports/{id}.pdf') to decide yourself when a route moves to the binary; leave it empty if everything translates (the build then fails on any refused construct, which is what you want in CI).PY2AXUM_IMAGE: pin the py2axum version (X.Y.Z): a new py2axum can translate more routes, or the same ones differently.- Copy your
uv.lock(or requirements file) with the application: library behaviours follow its versions. - The build runs
py2axum checkbefore generating, so the build log lists every route's verdict.
The release profile uses fat LTO and one codegen unit: the runtime's crates are precompiled in the image, but
the final link optimises the whole program and takes a few minutes. The generated crate ships with the
Cargo.lock py2axum is tested with (cargo build --locked). Without Docker, the same steps are
py2axum check, py2axum … -o crate and cargo build --release (first application).
Running it¶
- Command line: uvicorn's options (
--host 0.0.0.0 --port 8000 --proxy-headers --forwarded-allow-ips='*',--log-level,--no-access-log...), so the container command can keep them (Logging). - Environment:
DATABASE_URL,HOST,PORT,PY2AXUM_PYTHON_URL(hybrid mode),PY2AXUM_SHUTDOWN_TIMEOUT, plus your application's own settings; the full list is in Environment variables. The binary does not read.envfiles. - Run database migrations as a separate step (a Kubernetes Job, an init container,
alembic upgrade headin your Python image): the binary does not create tables. - The binary handles SIGTERM like uvicorn (stops accepting, finishes in-flight requests, exits with 143);
keep
PY2AXUM_SHUTDOWN_TIMEOUT(25 s by default) below the orchestrator's grace period.
Hybrid: binary + Python for the rest¶
The binary is the public entry point; the Python container only needs to be reachable from it:
services:
api: # the binary
build: {context: ., dockerfile: Dockerfile}
environment:
DATABASE_URL: postgresql+psycopg://app@db/app
PY2AXUM_PYTHON_URL: http://python:8000
ports: ["8080:8080"]
python: # the same application, for the --python-side paths
build: {context: ., dockerfile: Dockerfile.python}
environment:
DATABASE_URL: postgresql+psycopg://app@db/app
The binary relays matching requests (method, headers, body) and streams the response back. Both processes
must share their settings (a token secret, for instance) and keep shared state outside themselves (database,
Redis). WebSockets are not relayed: a WebSocket route must translate, or be routed to the Python service by
your ingress. If you leave the lifespan to Python (--python-side lifespan), its startup and shutdown code
runs in the Python container only. More in How it works § Hybrid deployments.
Kubernetes¶
The same rules apply in a cluster:
- One container runs the binary; it needs
DATABASE_URLand your application's own settings as environment variables (it does not read.envfiles). - Run migrations as a Kubernetes Job or an init container: the binary does not create tables.
- Keep
PY2AXUM_SHUTDOWN_TIMEOUT(25 s by default) belowterminationGracePeriodSeconds(30 s by default), so that the binary closes its remaining streams itself before the SIGKILL (Graceful shutdown). - In hybrid mode, run the Python application as a sidecar container reachable from the binary only, and set
PY2AXUM_PYTHON_URLto it (for instancehttp://127.0.0.1:8000in the same pod). See Security § Hybrid deployments for the sidecar's--forwarded-allow-ips. - Request bodies are not capped by default: cap them at the ingress or with
PY2AXUM_MAX_BODY(Security § Request input).
A smaller, fully native example with its own Dockerfile is in examples/notes.