F6.08 · part 2 of 3
Releases & config
A release is how an Elixir app ships. mix release assembles
your compiled code, every dependency, and the ERTS — the Erlang runtime, the BEAM itself
— into one self-contained directory that runs on a server with no Elixir or Erlang installed. It comes with
a launcher, bin/portal, that can start the system, open a remote
console, or run a one-off command. The subtlety that trips people is configuration timing.
config/config.exs and config/prod.exs are read at
compile time and baked into the artifact — good for fixed settings, wrong for secrets, which would
then live in your build. config/runtime.exs is different: it is evaluated every time
the release boots, on the server, so it can read environment variables for the database URL, the secret key
base, and the host. That single distinction is most of release config. The last wrinkle is migrations: a release
has no mix, so mix ecto.migrate is unavailable. Instead you add
a tiny Portal.Release module and invoke it with
bin/portal eval, which boots only enough of the app to run the migrator.
Four parts: what a release is, the compile-time-versus-runtime config split, reading the
environment in runtime.exs, and running migrations without mix.
Release, runtime, command
Three ideas. A release is the self-contained artifact. runtime.exs
is the config evaluated at boot. A release command runs code on the server without
mix. Select one.
Releases · select one
idea: mix release
is: a self-contained artifact
Because the BEAM ships inside the release, the production image needs no language runtime — only the operating system. That is what makes Elixir releases small to deploy and easy to roll back: one directory, one launcher.
Compile time vs boot time
Config comes in two flavours separated by when it runs. config.exs and
prod.exs are evaluated while you build and are frozen into the artifact — fine
for things that never change, unsafe for secrets. runtime.exs is evaluated when the
release boots on the server, so it sees the real environment and can pull secrets from it. Same syntax,
different moment.
build (baked in) → artifact → boot (reads env)
The rule of thumb is simple: if a value differs between environments or must stay secret, it
belongs in runtime.exs read from the environment; if it is the same everywhere and safe
to commit, compile-time config is fine.
Reading the environment
runtime.exs guards production settings with config_env()
and reads each secret with System.fetch_env!/1, which raises if the variable is
missing — so a misconfigured deploy fails loudly at boot rather than limping along. The Repo gets its
URL, the Endpoint its host and secret key base.
# config/runtime.exs — evaluated when the release boots, on the server import Config if config_env() == :prod do database_url = System.fetch_env!("DATABASE_URL") config :portal, Portal.Repo, url: database_url, pool_size: 10 config :portal, PortalWeb.Endpoint, url: [host: System.fetch_env!("PHX_HOST"), port: 443, scheme: "https"], secret_key_base: System.fetch_env!("SECRET_KEY_BASE") end
Failing hard on a missing variable is the point of fetch_env! over
get_env: you would rather the release refuse to start than serve traffic without a
database URL or with a blank secret.
Migrating without mix
Migrations are a build-tool task, but a release has no mix. The standard answer is a
small Portal.Release module that loads the app and runs the Ecto migrator for each
repo. You invoke it with bin/portal eval, which starts only enough of the system to
execute the function — the same migrations from F6.03, run on the server.
defmodule Portal.Release do @app :portal def migrate do load_app() for repo <- repos() do {:ok, _, _} = Ecto.Migrator.with_repo(repo, fn repo -> Ecto.Migrator.run(repo, :up, all: true) end) end end defp repos, do: Application.fetch_env!(@app, :ecto_repos) defp load_app, do: Application.load(@app) end # on the server, with no mix: bin/portal eval "Portal.Release.migrate()"
fixed config
Compiled into the artifact at build time from prod.exs.
secrets & migrations
runtime.exs reads env at boot; a release command migrates.
Next: deploying to production — the release is built and configured; now build, migrate, and boot it, clustered across nodes.