Dockerize Phoenix + Tailwind application
Dockerise Phoenix, Tailwind application
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 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 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.