Skip to content

Docker image

ghcr.io/larrymotalavigne/py2axum is a builder image: Python, py2axum, a pinned Rust toolchain and the runtime's Rust dependencies, already compiled. It turns your FastAPI application into a binary without installing anything on your machine or in CI, and it is the first stage of a multi-stage build whose final image holds the binary alone.

Tag
X.Y.Z one py2axum release (0.8.0); pin this in production builds
X.Y the latest patch release of a minor version (0.8)
latest the latest release

Each tag is a multi-arch image (linux/amd64, linux/arm64) built by GitHub Actions from the release published on PyPI, with an SBOM and a provenance attestation (docker buildx imagetools inspect ghcr.io/larrymotalavigne/py2axum:latest --format '{{ json .SBOM }}').

What is inside:

  • Python 3.14 (the transpiler must run on a Python at least as recent as your application's) and py2axum;
  • Rust 1.99 (rustc, cargo), gcc for linking;
  • the runtime's crates, compiled with the release profile py2axum generates (fat LTO, one codegen unit) into CARGO_TARGET_DIR=/opt/py2axum/target: a build compiles your application's crate and links, nothing else;
  • Debian bookworm (glibc 2.36): the binaries run on bookworm, trixie, Ubuntu 22.04 and later, and gcr.io/distroless/cc-debian12;
  • a non-root user, py2axum (uid 1000), working directory /app.

Build a binary with docker run

Mount the directory you run uvicorn from on /app, then name the application package:

docker run --rm -v "$PWD:/app" ghcr.io/larrymotalavigne/py2axum:0.8 build app --python-side auto
native       GET /books                     app/routers/books.py:39
...
py2axum-build: dist/app (15M, 126 s)

build runs py2axum check (it stops with file:line when a route neither translates nor is left to Python), translates the package, compiles it, and writes the binary to dist/<name>.

Option
PACKAGE the package holding the FastAPI application (default app)
--name NAME the binary's name (default: the package's directory name)
--out DIR where the binary goes (default dist, relative to /app)
--crate DIR keep the generated Rust crate there (default: a temporary directory)
--no-check skip py2axum check
--python-side, --no-stream, --root, --allow-untested-versions passed to py2axum (command line)

The binary is a Linux executable for the image's architecture: on an Apple silicon Mac it is linux/arm64; add --platform linux/amd64 to build for x86-64 servers (emulated, much slower).

Other commands: docker run … check app --root . is py2axum check, docker run … py2axum --help runs the CLI, and any other command runs as is (docker run -it … bash).

File ownership

The image runs as uid 1000. If your user has another uid on Linux and /app is not writable by uid 1000, run as yourself: docker run --user "$(id -u):$(id -g)" … (the toolchain and the target directory are writable by any user).

Multi-stage build

The reference examples/docker/Dockerfile builds with the image and ships the binary on gcr.io/distroless/cc-debian12:nonroot (glibc, libgcc and CA certificates; no shell, no package manager):

FROM ghcr.io/larrymotalavigne/py2axum:0.8 AS build
COPY --chown=py2axum:py2axum . /app/
RUN py2axum-build app --name app --out /tmp/out --python-side auto

FROM gcr.io/distroless/cc-debian12:nonroot
COPY --from=build /tmp/out/app /usr/local/bin/app
ENV HOST=0.0.0.0 PORT=8080
EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/app"]

Copy your lock or requirements file with the application: py2axum reads the pinned library versions, and library behaviours follow them. The reference file takes build arguments (APP_DIR, PACKAGE, PY2AXUM_FLAGS, PY2AXUM_IMAGE), so it can often be used as is:

docker build -f examples/docker/Dockerfile --build-arg APP_DIR=. --build-arg PACKAGE=api \
  --build-arg PY2AXUM_IMAGE=ghcr.io/larrymotalavigne/py2axum:0.8.0 -t myapp .

compose.yaml, next to it, runs the bookshelf example in hybrid mode: PostgreSQL, the Python sidecar for the routes left to Python, and the binary as the only entry point.

docker compose -f examples/docker/compose.yaml up --build     # from the repository root
curl -s localhost:8080/health

Build time and size

The fat LTO link of the whole program dominates a build: the runtime's crates are precompiled, but LTO optimises them again with your code. Measured on the bookshelf example:

builder image about 700 MB compressed (2.1 GB unpacked on amd64, 2.7 GB on arm64; 1 GB of precompiled crates)
bookshelf build (docker run … build), GitHub-hosted runner (4 vCPU, amd64) 1 min 47, nearly all in the final link
the same, 6 vCPU arm64 VM about 2 min
bookshelf binary 15 MB
final image on distroless (examples/docker/Dockerfile) 39 MB (amd64), 68 MB (arm64) unpacked

Pin X.Y.Z for reproducible builds: a newer py2axum may translate more routes, or the same routes differently. A version's image may be rebuilt (base image fixes), always with the same py2axum, Python and Rust versions.

Building the image yourself

docker/Dockerfile builds the image from a published version or from a checkout:

docker build -f docker/Dockerfile --build-arg PY2AXUM=py2axum==0.8.0 -t py2axum .   # a release
docker build -f docker/Dockerfile --build-arg PY2AXUM=/src -t py2axum .              # this checkout

Build arguments PYTHON_VERSION and RUST_VERSION choose the toolchains.