Deploying BEAM software is unlike deploying almost anything else. You do not ship a jar, a binary or a container built from a Dockerfile that runs pip install. You ship a release: a directory containing your applications, a generated boot script, a sys.config, a vm.args, and — when built correctly — the entire Erlang runtime, so the target machine needs no Erlang installed at all. The three tools that build releases have very different momentum: rebar3 at 1,818 stars with a push on 2026-09-14, relx at 693 stars (last push 2026-03-24) as the library rebar3 drives, Mix releases built directly into Elixir (26,665 stars, pushed 2026-09-20), and Distillery at 2,953 stars — the most-starred of the four and abandoned since August 2024.
That last row is the trap: the most popular option is the one you should not use in 2026. Here is the release-engineering picture, with configuration taken from each project’s own documentation.
TL;DR: Quick Verdict
Writing Erlang? Use rebar3 with relx — it is the only combination with an active 2026 release line and first-class relup support. Writing Elixir? Use mix release — it is part of the language, and every tutorial that tells you to add Distillery is dated. Never run both release systems in one project, and never ship a release built with dev_mode or without ERTS included, because the whole point of a release is that it is self-contained.
Feature Comparison at a Glance
| Dimension | rebar3 + relx | relx standalone | Mix releases | Distillery (legacy) |
|---|---|---|---|---|
| GitHub stars | 1,818 (rebar3) | 693 | part of Elixir (26,665) | 2,953 |
| Last repository push | 2026-09-14 | 2026-03-24 | 2026-09-20 | 2024-08-04 |
| Target language | Erlang/OTP | Erlang/OTP | Elixir | Elixir (and Erlang) |
| Config file | rebar.config | relx.config / relx term in rebar.config | mix.exs :releases key | rel/config.exs |
| ERTS bundling | {include_erts, true} | {include_erts, true} | On by default for production | include_erts: true |
| Start scripts | `bin/ | foreground` | generated by relx | `bin/ |
| Hot upgrades (relup) | Yes, rebar3 relup | Yes | Not supported | Partial, unmaintained |
| Tarball for ops | rebar3 as prod tar | relx tar output | release archive supported | mix distillery.release --tar |
| Status in 2026 | Active, recommended | Library, used via rebar3 | Active, part of Elixir | Superseded — do not adopt |
Decision Matrix: Pick by Use Case
| Your situation | Recommended path | Why |
|---|---|---|
| Erlang service, containerised | rebar3 + relx, mode prod | Self-contained release, tarball artifact, no Erlang needed in the image |
| Elixir service, containerised | mix release | Built in, no extra dependency, --path outputs straight into a build stage |
| Need zero-downtime hot upgrades | rebar3 + relup (Erlang) | Mix releases do not support relup; Erlang’s relx tooling does |
| Existing Distillery project | Migrate to mix release | Distillery has had no commits since August 2024 and its maintainer documentation points at Mix |
| Library that must work under both | Neither release system | Publish on Hex, let the consuming application own the release |
| Kubernetes with per-pod node names | Either, with vm.args.src templating | The node name must be unique per pod; hard-coding it in vm.args breaks clustering |
CI that only runs mix test | Add a release build to CI | A release that fails to assemble is a production outage you can catch in a pull request |
What a BEAM Release Actually Contains
A release is a directory with a deterministic layout: bin/ holding generated start scripts, lib/ holding your compiled applications plus their dependencies, releases/<version>/ holding sys.config, vm.args and the release metadata, and — when ERTS is included — erts-<version>/. Two files decide runtime behaviour. sys.config holds application environment values; vm.args holds emulator flags including the node name and the distribution cookie. Everything painful about BEAM deployment lives in those two files, which is why every mature release workflow templates them instead of writing them by hand.
rebar3: The Erlang Standard, Driving relx
rebar3 is the build tool; relx is the release assembler it calls. You configure releases in rebar.config under a relx key. This block is the shape shown in rebar3’s own documentation:
| |
Those three option lines are exactly the ones teams get wrong. dev_mode, true symlinks your applications instead of copying them — convenient locally, disastrous in production because the release depends on your source tree. include_erts, false means the target host needs a matching Erlang installed; shipping ERTS is the reason releases exist. extended_start_script generates the richer bin/<release> script with foreground, remote_console and pid subcommands.
The production version of that same block, from the same documentation, flips both defaults:
| |
A release definition can also carry per-release options, which is how you keep a slim test release and a full production one in a single rebar.config:
| |
Applications inside a release are declared as tuples, and the optional fields are worth knowing because transient changes failure semantics:
| |
| |
For production builds, tell relx to keep debug information and to treat the build as a real release:
| |
Then the commands are short, and the as prod prefix is how you select the profile that supplies the production options:
| |
Carrying Common Test suites along with the release build is worth the extra profile: the same rebar3 ct invocation that validates your release assembly is the one described in our Erlang testing frameworks comparison of EUnit, Common Test and PropEr.

Templating vm.args and sys.config
Hard-coded node names break containers, so relx supports a source template that is rendered at build time:
| |
The template itself is plain text with {{ }} placeholders — the documented example sets the node name and the distribution cookie:
| |
Generate the pair once with rebar3 release in a scratch run, commit the templates, and let CI substitute per-environment values. Two warnings that cost real outage hours: the -setcookie value is a cluster-wide secret and belongs in your secret manager, not in git; and for a distributed cluster the node name must be reachable — myapp@127.0.0.1 will never cluster with a peer, no matter how correct sys.config is.
You can also trim a release that ships an unwanted module set:
| |
Mix Releases: Elixir’s Built-In Path
Elixir removed the need for a third-party release tool. Releases are declared in mix.exs under the :releases key, and the documented configuration includes a per-release options list:
| |
Key points from Mix’s own documentation: the key inside :releases is the release name and the value is a keyword list; include_executables_for accepts [:unix], [:windows] or []; and every application listed under applications becomes part of the release with the given start type. runtime_tools is a common addition because it pulls in the remote-observability tooling that a production node otherwise lacks.
Build and start, exactly as documented:
| |
bin/my_app start runs the node attached to the current standard input and output, which is what you want under a process supervisor. For a clean artifact, output the release to a directory outside _build:
| |
That two-step — build the release in a build stage, copy the directory into the runtime image — is the whole container story. If you prefer the process to run detached, daemon exists, but on Kubernetes or systemd the foreground variant is the correct choice because the supervisor owns the process lifecycle.
Custom templates for sys.config, vm.args and overlays are generated for you:
| |
That copies the rel/ template directory into your project, which is where you customise the node name and cookie for releases deployed outside containers.
Distillery: The Most-Starred Tool You Should Not Adopt
Distillery has 2,953 stars — more than rebar3 — and its last repository push was 2024-08-04. It solved a real problem in 2016: Elixir had no release story, and Distillery provided one, including a plugin that produced tarballs. Elixir then absorbed the idea into mix release, and the third-party tool stopped being necessary while continuing to accumulate stars from older articles and Stack Overflow answers.
The migration is smaller than it looks. rel/config.exs release definitions become the :releases key in mix.exs; mix distillery.release becomes MIX_ENV=prod mix release; and the include_erts option maps onto Mix’s production defaults (ERTS is included unless you deliberately exclude it). The parts that do not map cleanly are Distillery’s hooks and its tarball plugin: replace the first with documented release steps, and the second with tar plus your artifact store.
If you inherit a Distillery project, do not panic-migrate mid-release-cycle. Run the Mix build in parallel, compare the assembled directories, and switch when a release from the new pipeline has survived a real deploy — the same discipline as any other build-system port.
Pitfalls That Actually Break Deployments
- Shipping
dev_mode, true. The release silently depends on your source checkout. Build production releases withdev_mode, falseand verify the release directory contains real copies, not symlinks. - Forgetting ERTS.
include_erts, falsein a container that has no Erlang gives you a release that starts nowhere. Either bundle ERTS or pin the base image to the exact OTP version and setinclude_ertsdeliberately. - A hard-coded node name.
vm.argswith-name app@127.0.0.1cannot form a cluster in Kubernetes. Template it, and derive the name from the pod’s hostname or an environment variable. - One cookie for everything. The distribution cookie authenticates every cluster member. Rotate it per environment and keep it out of the image, or a single leaked value grants full remote code execution on your cluster.
- Untested relups. Hot upgrades are the reason many teams choose Erlang’s relx pipeline, and they are also the hardest thing to validate. Stage a relup against a production-shaped data set before you trust it.
sys.configthat assumes the working directory. Relative paths insidesys.configresolve differently under a systemd unit than in your shell. Use absolute paths or environment substitution.- Logs going to standard error without a supervisor.
startin the foreground is correct for systemd and Kubernetes, but only if the supervisor is configured to capture output — otherwise you get a release that runs perfectly and reports nothing. - Two release systems in one repo. Distillery and Mix releases side by side produce two artifacts that drift apart. Pick one and delete the other.
- Clustering assumptions that the release does not encode. A release that starts correctly can still fail to join a cluster, because membership and replicated state are application-level concerns — our Erlang distributed state comparison of Mnesia, Ra, Khepri and Partisan covers what has to be configured above the release layer.
FAQ
Do I still need Distillery in 2026?
No. mix release is part of Elixir and covers self-contained releases, tarball-style artifacts and custom runtime configuration. Distillery’s last repository push was 2024-08-04, so new projects should not adopt it.
Can Mix releases do hot code upgrades? No — Mix releases do not support relup, and the documentation says so. If you need zero-downtime upgrades with release handler callbacks, use Erlang’s relx tooling through rebar3, or design for rolling restarts instead.
Which one should a mixed Erlang and Elixir codebase use? Use rebar3 with the Erlang application as the release owner and consume the Elixir library from Hex, or run two separate deployments. Mixing two release tools in one repository doubles the surface area and guarantees drift between the artifacts.
How do I keep the cookie and node name out of git?
Use {vm_args_src, "config/vm_prod.args.src"} with relx, or the rel/ templates from mix release.init, and substitute values at build or deploy time from your secret manager and orchestrator metadata.
Is a release tarball enough for Kubernetes? For most teams, yes: build in CI, publish the artifact, copy the release directory into a minimal image, and start the node in the foreground. What the tarball cannot do is decide your readiness probe — expose a health endpoint and let the orchestrator decide when the node is up, as the server library you chose will define, something we cover in our Erlang HTTP server comparison covering Cowboy, Mochiweb and Yaws.
💰 想测试你的市场判断力?我用 Polymarket 做预测市场交易——这是全球最大的预测市场平台,从大选结果到技术监管时间线,什么都可以押注。和赌博不同,这是真正的信息市场:你懂的信息越多,胜率越高。我靠预测技术相关事件的走向已经赚了不少。用我的邀请链接注册:Polymarket.com