Dockerize Phoenix + Tailwind application

Manjunath ReddyManjunath Reddy
6 min read

Preamble

I'm hoping you already have a Phoenix application running If you happen to hop here directly.

If not, you may visit the previous two parts where we went through Creating a simple phoenix application, and how to set up Tailwind with phoenix.

Build a production-ready app and run it locally

In this article, I'm directly going to jump into setting up the application to make it production ready by

  • Testing it locally in production mode (env=prod)

  • Run the app in production mode to ensure our build process is correct,

  • And then use the same instructions to dockerize the application

There is an excellent series written by Miguel Cobá on how to prepare Phoenix application deployment with Elixir Releases and also, official elixir release documentation.

However, the main reason I'm writing this article is that I bump into some obstacles to configure a Dockerfile to build the production-ready application. Here, I will go through the issues I have had in detail.

Setup, Compile In prod mode

// Initial setup
$ mix deps.get --only prod
$ MIX_ENV=prod mix compile

With this, you may notice that there is a new folder created under _build/ called prod

// Compile assets
$ MIX_ENV=prod mix assets.deploy

// lets test the build with production mode
$ PORT=4000 MIX_ENV=prod SECRET_KEY_BASE=$(mix phx.gen.secret)  mix phx.server

// You will see the following instructions
╰─$ PORT=4000 MIX_ENV=prod SECRET_KEY_BASE=$(mix phx.gen.secret)  mix phx.server
00:21:54.461 [info] Running DockerPhoenixTailwindWeb.Endpoint with cowboy 2.9.0 at :::4000 (http)
00:21:54.468 [info] Access DockerPhoenixTailwindWeb.Endpoint at http://example.com

If you are wondering about the env variable SECRET_KEY_BASE which is a secret Phoenix uses to sign and encrypt important information.

Head over to http://localhost:4000 and ensure that the application is running.

Generate a release for a production-ready build

This is the step which I missed during my entire process of preparing a release and almost spent a day and a half to figure it out. Before generating a release, we need to uncomment the following in /config/runtime.exs

config :docker_phoenix_tailwind, DockerPhoenixTailwindWeb.Endpoint, server: true

Essentially, it instructs Phoenix to start the webserver from the release.

Once the above line is uncommented, run the following command to generate the actual release

╰─$ MIX_ENV=prod mix release                                                                        
* assembling docker_phoenix_tailwind-0.1.0 on MIX_ENV=prod
* using config/runtime.exs to configure the release at runtime
* skipping elixir.bat for windows (bin/elixir.bat not found in the Elixir installation)
* skipping iex.bat for windows (bin/iex.bat not found in the Elixir installation)

Release created at _build/prod/rel/docker_phoenix_tailwind

    # To start your system
    _build/prod/rel/docker_phoenix_tailwind/bin/docker_phoenix_tailwind start

Once the release is running:

    # To connect to it remotely
    _build/prod/rel/docker_phoenix_tailwind/bin/docker_phoenix_tailwind remote

    # To stop it gracefully (you may also send SIGINT/SIGTERM)
    _build/prod/rel/docker_phoenix_tailwind/bin/docker_phoenix_tailwind stop

To list all commands:

    _build/prod/rel/docker_phoenix_tailwind/bin/docker_phoenix_tailwind

As shown above, the release is created at _build/prod/rel/docker_phoenix_tailwind/bin/ folder,

Let's run it and see if the release works

╰─$ _build/prod/rel/docker_phoenix_tailwind/bin/docker_phoenix_tailwind start
ERROR! Config provider Config.Reader failed with:
** (RuntimeError) environment variable SECRET_KEY_BASE is missing.
You can generate one by calling: mix phx.gen.secret

    /Users/manju/Codes/github/docker_phoenix_tailwind/_build/prod/rel/docker_phoenix_tailwind/releases/0.1.0/runtime.exs:17: (file)
    (elixir 1.14.2) src/elixir.erl:309: anonymous fn/4 in :elixir.eval_external_handler/1
    (stdlib 4.2) erl_eval.erl:748: :erl_eval.do_apply/7
    (stdlib 4.2) erl_eval.erl:492: :erl_eval.expr/6
    (stdlib 4.2) erl_eval.erl:136: :erl_eval.exprs/6
    (elixir 1.14.2) src/elixir.erl:294: :elixir.eval_forms/4
    (elixir 1.14.2) lib/module/parallel_checker.ex:107: Module.ParallelChecker.verify/1
    (elixir 1.14.2) lib/code.ex:425: Code.validated_eval_string/3

That did not work?

It's because we need to make sure to run the above command with a SECRET_KEY_BASE a secret which is required in production mode to encrypt important information. Let's try it again

╰─$ SECRET_KEY_BASE=$(mix phx.gen.secret) _build/prod/rel/docker_phoenix_tailwind/bin/docker_phoenix_tailwind start
00:41:29.927 [info] Running DockerPhoenixTailwindWeb.Endpoint with cowboy 2.9.0 at :::4000 (http)
00:41:29.928 [info] Access DockerPhoenixTailwindWeb.Endpoint at http://example.com

Head over to the browser and test http://localhost:4000/. The application now is running through a release executable (As we will do the same in production)

Let's sum up all the commands

$ mix deps.get --only prod
$ MIX_ENV=prod mix compile
$ MIX_ENV=prod mix assets.deploy
$ MIX_ENV=prod mix release
$ SECRET_KEY_BASE=$(mix phx.gen.secret) _build/prod/rel/docker_phoenix_tailwind/bin/docker_phoenix_tailwind start

It's been quite a journey to get the phoenix app release to make it work locally. In the following section, let's prepare a Dockerfile with all the instructions we went through so far.

Let's Dockerize

Initially, I followed the Phoenix release documentation to generate the Dockerfile automatically and try it build the image, and run the container, however, the container did not run due to incorrect instructions to build the assets and also missing SECRET_KEY_BASE and it took me a couple of hours to debug the Dockerfile.

Then I stumbled upon two issues

  • Assets weren't compiling (Tailwindcss) and therefore website was broken

  • Was running docker container without SECRET_KEY_BASE

To address the above issues, I slightly modified the Dockerfile instructions to build the assets correctly. This is instructed quite well in the Dockerfile comments, which I did not give much attention to.

We are not going to explore the entire Dockerfile instruction, I hope the comments are self-explanatory.

An important change to notice from the auto-generated Dockerfile is to ensure asset compilation is after copying lib

Setup Asset compilation correctly

In the Dockerfile, we need to make sure to setup asset compilation correctly so that our tailwind is working correctly.

Before.

Notice that, in the following Docker instructions, we COPY the assets and then we immediately compile the assets, Which was causing assets not getting minified correctly.

# Copy assets
# note: if your project uses a tool like https://purgecss.com/,
# which customizes asset compilation based on what it finds in
# your Elixir templates, you will need to move the asset compilation
# step down so that `lib` is available.
COPY assets assets
# Compile assets
RUN mix assets.deploy

# Compile project
COPY lib lib

RUN mix compile

# Copy runtime configuration file
COPY config/runtime.exs config/

# Assemble release
COPY rel rel
RUN mix release

After

Moved instruction RUN mix assets.deploy after, COPY lib lib instruction. Otherwise, assets (JS and CSS) won't be properly minified.

# Copy assets
# note: if your project uses a tool like https://purgecss.com/,
# which customizes asset compilation based on what it finds in
# your Elixir templates, you will need to move the asset compilation
# step down so that `lib` is available.
COPY assets assets

# Compile project
COPY lib lib

# IMPORTANT: Make sure asset compilation is after copying lib
# Compile assets
RUN mix assets.deploy

RUN mix compile

# Copy runtime configuration file
COPY config/runtime.exs config/

# Assemble release
COPY rel rel
RUN mix release

Build the image and run it from a container

Copy and create the Dockerfle in the project root directory.

Build the image

╰─$ docker image build -t elixir/docker_phoenix_tailwind .

Run the container

╰─$ docker run -e SECRET_KEY_BASE="$(mix phx.gen.secret)" -p  4000:4000 elixir/docker_phoenix_tailwind
00:17:50.396 [info] Running DockerPhoenixTailwindWeb.Endpoint with cowboy 2.9.0 at :::4000 (http)
00:17:50.396 [info] Access DockerPhoenixTailwindWeb.Endpoint at http://example.com

Now head over to http://localhost:4000. We now have an application running from the container which you could use to deploy in production.

The main Dockerfile is based on Debian bullseye, however, I have also experimented with an alpine image which I have explained in the README.

The entire application source is here

For now, that completes the series. However, in the future, I would like to explore deploying by containerising the application on different platforms such as Digitalocean, and Fly.io.

2
Subscribe to my newsletter

Read articles from Manjunath Reddy directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Manjunath Reddy
Manjunath Reddy

Software Engineer with over a decade of experience in building monolithic to microservices with PHP, Java (Spring), JavaScript (Node, Vue), Erlang/Elixir (Phoenix), Search engineering (Solr, Elasticsearch), Cloud (AWS).