Elixir-Elm project from zero to deployment (using Docker)

This blog post is about my experiences building an Elixir-Elm I started last weekend. It won’t serve as a detailed documentation, but I will share other links to the documentation pages and blog posts I used.

I started to work on a project last week. I cannot tell too much about the details yet, but it is a simple CRUD application using a Postgres database, with some interactive front-end.

I choose to write my front end in Elm simply because I am quite comfortable in it. Also, I do work with React in my day job, but I know I can move much faster and safer with Elm. Even TypeScript cannot give me that kind of freedom of refactoring, that I can achieve with Elm.

I also wanted to try the mdgriffith/elm-ui library. Many people seemed to like it and once I started using it I understood why. It just works… You won’t need to know any CSS tricks, it is just so simple anyone can build a great UI. All the parts work together so well, I cannot actually believe it. I don’t ever want to write CSS again. Huge respect to Matthew Griffith!

For the backend, I had some research earlier. First, I found Haskell really appealing, because of the same kind of type safety. I tried Servant, which is a small framework for building RESTful services. It felt nice, it even generates Elm decoders with mattjbray/servant-elm (unfortunately it is not yet upgraded to work with Elm 0.19). However, maybe it is only my lack of knowledge, but I had some trouble setting up the database connection (my brain is not yet ready for monad transformers). And then I realized that, in case of a simple CRUD service, you almost only deal with side-effects, uncertain responses, database failures, so having a type-safe language almost doesn’t really add so much safety — but please let me know if I’m wrong.

And then I saw that many Elm developers use Elixir. Elixir has a quite different approach to safety. It is actually a dynamically typed language, with high fault tolerance. Your application will consist of a bunch of lightweight processes, and in case one of your processes throw an exception, only that single process will fail, while your whole application keeps running. Plus the so-called Supervisor process will restart that process. It is an interesting approach well suited for the uncertainty of the web.

I used the Phoenix framework to build my REST service and to serve my compiled Elm files. The framework works nice, it looks much like Ruby on Rails — however I myself have never worked with Rails. Database CRUD is easy, the documentation is also nice, I did not experience any problems here.

Since version 1.4 Phoenix uses Webpack to bundle assets. Prior versions use Brunch, so older blog posts might be misleading. Setting up Webpack to work with elm needs to add the elm-webpack-loader. Phoenix also has a template system, which we don’t need, so I just cleared all the unneeded HTML from the template files and added a single div tag to render my Elm app.

Everything was a smooth sailing. To the point of deployment… Of course, it might be my lack of knowledge again. This is an area where my knowledge is the least deep.

But I tried my best. Dockerized my application, used Distillery to create a production build, wrote DB migration and seed scripts, and basically read a whole lot of documentation. The Distillery documentation has a page about Dockerized deployments, the Dockerfile there worked without almost any changes, I only added my migration script to the end, so when my app is deployed, the database is also migrated and ready to use. I also needed to add the /assets/elm-stuff folder to .dockerignore, so my downloaded Elm packages won’t get packed in the image causing trouble when fetching dependencies inside the docker image.

The Twelve-Factor App is a guideline for modern applications. I highly recommend reading it. The third point says that the configuration should be kept in the environment. In our case this means DB host and credentials, authentication secret keys etc. will be set as environment variables on the deployment server. And unfortunately, Elixir caused me some headache and sleep deprivation here.

Elixir is a compiled language, and unfortunately, references to environment variables also get compiled. This means if your environment variables are not present on compile time or change later, your application won’t work properly. In my case, since I had my environment variables set in docker-compose, when I ran my app locally everything worked fine, but once I deployed it to the server, the database wouldn’t connect.

Fortunately, there is a solution. If the environment variable REPLACE_OS_VARS=true is set, all instances of “${VAR}” will be replaced with the value from the environment (strictly speaking, from the sys.config file, which is managed by Distillery). Not so nice, the Distillery documentation also says it is a hack. But this is the only way I managed to make it work.

So that’s it. I had some difficulties but they were not unsolvable, and now I have a working Elixir-Elm app in a Docker image. Since the first deployment I also built my CI/CD pipeline using Gitlab, so when I merge some branch to master my tests and staging deployment runs automatically.

References: