Deploying Rust in under five seconds
Fast Rust deploys come from separating build time from release time: compile once, push a small runtime image or static bundle, then publish by swapping bytes or image refs.
Listen to this article
On-device voiceUses the voice built into your browser; no article text leaves this page.
Audio narration is not supported by this browser.
"Deploying Rust in under five seconds" is not the same thing as "building Rust in under five seconds." A clean cargo build --release on a real service can spend minutes in dependency compilation, LLVM, linker work, and Docker layer export. The deploy path we want on app.nz is narrower: once a release artifact exists, publishing it should be a metadata and routing operation, not a full source rebuild.
That distinction shapes the deploy system.
The two deploy paths
app.nz apps use appnz.yaml as the contract between the CLI and the control plane. The current schema has two runtimes:
| Runtime | What ships | Fast path |
|---|---|---|
static | A directory such as dist/ | Upload changed files, prune removed files, serve from R2/DB |
server | A container image | Build once, push to the private registry, release by image ref |
The static path is already close to the target. app sites deploy <slug> ./dist walks the directory, skips hidden files and common junk, uploads files, updates the DB index, and publishes immediately at both /sites/<slug>/ and <slug>.app.nz. Serving is a single fasthttp route: resolve slug, map the path to a file, fall back to index.html for client-side routes, fetch bytes from R2 when enabled, set the content type, and write the response.
That is why Rust/WASM apps are a good fit: the slow part is trunk build, wasm-pack, or whatever produced the bundle. The deploy is just bytes.
Server Rust is different
For a Rust API, the slow part moves into the image build. The default inferred Dockerfile is intentionally Node-oriented, so serious Rust services should bring their own build.dockerfile. Build Studio accepts an inline Dockerfile plus optional context, runs docker build, logs into registry.app.nz with an isolated Docker config, pushes registry.app.nz/u-<user>/<image>:<tag>, and stores the build row.
That gives us a clean optimization boundary:
- Build the Rust binary once in a builder job.
- Push a small runtime image that contains only the binary and assets.
- Release the server app by pointing the runtime at the already-built image.
- Reverse-proxy
<slug>.app.nzto the warm container.
The release should not run cargo. If it does, it is a build, not a deploy.
What the hot path should look like
For a static Rust/WASM app:
CLI deploy
-> diff local files against site index
-> upload changed bytes to R2
-> upsert hosted_site_files rows
-> request hits <slug>.app.nz
-> fasthttp serves index.html/assets directlyFor a Rust server app:
Build Studio
-> docker build with cached cargo layers
-> docker push to private registry
Release
-> runtime starts or replaces container from image ref
-> slug router proxies to the app portThe under-five-second target applies to the second half of both diagrams: publish and route. Source compilation is deliberately outside that latency budget.
The current speed levers
- No build on static deploy. Static hosting never invokes a builder.
- Tiny request-time router. Hosted-site serving stays in the Go control plane and avoids an external pages service hop.
- Separate origin for real apps.
<slug>.app.nzis its own origin. The path form under/sites/<slug>/is sandboxed because it shares the app.nz origin. - Private registry namespace. User images are namespaced under
u-<short-user-id>, so release can refer to a stable internal image ref. - Isolated Docker auth. Build jobs write Docker credentials into a temp
DOCKER_CONFIG, not the host config. - Secret redaction. Build logs are scrubbed before persistence, because deploy logs become user-facing debugging material.
What we still want to improve
The next obvious wins are build-cache wins, not routing wins:
- prebuild Rust base images with
cargo-chefor a similar dependency-planning layer, - run builders on Hetzner boxes with large persistent cache disks,
- add
sccachefor repeated workspace builds, - split release from build explicitly in the UI,
- reject Dockerfiles that download heavyweight toolchains in the runtime stage,
- show users whether a run was "build time" or "deploy time."
The product lesson is simple: fast deploys come from refusing to do unnecessary work. Rust can be slow to compile, but a compiled Rust service is just a small binary. app.nz should make the release path treat it that way.