F7 · runtime config · module 2
Config at boot
F7.1 put every file the configuration names inside the image. This module is about what that
configuration reads when the release starts. config/runtime.exs is evaluated
once at boot: it resolves the secret key, the public host, the port, and the network bind from the environment, and
it raises when a required value is absent — so a misconfigured deploy fails loudly instead of
starting with an insecure default. Two faults live in this module, and both surface as the identical
Postgrex :nxdomain crash: a private name that resolves only for a running, same-org
instance, and — the decisive one — ECTO_IPV6 set in
fly.toml yet never consumed in runtime.exs, so Postgrex
asks for an IPv4 address of a host that has only an IPv6 one. The lesson under all of it: setting an environment
variable configures nothing the application does not also read.
Three dives: which configuration layer resolves which value, and why the split falls where it does;
Fly’s IPv6-only private network and the set-versus-consume trio that one variable teaches the hard way; and
the two private-name schemes — .internal and .flycast
— with the one command that tells an unreachable host from a wrong address family.
Runtime versus compile
A release decides its configuration at two distinct moments. Some values are fixed when the image is
built — they never vary per deploy, so they are baked into config/prod.exs
at compile time. Others are known only when the release boots on its target machine — a
secret, the public host, the port — so they are read from the environment in
config/runtime.exs, which is evaluated once at startup. The rule that sorts them is
simple to state: a value that is secret or varies per deploy belongs at runtime; a value fixed at build belongs at
compile time. Select a configuration value to see which layer owns it and why.
Two configuration layers · select a value
value: SECRET_KEY_BASE · layer: config/runtime.exs
No secret reaches compile-time config, so the image carries none; and the server?
gate is read at boot, not frozen at build, precisely so a bin/portal eval migration run
does not bind a port.
Resolving Fly’s private DNS
Fly’s private network is IPv6-only. Before the BEAM can reach the database over it, three independent
settings must agree — and one of them teaches the difference between setting an environment variable
and consuming it. ERL_INETRC and ERL_AFLAGS take effect
the moment they are set, because the BEAM runtime reads them with no application code involved.
ECTO_IPV6 is different: setting it in fly.toml does nothing on
its own — runtime.exs must read it into the repository’s
socket_options. Omit that one line, and Postgrex issues an IPv4 (A) query for a host that
has only an IPv6 (AAAA) record, and the boot crash-loops on :nxdomain — the same
error an unreachable host gives. Select a variable to see where it is set, what consumes it, and whether setting
it is enough.
The IPv6 trio · set is not consume
A variable named in fly.toml configures the application only where the
application reads it. ERL_INETRC and ERL_AFLAGS are read by the
runtime; ECTO_IPV6 is read only by runtime.exs. Remove that read,
and ECTO_IPV6=true becomes a silent no-op.
Three deep dives
In order: what runtime.exs resolves at boot against what
prod.exs bakes at build; the three settings Fly’s IPv6-only network needs and the
one that must be consumed by hand; and the two private-name schemes with the single command that isolates an
unreachable host from a wrong address family.
runtime.exs reads at boot against what prod.exs bakes, the config_env() != :test guard, the server? gate, and why no secret lives at compile time.ERL_INETRC / ECTO_IPV6 / ERL_AFLAGS trio, AAAA-only names, and the set-versus-consume distinction that makes ECTO_IPV6 the decisive fix.getent hosts test that tells an unreachable host from a wrong address family when both crash with the same :nxdomain.F7.1 shipped the file
the image carries /app/inetrc, so the resolver loads and the BEAM can see Fly’s private DNS.
F7.2 reads the config
runtime.exs consumes ECTO_IPV6 so Postgrex resolves the AAAA-only host over IPv6 and connects.
Start with the runtime-vs-compile split, then Fly’s IPv6 trio, then the resolution discriminator. This module is the second rung of F7; once the config resolves the database host, F7.3 brings the database itself live and migrated. It builds on F6.08 — Auth, deployment & going live, which produced the config split this module completes.
References
The boot-time config layer, the runtime-versus-compile split, the socket option that carries the IPv6 address family, and Fly’s IPv6-only private network.
Sources
- Phoenix — Deploying with releases —
runtime.exs, the boot-time configuration, and thePHX_SERVERgate. - Elixir — Config —
config_env/0and the runtime-versus-compile split a release evaluates. - Postgrex — start_link options —
socket_options, where[:inet6]selects the AAAA address family. - Fly.io — Private networking — the IPv6-only 6PN,
.internalnames, and AAAA-only resolution. - Erlang/OTP — Inet configuration — what
ERL_INETRCpoints at and how the resolver loads it.
Related in this course
- F7.2.1 · The runtime-vs-compile split
- F7.2.2 · Fly’s IPv6-only private network
- F7.2.3 · .internal vs .flycast & the discriminator
- F7.1 · The release image — ships the
inetrcfile this config reads. - F7 · Deploy to Fly — the chapter overview and the deploy ladder.