Deploying azul web apps with the pre-lifted base image
WIP / DRAFT. This page documents a Docker base image (
ghcr.io/fschutt/azul-web-base) that is not yet published. It also depends on two small library changes tracked indocker/web-base/README.md. Treat the speedup numbers as targets.
Why cold starts are slow
When you run an azul app with AZ_BACKEND=web://<host>:<port>, azul does
not ship a hand-written WASM build of itself. Instead, the web backend
lifts the native machine code of the azul library into WebAssembly at
server startup, using an embedded remill-based
lifter. This is what lets the same Rust callbacks run server-side on the
desktop and client-side in the browser without a separate WASM toolchain.
The catch: lifting the entire library is slow — on the order of minutes
for the full layout + cascade dependency graph. That cost lands on the very
first request, which is a poor first experience for hello-world.
The cache azul already keeps
The lifter caches the expensive part — the per-function lift — on disk, keyed by a hash of each function's machine bytes. The same library function always hashes to the same cache entry, independent of which app is running, because the key is the code bytes, not the app. That is the property that makes a shared cache possible: warm it once in CI, ship it, and every app reuses it.
(Today the cache stores the lifted LLVM IR, so a hit skips the single
slowest step — the lift itself — but the app still runs the cheaper
optimize + WASM-link passes. See docker/web-base/README.md for the full
mechanics and the planned change that would persist the final WASM too.)
Using the base image
Build your app's binary as usual, then base your container on the pre-lifted image:
FROM ghcr.io/fschutt/azul-web-base:0.1.0
# Your statically- or dynamically-linked azul app.
COPY target/release/my-app /usr/local/bin/my-app
# Bind the web backend. allow_public=1 is required to bind a non-loopback
# address (the default refuses 0.0.0.0 because the server has no auth on by
# default — add ?auth_token=... if you expose it).
ENV AZ_BACKEND="web://0.0.0.0:8080?allow_public=1"
EXPOSE 8080
CMD ["/usr/local/bin/my-app"]
Pull it directly to inspect:
docker pull ghcr.io/fschutt/azul-web-base:latest
What happens on first request
- The library functions your app touches are found in the baked cache — no multi-minute library lift.
- Only your own code (your
LayoutCallbackand widget callbacks) is lifted, which is seconds, not minutes. - Subsequent requests reuse everything.
The image also carries the lifter toolchain (remill-lift-17, LLVM llc,
opt, llvm-link, wasm-ld) because step 2 still needs it to lift your
callbacks at runtime.
How the cache is laid out in the image
The image bakes the warm cache at /opt/azul/lift-cache and points the
backend at it. Your derived image inherits that, so no extra configuration
is required. If you build your own variant, keep the cache location
consistent between the build-time warm-up and runtime.
Caveats
- The base image pins a specific
libazulbuild. If your app links a different azul version, the byte hashes differ and the cache misses — always match your app's azul version to the base image tag. - The first lift of your own callbacks still happens on the first request. For latency-sensitive deployments, send one warm-up request at container start (a readiness probe works well).
- See
docker/web-base/README.mdfor the load-order / cache-key caveat that must be addressed in the library before the cache hits reliably across arbitrary apps.
Related
- Headless rendering:
headless-rendering - Web backend internals:
internals/web