jonnifyelixir
F7 · Deploy to Fly / F7.1 · the release image

F7 · the release image · module 1

The release image

F6.08.2 produced the deploy artifacts and proved they build; this module turns that build-local release into an image a server can actually boot. A mix release already bundles the app, its dependencies, and the BEAM into one self-contained tree — but a release is not an image. The image is a multi-stage Dockerfile: a builder stage compiles the scoped umbrella release (the Portal apps, never the out-of-band bot) and digests the static assets, and a slim runtime stage carries that release plus the operating-system libraries the BEAM links against. The fault this module exists to retire is quieter than a compile error. The first live boot logged inet_config: file /app/inetrc not found, because the runtime configuration named a file the image never shipped. An image is not done when it builds; it is done when the booted release finds every file its configuration points at.

Three dives: the multi-stage anatomy and the scoped umbrella build; pinning the toolchain so an artifact built in one place runs in the next; and shipping the config files the runtime reads — where each FROM is a clean slate.

From source to a bootable image

The build runs in stages, and each stage exists for one reason. A base stage fixes the toolchain — a Debian image carrying a known Elixir and OTP, with hex and rebar installed. A build stage compiles the release from the umbrella root: it fetches and compiles dependencies for production, copies the Portal apps’ source, digests the static assets, and runs mix release portal. A slim runtime stage then carries only the assembled release and the shared libraries the BEAM needs to start. Select a stage to see what goes in and what comes out.

The multi-stage build · select a stage

BASE — elixir-base a pinned Elixir + OTP image, hex and rebar installed FROM #1 BUILD — elixir-build deps.get · compile · phx.digest · mix release portal FROM #2 RUNTIME — debian-slim copy the release + OTP shared libs · ENTRYPOINT bin/portal start FROM #3

stage: elixir-base

produces: a fixed toolchain

Stages are not decoration: the builder needs the full Elixir toolchain, but the running server needs none of it. Splitting the two is how the shipped image stays small and carries no compiler — only the release and the libraries it links against.

What the image must contain

A release boots on a host with no Elixir installed because the BEAM is bundled inside it. But the BEAM is not the only thing the boot needs. On Fly, the runtime configuration sets ERL_INETRC=/app/inetrc — a path to a resolver-config file that teaches Erlang how to see the platform’s private DNS. If the image never ships that file, the variable points at nothing, and the boot fails before the database is ever contacted. The image must contain every file its runtime configuration names. Toggle whether the COPY is present.

Gotcha 1 · ship the file the config points at

RUNTIME IMAGE · /app bin/portal · lib/ · releases/ — the release openssl · libstdc++ · ncurses — OTP libs /app/inetrc — shipped by COPY boot AT BOOT ERL_INETRC=/app/inetrc resolver loads — Fly DNS is visible the BEAM can resolve .internal names

The variable and the COPY are a cross-file coupling that fails silently: a wrong or missing path fails no build, only the boot. The image is correct when it carries what its configuration promises.

Three deep dives

In order: the multi-stage anatomy and why the build is scoped to Portal; the toolchain pinning that keeps the image, the release, and the dev shell in agreement; and the two clean-slate FROM traps behind shipping a config file to the right path.

F7.1.1
Multi-stage anatomy & the scoped build
The three stages, mix.exs-first layer caching, the umbrella-root build, and why echo_bot is never copied.
F7.1.2
Toolchain pinning & image coherence
One OTP and Elixir version across the image, the release, and .tool-versions — and the trap when a local pin names an erlang that is not installed.
F7.1.3
Shipping config files in
The ERL_INETRC ↔ COPY coupling and the two clean-slate FROM traps: a WORKDIR-relative destination, and --chown with no such user.

F6.08.2 built it

a scoped release that passes build-local — it compiles and assembles into a self-contained tree.

F7.1 ships it

the same release inside an image that boots, carrying every file its runtime config names.

Start with the multi-stage anatomy, then toolchain pinning, then the clean-slate FROM traps. This module is the first rung of F7; once the image boots, F7.2 configures it for Fly’s IPv6 private network. It builds on F6.08 — Auth, deployment & going live, which produced the release this image carries.

References

The multi-stage build, the scoped release, the pinned toolchain, and the resolver-config file the runtime reads at boot.

Sources

Related in this course