Deploy
The release build, the image with no interpreter in it and what it measures of itself, Fly.io.
A Mojo application ships as the binary and the Mojo runtime libraries
beside it. m0 new writes deploy/Dockerfile, deploy/fly.toml and a
.dockerignore; m0 image builds the image.
The release build
uv run m0 build --release
This compiles for the platform's baseline CPU rather than the machine doing
the build, rewrites the binary's library search path to its own directory,
and bundles the runtime into dist/. dist/ is relocatable: copy it
anywhere on the same platform and run dist/server. --target-cpu CPU
compiles for something newer; native is refused, because the machine
that builds is rarely the machine that runs.
A build needs a C compiler reachable as cc: mojo build links through
that name and takes no other. m0 build checks for it first, with a link
test, and refuses with exit 78 naming the package to install
(build-essential, or Xcode's command line tools). m0 test needs none.
The image
uv sync # once; commit uv.lock
uv run m0 image # docker build -f deploy/Dockerfile -t NAME .
docker run --rm -p 8080:8080 NAME
m0 image needs docker and a committed uv.lock, and no local toolchain:
the builder stage runs uv sync --frozen and uv run m0 build --release.
The runtime stage copies dist/ onto debian:12-slim and runs the binary
as PID 1, as an unprivileged user. --tag T names the image (the default
is the project directory's name), --target-cpu CPU reaches the release
build, and everything after a bare -- goes to docker build, for example
-- --platform linux/amd64.
What about.json proves
The image's last layer measures the image from inside and writes
/app/about.json; m0 image prints it as its last line.
{"app":"shop","version":"0.1.0","arch":"x86_64","cpu":"x86-64-v2","base":"debian:12-slim","python":false,"app_bytes":2953336,"image_bytes":102047098}
| key | source |
|---|---|
python |
always false: the layer searches the filesystem for an interpreter and fails the build if it finds one |
cpu |
what the builder's release build said it compiled for, not what was asked |
app_bytes, image_bytes |
du over /app and over the whole filesystem, unpacked |
version |
the project's pyproject.toml |
The views scaffold's /app is about 3 MB and the image about 102 MB
unpacked, nearly all of it the Debian base. A cold build spends about 10 s
on apt, 5 s on the frozen sync and 13 s compiling.
CI builds this image from a freshly scaffolded project on x86-64 Linux on
every pull request, reads about.json, checks PID 1, probes the
application through a published port and requires docker stop to exit 0
(N31 in Capabilities).
Fly.io
fly apps create NAME
fly deploy -c deploy/fly.toml --remote-only
fly scale count 1 -a NAME
--remote-only. Fly's machines are x86-64. An image built on an Apple Silicon laptop is built under emulation, and the Mojo compiler does not survive it.scale count 1. The first deploy creates two machines. An application whose state lives in the process has one machine's state on each.- One loop. On one shared vCPU a second worker or loop has no core to
run on, so the scaffold's
fly.tomlleavesM0_WORKERSandM0_THREADSunset. - The scaffold's
fly.tomlcounts connections rather than requests, never stops the machine, and sets a 25-second SSE heartbeat: a held stream is one connection for its whole life, and a quiet one needs traffic to stay open through the proxy.
The health check is GET /health, which both scaffolds answer on the event
loop.
Behind a proxy
The server speaks HTTP/1.1 and no TLS; terminate TLS in front of it.
SIGTERM drains and exits 0 within five seconds, which is inside
docker stop's default grace. The host has the shutdown
rules and every environment variable the binary reads.
https://blobs.m0serve.dev is apps/blobs from the repository, deployed
this way; its footer is its own about.json.