jonnifyelixir
F7.1 · release image / F7.1.3 · clean-slate FROM traps

F7.1 · dive 3 of 3 F7.1.3

Shipping config files in

The image must carry every file its runtime configuration names. On Fly, echo/fly.toml’s [env] sets ERL_INETRC = "/app/inetrc", and the image must COPY infra/inetrc /app/inetrc to put a file there. That env-var path and that COPY destination are a cross-file coupling, and the coupling breaks silently: a wrong destination fails no build, only the boot. The first live boot logged inet_config: file /app/inetrc not found, because the file landed somewhere the variable did not point. Behind that one symptom are two Docker traps, both consequences of a single fact — each FROM is a clean slate. The runtime stage inherits nothing from the build stage: not a working directory, not a user.

Two traps in echo/Dockerfile’s runtime stage: a WORKDIR-relative destination that lands the file at the wrong path, and a --chown to a user the slim stage never created. Toggle both and read the outcome.

The coupling that breaks silently

Two files name the same path. echo/fly.toml sets ERL_INETRC = "/app/inetrc" — the path Erlang’s resolver reads at boot to learn how to see Fly’s private DNS. echo/Dockerfile must place a file there with COPY infra/inetrc /app/inetrc. Neither file checks the other. If the destination drifts by one segment, docker build still succeeds and the image still ships; the fault surfaces only when the booted release evaluates ERL_INETRC, finds nothing at the named path, and logs inet_config: file /app/inetrc not found. The build is green, the boot is dead.

The contents of echo/infra/inetrc — the resolver order {lookup, [native, file, dns]}, Fly’s fdaa::3 nameserver, and {search, ["internal"]} — are F7.2’s depth. This dive is about getting the file to the path, not what the file says.

Two files, one path · the coupling

echo/fly.toml · [env] ERL_INETRC = "/app/inetrc" echo/Dockerfile · runtime stage COPY infra/inetrc /app/inetrc must match AT BOOT resolver reads ERL_INETRC’s path finds the file only if both name /app/inetrc

The env var and the COPY are one coupling spread across two files; the build cannot catch a mismatch, so only the boot does.

The COPY placement checker

Both traps follow from the same fact: the runtime stage opens with its own FROM, so it starts from a clean slate. It inherits no working directory and no user from the build stage. Trap A — a COPY infra/inetrc ./inetrc placed before WORKDIR /app resolves the relative destination against Debian’s default working directory, which is /, so the file lands at /inetrc and the env var still points at nothing. Trap B — a COPY --chown=app:app … fails the build with unable to find user app, because the slim runtime stage creates no app user and the release itself was copied unchowned, running as root. Set both toggles and read which of the three outcomes you get.

Set the COPY · read the outcome

placement
--chown
THE COPY LINE COPY infra/inetrc /app/inetrc EVALUATED AT build of the runtime stage file lands at /app/inetrc the absolute, intended path AT BOOT resolver finds /app/inetrc ERL_INETRC’s path exists — boot proceeds the BEAM can reach Fly’s private DNS

verdict: file at /app/inetrc — boot finds it

Only one of the three outcomes ships the file to the path the config names: the COPY after WORKDIR /app with no --chown.

The corrected runtime tail

The runtime stage ends in four lines: set the working directory, copy the release in, copy the config file to its absolute path, then declare the port and the entry point. The two broken variants are shown first, annotated, so the failure mode is visible beside the fix. The COPY infra/inetrc /app/inetrc comes after WORKDIR /app and after the release COPY (a directory copy merges and will not overwrite it), with no --chown — matching how the release itself is copied into a stage that runs as root.

# echo/Dockerfile, runtime stage — the two wrong forms first
# TRAP A: relative dest before WORKDIR — lands at /inetrc (default WORKDIR is /), build still passes
COPY infra/inetrc ./inetrc
WORKDIR /app

# TRAP B: --chown to a user the slim stage never created — build fails: unable to find user app
COPY --chown=app:app infra/inetrc /app/inetrc

# THE RIGHT FORM: after WORKDIR /app, after the release COPY, no --chown
WORKDIR /app
COPY --from=elixir-build /app/_build/prod/rel/portal ./
COPY infra/inetrc /app/inetrc
EXPOSE 4000
ENTRYPOINT ["bin/portal", "start"]

the config names a file

ERL_INETRC = "/app/inetrc" in echo/fly.toml — a fixed, absolute path the boot will read.

the image ships it there

COPY infra/inetrc /app/inetrc after WORKDIR /app, no --chown — the file lands at exactly that path.

That closes F7.1 · the release image: a multi-stage build, a pinned toolchain, and a config file shipped to the path its variable names. Back to the F7.1 hub for the other two dives, or on to F7, where F7.2 takes up what the inetrc says — the {lookup, [native, file, dns]} order, the fdaa::3 nameserver, and the {search, ["internal"]} domain that let the BEAM resolve Fly’s private network.

Sources & related

The two Docker primitives behind the traps, the Erlang variable that names the file, and the Fly private network the file reaches.

Sources

Related in this course