What follows is a step-by-step guide to set up an Elixir project so that it produces an executable that can be distributed. The executables will run on other Linux machines with the same processor architecture.
We will be using Bakeware.
The Project
I've created an example project
mix help bakeware_example
It's here on GitHub.
Add the Dependency
Add the following to mix.exs
defp deps do
[
...
{:bakeware, runtime: false}
]
end
We set `runtime: false` as the dependency is not part of the runtime application (See the Mix doc for a full explanation).
Update dependencies
mix deps.get
Configure the Release
Add release details to mix.exs:
@app :bakeware_example
def project do
[
app: @app,
releases: [{@app, release()}],
preferred_cli_env: [
release: :prod
]
...
]
end
...
defp release do
[
overwrite: true,
quiet: true,
steps: [:assemble, &Bakeware.assemble/1],
strip_beams: Mix.env() == :prod
]
end
Create the First Release
We can now create a first executable to see where we're at
$ MIX_ENV=prod mix release
...
Bakeware successfully assembled executable at:
_build/prod/rel/bakeware/bakeware_example
Running the executable works, but doesn't produce any output
$ _build/dev/rel/bakeware/bakeware_example
Implement main
We need to implement a function that will get called when the application starts.
Create lib/bakeware_example/cli.ex
defmodule BakewareExample.CLI do
use Bakeware.Script
@impl Bakeware.Script
def main(_args) do
IO.puts "bakeware_example works!"
0
end
end
Configure that as the startup module in mix.exs
--- mix.exs
+++ mix.exs
@@ -16,7 +16,8 @@ def project do
def application do
[
- extra_applications: [:logger]
+ extra_applications: [:logger],
+ mod: {BakewareExample.CLI, []}
]
end
Now when be rebuild and run, we get some output
$ MIX_ENV=prod mix release
...
$ _build/dev/rel/bakeware/bakeware_example
bakeware_example works!
We're not Finished!
Now, if I copy that executable onto a computer with a recent Linux distribution, it will work.
But, if the distribution is a bit older, it fails:
$ ./bakeware_example
./bakeware_example: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.34' not found (required by ./bakeware_example)
The problem is that our executable relies on a more recent version of glibc.
The solution, as explained in the Bakeware README is to build with an older version of glibc.
A Containerized Build
So, let's use a slightly older Linux version in a container to build the executable.
What follows uses Podman but should work exactly the same with Docker.
# Build releases with a recent Elixir, but using an older Linux
# so the executable depends on an older
# (and more widely available) GLibC version.
FROM debian:buster-slim
# Installing the dependency packages takes 90% of the time
# so we do it first to allow quick builds of images
# with varied options.
RUN \
apt-get update && \
apt-get install -y curl libncurses5 procps build-essential zstd unzip locales
# Set up workspace
WORKDIR /app
# Environment for Erlang
ENV ERLANG_URL=https://packages.erlang-solutions.com/erlang/debian/pool
ENV ERTS_RELEASE=24.3.3-1
ENV ERTS_PLATFORM=debian~buster_amd64
ENV PACKAGE=$ERTS_RELEASE~$ERTS_PLATFORM.deb
# Environment for Elixir
ARG ELIXIR_VERSION
ENV ELIXIR_VERSION=$ELIXIR_VERSION
ENV ELIXIR_ZIP=v$ELIXIR_VERSION-otp-24.zip
ENV PATH="${PATH}:/app/elixir/bin"
ENV LC_ALL=en_US.UTF-8
# Set up Erlang and Elixir
RUN \
curl -O $ERLANG_URL/erlang-base_$PACKAGE && \
curl -O $ERLANG_URL/erlang-ssl_$PACKAGE && \
curl -O $ERLANG_URL/erlang-crypto_$PACKAGE && \
curl -O $ERLANG_URL/erlang-public-key_$PACKAGE && \
curl -O $ERLANG_URL/erlang-asn1_$PACKAGE && \
curl -O $ERLANG_URL/erlang-syntax-tools_$PACKAGE && \
curl -O $ERLANG_URL/erlang-inets_$PACKAGE && \
curl -O $ERLANG_URL/erlang-mnesia_$PACKAGE && \
curl -O $ERLANG_URL/erlang-runtime-tools_$PACKAGE && \
dpkg -i *.deb && \
echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && \
locale-gen && \
mkdir elixir && \
curl --output elixir/$ELIXIR_ZIP https://repo.hex.pm/builds/elixir/$ELIXIR_ZIP && \
(cd elixir; unzip $ELIXIR_ZIP) && \
mix local.hex --force && \
mix local.rebar --force
# Install Elixir Dependencies
COPY ../mix.* /app/
RUN mix deps.get
COPY ../lib /app/lib/
We've kept the ELIXIR_VERSION
as a build argument, so we can make it match the project more easily.
$ podman build --file Containerfile --env ELIXIR_VERSION=1.15.5 --tag bakeware_example:latest .
...
Successfully tagged localhost/bakeware_example:latest
$ podman run -v `pwd`:/app/_build/prod/rel/bakeware -e MIX_ENV=prod bakeware_example:latest mix release
...
Bakeware successfully assembled executable at:
_build/prod/rel/bakeware/bakeware_example
Because we set up a volume pwd:/app/_build/prod/rel/bakeware
the executable is actually build in the project root.
It Works!
Running it locally works
> uname -a
Linux framework 5.19.0-43-generic #44~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Mon May 22 13:39:36 UTC 2 x86_64 x86_64 x86_64 GNU/Linux
> ./bakeware_example
bakeware_example works!
As does running it on an older machine
> uname -a
Linux foobar 5.4.0-107-generic #121-Ubuntu SMP Thu Mar 24 16:04:27 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux
> ./bakeware_example
bakeware_example works!
🎉