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
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
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
- Docker — Dockerfile reference: COPY — how
COPYresolves a relative destination againstWORKDIR, and what--chownrequires. - Docker — Dockerfile reference: WORKDIR — the working directory each stage starts from and why a fresh
FROMresets it. - Erlang/OTP — Inet configuration — what
ERL_INETRCpoints at and how the resolver reads the file at boot. - Fly.io — Private networking — the
.internalDNS over the IPv6 6PN that the inetrc teaches the BEAM to reach.
Related in this course
- F7.1 · the release image — the module hub this dive closes.
- F7.1.1 · Multi-stage anatomy & the scoped build — where the runtime stage and the release
COPYcome from. - F7.1.2 · Toolchain pinning & image coherence — the sibling dive on keeping the stages in agreement.
- F6.08 · Auth, deployment & going live — produced the release this image carries.