jonnifyelixir
F7 · Deploy to Fly / F7.2 · runtime config

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

RUNTIME — config/runtime.exs · resolved at boot SECRET_KEY_BASE · PHX_HOST · PORT · bind_ip · PHX_SERVER · DATABASE_URL per-deploy / secret COMPILE — config/prod.exs · baked at build url: [port: 443, scheme: "https"] · cache_static_manifest · event_store adapter fixed at build

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

SET · fly.toml [env] ECTO_IPV6 = "true" a value in the deploy config must wire CONSUMED BY runtime.exs → Portal.Repo socket_options set alone is a no-op — unread, the boot :nxdomain crashes

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.

F7.2.1
The runtime-vs-compile split
What 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.
F7.2.2
Fly’s IPv6-only private network
The ERL_INETRC / ECTO_IPV6 / ERL_AFLAGS trio, AAAA-only names, and the set-versus-consume distinction that makes ECTO_IPV6 the decisive fix.
F7.2.3
.internal vs .flycast & the discriminator
The two private-name schemes and the cross-machine 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

Related in this course