Imagine that you’ve just finished building an application in Phoenix, and now, you’re ready to share it with the rest of the world. You might be wondering how exactly to navigate the release environment and where to release your application.
Put simply, you can release your project everywhere by using Docker. Elixir requires some runtime dependencies, mainly Erlang, in order to start up the BEAM. In the Deploying with Releases section of the Phoenix release docs, there is an excellent Dockerfile available that serves as a great starting point. In this tutorial, I’ll dissect this Dockerfile and explain what each step does, enabling you to pick and choose what parts of it you like and maybe what parts you need to change to serve your own project’s needs. Let’s get started!
Just as a teaser, I’ll include Node.js and npm in this post, which are not included in the official Dockerfile in the docs:
ARG ELIXIR_VERSION=1.14.0 ARG OTP_VERSION=25.0.3 ARG DEBIAN_VERSION=bullseye-20210902-slim ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}" ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}"
In the code above, we define each step separately, which will make more sense later on in the tutorial. For now, we define which Elixir version we want to compile with, what Erlang OTP version we want, and what Linux image we want to serve as our workhorse.
We’ll start off by using a builder image:
FROM ${BUILDER_IMAGE} as builder # install build dependencies RUN apt-get update -y && apt-get install -y build-essential git nodejs npm curl \ && apt-get clean && rm -f /var/lib/apt/lists/*_* RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \ && apt-get install -y nodejs # prepare build dir WORKDIR /app
The code above includes most of the dependencies that we’ll need, like Node.js, npm, and cURL. But, if you need additional ones, you can add them here. Keep in mind that not every image will need to add Node.js, so you can remove this step if you want to.
That takes care of the setup. From here on out, it’s all about our application:
# install hex + rebar RUN mix local.hex --force && \ mix local.rebar --force # set build ENV ENV MIX_ENV="prod" # install mix dependencies COPY mix.exs mix.lock ./ RUN mix deps.get --only $MIX_ENV RUN mkdir config # copy compile-time config files before we compile dependencies # to ensure any relevant config change will trigger the dependencies # to be re-compiled. COPY config/config.exs config/${MIX_ENV}.exs config/ RUN mix deps.compile COPY priv priv COPY lib lib COPY assets assets WORKDIR assets RUN node --version RUN npm i -g yarn; yarn set version stable RUN yarn install WORKDIR ../ # compile assets RUN mix assets.deploy # Compile the release RUN mix compile
First, we use mix to install Rebar v3 and Hex. Rebar handles native Erlang libraries, while mix gets our Elixir dependencies; you can compare it with what npm is for Node.js. Then, we copy over our mixfile, which denotes our dependencies for the project, as well as the lockfile and configs from our source code.
We then fetch all the dependencies and compile them. Note that this only compiles the dependencies and not our project files; these are two separate steps. Finally, we copy over our project files:
priv
: Migrations and static fileslib
: Our source codeassets
: Our JavaScript and CSS codeThe next five steps are optional. If you’re using Node.js and npm, change your workdir
to the assets
folder, install the dependencies using either Yarn or npm, and then change the workdir
back to src/
.
At this point, we can deploy our assets, which is a special step that makes all our JavaScript and CSS files ready for deployment.
Next, we compile the rest of our Elixir source code, making every file in our project ready for the final build step, the release build:
# Changes to config/runtime.exs don't require recompiling the code COPY config/runtime.exs config/ COPY rel rel RUN mix release
Notice how we copy over the runtime config after the compilation step. This serves as a good reminder that all the configuration in every other config file is compiled into the release and therefore not changeable at this point. But, the config in our runtime config, as the name suggests, is read at runtime.
RUN mix release
will build a release file that consists of everything we need to run our application.
# start a new build stage so that the final image will only contain # the compiled release and other runtime necessities FROM ${RUNNER_IMAGE} RUN apt-get update -y && apt-get install -y libstdc++6 openssl libncurses5 locales \ && apt-get clean && rm -f /var/lib/apt/lists/*_* # Set the locale RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen ENV LANG en_US.UTF-8 ENV LANGUAGE en_US:en ENV LC_ALL en_US.UTF-8 WORKDIR "/app" RUN chown nobody /app # set runner ENV ENV MIX_ENV="prod" # Only copy the final release from the build stage COPY --from=builder --chown=nobody:root /app/_build/${MIX_ENV}/rel/myapp ./ USER nobody CMD ["/app/bin/server"]
Again, the code above is lifted from the official Phoenix docs, but once again, I’ll clarify where the comments fall short.
Here, we reference our earlier ARGS
, but this time, we only take the Linux image. This in itself makes our runtime Docker image exceptionally smaller.
Another benefit from our earlier setup where we installed node_modules
, mix packages, and so on, is that to run our Elixir app, we only need the binary created from our mix release
step at the end of the build part, reducing our build image size exceptionally.
We allow everyone to touch our app directory, change our user to a severely restricted user, and then run the app.
Although the steps in this article are long, basically, it boils down to the following single command that you can run:
> mix phx.gen.release --docker
The code above will generate a Dockerfile similar to the one we covered in this article, but with some differences. The standard Phoenix project does not use Node.js, and therefore, it does not include the Node.js steps I have included in this Dockerfile.
This Dockerfile serves as a starting point to deploy your Phoenix application, and in this article, I’ve shown how to change it to fit your needs. In this case, we included the npm and Node.js steps. From here, all you need to do is play around and figure out what specifics you need.
The cool thing about having a separate builder and runtime image is that there is no real cost to including too much data in the builder image. Yes, it will slow your build down, but most of these things can be cached in your pipeline, and locally, it is automatically cached. No matter what, your runtime image will be small and quick to release because it is a barebones Linux distribution.
I hope you enjoyed this article, and be sure to leave a comment if you have any questions.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowToast notifications are messages that appear on the screen to provide feedback to users. When users interact with the user […]
Deno’s features and built-in TypeScript support make it appealing for developers seeking a secure and streamlined development experience.
It can be difficult to choose between types and interfaces in TypeScript, but in this post, you’ll learn which to use in specific use cases.
This tutorial demonstrates how to build, integrate, and customize a bottom navigation bar in a Flutter app.