Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Shipping your own application

The image embeds whatever mix release produces from app/. Replace the sample with your project and rebuild.

What to keep from the sample

Three small pieces in app/ are worth copying into your project:

mix.exs release options:

releases: [
  myapp: [
    include_executables_for: [],      # no bin/myapp shell script, /init starts beam.smp
    include_erts: erts(),             # cross-compiled ERTS from the builder, see below
    strip_beams: true,
    steps: [:assemble, &Myapp.ReleaseSteps.prune/1]
  ]
]

defp erts do
  case System.get_env("UNIAPP_ERTS") do
    nil -> true
    root -> root |> Path.join("erts-*") |> Path.wildcard() |> List.first()
  end
end

UNIAPP_ERTS is set by the Dockerfile to /opt/otp-x86, the cross-compiled OTP tree. Mix then takes both ERTS and the OTP applications from there, which is what makes the release x86-64 even though Mix itself runs natively.

lib/.../release_steps.ex (prune/1) deletes everything the shell-free boot does not need: the release’s bin/, all ERTS binaries except beam.smp and erl_child_setup, sources, includes and docs. scripts/assemble-rootfs.sh refuses to build an image that contains any other ELF file, so keep this step.

lib/.../cmdline.ex reads key=value pairs from the kernel command line (/init copies it into KERNEL_CMDLINE). Use it for runtime configuration instead of environment variables.

Dependencies

Pure Elixir and Erlang dependencies work unchanged. Add the Hex steps to the release stage of builder/Dockerfile:

COPY app/mix.exs app/mix.lock ./
RUN mix local.hex --force && mix local.rebar --force && mix deps.get --only prod
COPY app/config config
COPY app/lib lib
COPY app/priv priv
RUN mix release --overwrite --path /out/release

Dependencies with NIFs do not work: a NIF is a shared object loaded with dlopen, and the image has no dynamic loader. Prefer the pure-Elixir alternative (pbkdf2_elixir instead of bcrypt_elixir, postgrex is fine, exqlite is not) or link the NIF statically into beam.smp the way OTP’s own crypto and asn1 are. The latter means building the NIF as a .a archive and adding it to --enable-static-nifs in the cross OTP stage; it is real work but has been done for OTP’s NIFs already.

Configuration

config/runtime.exs is evaluated at boot inside the image. Keep reboot_system_after_config at its default (false): the release directory is read-only and a reboot would need to write a new sys.config.

Environment variables are what /init sets (RELEASE_*, LANG, TERM, KERNEL_CMDLINE). Anything else must come from the command line via Cmdline.get/2 or be baked into config/config.exs.

Starting without a shell

In app mode the release boots with -noshell and the applications listed in mix.exs start under application_controller. If an application exits, init stops the node, which ends the VM (there is no supervisor above beam.smp). The sample’s echo server therefore retries a failed listen instead of crashing, so that a kernel limitation never takes the image down. Do the same for anything that touches the OS at start.

Build and boot

make initramfs      # ~1 min once OTP and Elixir are cached
make run-m1-app

The release name and version are read by /init from RELEASE_NAME and RELEASE_VSN compile-time defines; if you rename the app from uniapp, update the two #defines at the top of init/init.c (or pass them as -DRELEASE_NAME=... in the Dockerfile).