Build a shell-free, distribution-hardened mix release for distroless images.
mix release emits a layered set of #!/bin/sh scripts as the launch
interface (bin/<name> → releases/<v>/elixir → erts-*/bin/erl), all of
which ultimately exec the native erlexec. On a distroless :nonroot image
there is no /bin/sh, so those scripts cannot run.
This library replaces them with a single native launcher (compiled from the
bundled priv/launcher/start.c) that execves the BEAM directly and
pre-starts epmd itself, so the image needs no shell. The launcher is a
multi-call binary installed over the existing bin/<name> entry points,
preserving the interface so nothing downstream changes. It also enforces:
- a fixed command whitelist (server + your declared task verbs) — no
generic
eval, so controlling the container's args can't run arbitrary code; - a pinned distribution port (no ephemeral fallback → firewallable);
- mandatory mutual-TLS distribution (fail-closed without certs);
- a fail-closed cookie strength check.
Usage
Add to your release in mix.exs:
releases: [
my_app: [
steps: [:assemble, &ShelllessRelease.harden/1]
]
]Configure it (all keys optional except :tasks if you want task verbs):
config :shellless_release,
# zero-arg task verbs -> compiled-in expressions (the whitelist).
# These replace `bin/<verb>` shell overlays; each runs non-distributed.
tasks: [
migrate: "MyApp.Release.migrate()",
seed: "MyApp.Release.seed()"
],
# pinned Erlang distribution port (build-time constant; default 24369)
dist_port: 24369,
# require TLS distribution + the cert bundle (default true). When false,
# distribution is cookie-only (only sensible for non-clustered apps).
require_tls: true,
# extra entry-point names to install the launcher at, beyond "server"
# and the task verbs (rarely needed)
extra_entry_points: [],
# remove the generated shell launchers after install (default true)
strip_shell_scripts: trueThe launcher is compiled by this step during mix release, which runs in your
Dockerfile's builder stage (where a C compiler is present). The library itself
ships only source — it never compiles C at deps.compile time.
See ShelllessRelease.EpmdLess for EPMD-less distribution (drops port 4369).
Summary
Functions
Post-:assemble release step. Compiles the launcher, installs it over the
entry points, stages the TLS config, and strips the shell launchers.
Functions
@spec harden(Mix.Release.t()) :: Mix.Release.t()
Post-:assemble release step. Compiles the launcher, installs it over the
entry points, stages the TLS config, and strips the shell launchers.