Simon Bundgaard-Egeberg I work as a lead developer at a consultancy called it-minds. My primary work has been in React, but I'm now working full-time on an Elixir/Phoenix project.

How to run a Phoenix application with Docker

4 min read 1233

Run Phoenix Application Docker

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!

Getting 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 DEBIAN_VERSION=bullseye-20210902-slim

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.

The build step

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 | bash - \
  && apt-get install -y nodejs
# prepare build dir

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
# 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 files
  • lib: Our source code
  • assets: Our JavaScript and CSS code

The 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.

The runtime step

# start a new build stage so that the final image will only contain
# the compiled release and other runtime necessities
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
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.

Get setup with LogRocket's modern error tracking in minutes:

  1. Visit to get an app ID.
  2. Install LogRocket via NPM or script tag. LogRocket.init() must be called client-side, not server-side.
  3. $ npm i --save logrocket 

    // Code:

    import LogRocket from 'logrocket';
    Add to your HTML:

    <script src=""></script>
    <script>window.LogRocket && window.LogRocket.init('app/id');</script>
  4. (Optional) Install plugins for deeper integrations with your stack:
    • Redux middleware
    • ngrx middleware
    • Vuex plugin
Get started now
Simon Bundgaard-Egeberg I work as a lead developer at a consultancy called it-minds. My primary work has been in React, but I'm now working full-time on an Elixir/Phoenix project.

Leave a Reply