DEV Community

loading...

Learn how to deploy Elixir apps on Heroku

ronald_arias profile image Ron ・11 min read

Alt Text

With every app we decide to take out to the world, there's one important decision to make when the time comes.

Where is the best place to deploy it?

The short answer is: "It depends, probably there's no one best place actually".

The long answer however, will depend on what your needs are, your budget, the setup of your current applications, company restrictions etc.

Particularly, I think a really convenient way to deploy your apps in no time is using Platform as a Service (PaaS) options like Heroku.

Heroku is a platform that allows you to abstract yourself from all the complexity that comes from deploying and maintaining an app, and to focus your efforts on the app you want to build. Here's a video from Heroku's Greg Nokes explaining the concept behind their platform.

Heroku has this idea of buildpacks, which transform your code into a slug that can be run on their platform and abstract all the complexity.

They support a small range of popular programming languages using these buildpacks, however Elixir is not one of them.

But not all hope is lost, we have the ability to use custom buildpacks to support it ourselves. And there is one already available that Akash Manohar and his contributors built for us.

If this is all new for you, don't worry, we're going to start putting up the pieces together in a bit.

Let's build a very small app using Elixir that queries an open API and displays the data via HTTP in a JSON format.

Before you get started, you'll need a Heroku account. You can create a new one for free at heroku.com, and there's a free tier we can use for our development projects.

If you're not too familiar with Elixir, you can check my hello-world-elixir article to give you an overview.

This article will be divided into 5 parts

  1. Creating your Elixir app skeleton.
  2. Writing the basic functionality.
  3. Testing it works locally.
  4. Creating your Heroku app.
  5. Deploying and testing your app.

1. Creating your Elixir app skeleton

There are multiple ways to create an Elixir app. You could use one of the available frameworks or you could potentially create the files yourself one by one for example. In this exercise, we'll use the convenient mix new helper that will create most of the initial structure for us.

Go to your project folder on your shell, then type mix new simple_app --sup

❯ mix new simple_app --sup
* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating lib
* creating lib/simple_app.ex
* creating lib/simple_app/application.ex
* creating test
* creating test/test_helper.exs
* creating test/simple_app_test.exs

Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:

    cd simple_app
    mix test

Run "mix help" for more commands.
Enter fullscreen mode Exit fullscreen mode

This will create a basic skeleton for your app, including a module called SimpleApp and a basic supervisor.
Make sure to check your version of Elixir. Syntax and structure might differ slightly on different versions. I'm using 1.11.3 for the following exercise.

Let's go ahead and compile our app first to test the hello method.

cd simple_app
❯ mix compile
Compiling 2 files (.ex)
Generated simple_app app
Enter fullscreen mode Exit fullscreen mode

We can use the interactive console to test manually or just use mix test to use the test suite

❯ iex -S mix
Erlang/OTP 23 [erts-11.1.7]

Interactive Elixir (1.11.3) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> SimpleApp.hello()
:world
Enter fullscreen mode Exit fullscreen mode

By typing SimpleApp.hello() we are invoking the hello method created by the helper and we should get a symbol :world in return if everything is set up correctly.

2. Writing the basic functionality

For this app, we'll use the public API of Coindesk to query the price of Bitcoin today. If you'd rather prefer to use a different API, you can find a list of public ones on this Github repo. The data that we're getting is really not that important for the purpose of this exercise.

Before continuing however, you may want to check the API you choose is operational and to check the format of the returned data. I'd use curl in my shell, but wget, Postman or any other UI client should do the trick.

❯ curl https://api.coindesk.com/v1/bpi/currentprice.json
Enter fullscreen mode Exit fullscreen mode

To integrate the API via Elixir let's use the HTTP wrapper Tesla. There are many good options out there, such as the good old Httpoison. However, Tesla has some added benefits. I won't go into details as it's not the purpose of this article, but it's worth checking out.

To install Tesla, we need to first add the dependency to our mix.exs file

defp deps do
  [
        {:tesla, "~> 1.4.0"},
        {:hackney, "~> 1.16.0"},
        {:jason, ">= 1.0.0"}
        ...
    ]
end
Enter fullscreen mode Exit fullscreen mode

Notice that I also included hackney and jason. We'll use Hackey as the adapter to actually perform our http calls. Tesla uses httpc as the default adapter, but Hackney is far better in many aspects.

We'll need jason too as it's a required JSON parser for our middleware. It's also more performant than other JSON parsers so it's good to have anyway.

Go back to your shell and install the newly added dependencies

❯ mix deps.get
Enter fullscreen mode Exit fullscreen mode

You may not have hex installed in your system yet, so if you get prompted an option to do it, just say yes.

The last part of the Tesla setup is adding the adapter to our config file. If you haven't created the file yet, just put it under config/config.exs.

We can then add the Hackney adapter so your config file should look like this

use Mix.Config

config :tesla, adapter: Tesla.Adapter.Hackney
Enter fullscreen mode Exit fullscreen mode

And that's it, we're done configuring. Throw in a mix compile for good measure, to make sure we're still good to go and we haven't made any typos.

The API calling class

To query the Coinbase API via Tesla, we're going to create a model.

I like to have business logic inside a models folder because it helps me keep things organised on larger projects. Go ahead and create lib/models/coinbase.ex

Inside it we'll need 3 things, our module definition, our Tesla middleware configuration and the method(s) we want to abstract.

defmodule Coinbase do
  @moduledoc """  
  A module abstraction used to interact with the coinbase http API  
  """
  use Tesla

  plug Tesla.Middleware.BaseUrl, "https://api.coindesk.com"
  plug Tesla.Middleware.Headers, [{"Content-Type", "application/json"}]
  plug Tesla.Middleware.JSON

  @doc """  
  Returns the Bitcoin Price Index from coinbase  
  """
  def bpi_current_price do
    get("/v1/bpi/currentprice.json")
  end
end
Enter fullscreen mode Exit fullscreen mode

We can test this right away in our iex console

❯ iex -S mix
Erlang/OTP 23

Interactive Elixir (1.11.3) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> Coinbase.bpi_current_price
{:ok,
 %Tesla.Env{
   __client__: %Tesla.Client{adapter: nil, fun: nil, post: [], pre: []},
   __module__: Coinbase,
   body: ...,
   headers: [
     ...
   ],
   method: :get,
   opts: [],
   query: [],
   status: 200,
   url: "https://api.coindesk.com/v1/bpi/currentprice.json"
 }}
Enter fullscreen mode Exit fullscreen mode

Displaying the data

With our base model done, the last thing we need is something to display the data in an easy to read format.

We are going to be using the light http server Cowboy and the adapter Plug to return our data.

Exploring the different server options and frameworks along with their pros and cons is out of the scope of this article, so we'll stick with the simplest solution.

Let's go back to our mix.exs file to add Plug and Cowboy to our dependencies

 defp deps do
  [
        {:tesla, "~> 1.4.0"},
        {:hackney, "~> 1.16.0"},
        {:jason, ">= 1.0.0"},
        {:plug_cowboy, "~> 2.0"} # NEW!
        ...
    ]
end
Enter fullscreen mode Exit fullscreen mode

Get those deps, compile the project and we're ready to move forward

❯ mix deps.get
❯ mix compile
Enter fullscreen mode Exit fullscreen mode

Now, without caring for organisation too much, let's add a little router file that will expose our endpoint to the world and call the API we've created previously.

Go ahead and create lib/router.ex and define a basic Plug endpoint

defmodule SimpleAppRouter do
  use Plug.Router

  plug :match
  plug :dispatch

  get "/bpi" do
    send_resp(conn, 200, "BPI price coming soon...")
  end

  match _ do
    send_resp(conn, 404, "Wrong place mate")
  end
end
Enter fullscreen mode Exit fullscreen mode

If you're not familiar with it, feel free to check the inner workings of Plug in their documentation. For now, the code above is fairly self-explanatory I hope. All we need to know is that we're using the Plug.Router capabilities and exposing an endpoint /bpi which we're going to use to retrieve our data and to show it.

We can now proceed to tell our supervision tree that our Router is ready to roll.

Go to the lib/simple_app/application.ex where you'll find the application file created by the mix new helper. Here we will ask our application to start the router under the supervision tree.

Your resulting file will look like this

defmodule SimpleApp.Application do
  # See https://hexdocs.pm/elixir/Application.html
  # for more information on OTP Applications
  @moduledoc """
  Application that starts the http router for `SimpleApp`.
  """

  use Application

  @impl true
  def start(_type, _args) do
    children = [
      {Plug.Cowboy, scheme: :http, plug: SimpleAppRouter, options: [port: 8080]}
    ]

    # See https://hexdocs.pm/elixir/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: SimpleApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end
Enter fullscreen mode Exit fullscreen mode

You can go ahead and test it all works fine by running your server with

 mix run --no-halt
Enter fullscreen mode Exit fullscreen mode

To display the Coinbase data, all we need to do is go back to our SimpleAppRouter and add the Coinbase call. Your router function will then look like this

get "/bpi" do
  send_resp(conn, 200, Coinbase.bpi_current_price)
end
Enter fullscreen mode Exit fullscreen mode

If you remember the format of the bpi_current_price function response, you may have noticed that it includes not only the body of the response but also headers, process status etc which we don't necessarily want to display. So to fix this, go back to lib/models/coinbase.ex and modify the function to only return the response body instead

def bpi_current_price do
    {:ok, response} = get("/v1/bpi/currentprice.json")
    response.body
 end
Enter fullscreen mode Exit fullscreen mode

Bear in mind we're not doing any sort of validations or error considerations for the sake of simplicity.

3. Testing it works locally

Now with the app finally ready, we can test it works in our local environment.

Go ahead and run your server again

❯ mix run --no-halt
Enter fullscreen mode Exit fullscreen mode

Then in your browser or curl client, you should be able to obtain the results on http://localhost:8080/bpi

❯ curl http://localhost:8080/bpi
HTTP/1.1 200 OK
...
server: Cowboy

{ ..."chartName":"Bitcoin","bpi":{"USD":{"code":"USD"... }
Enter fullscreen mode Exit fullscreen mode

4. Creating your Heroku app

Before we move entirely into the Heroku app creation, there's one last thing we need to change in the code.

Notice how we used port 8080 as the default, but once in Heroku our app port will be dynamic and determined by them. For this reason, we need to read the port from the environment variables.

To do this, go ahead and update the port 8080 inside the application file with one taken from the system environment variables.

children = [
  {Plug.Cowboy, 
        scheme: :http, 
        plug: SimpleAppRouter, 
        options: [port: (System.get_env("PORT") || "8080") |> String.to_integer()]
  }
]
Enter fullscreen mode Exit fullscreen mode

This has the added benefit of defaulting to 8080 if the PORT environment variable does not exist, which is convenient when using it locally.

To create our Heroku app we're going to be using the heroku-cli. However, if you feel more comfortable using an UI, you can go to your Heroku dashboard and the steps are pretty straightforward.

The first thing you need is a Heroku account, which I'm going to assume you already have. If you don't, just sign up on their website using your preferred email and password.

PRO TIP: If you happen to have an existing Heroku account that you do not want
to use for this exercise, you can use the heroku-accounts plugin to manage
multiple accounts easily.

PRO TIP: If you happen to have an existing Heroku account that you do not want 
to use for this exercise, you can use the [heroku-accounts](https://github.com/heroku/heroku-accounts) plugin to manage
multiple accounts easily. 
Enter fullscreen mode Exit fullscreen mode

To create a new Heroku app using the cli, just go to the root directory of your app in the shell and type

❯ heroku create simple-app-elixir
Creating ⬢ simple-app-elixir... done
https://simple-app-elixir.herokuapp.com/ | https://git.heroku.com/simple-app-elixir.git
Enter fullscreen mode Exit fullscreen mode

Where simple-app-elixir is a unique name you want to give to your app.

Because Heroku doesn't have any default buildpack for Elixir at the time of writing, we need to set our app's buildpack to HashNuke's version of it.

❯ heroku buildpacks:set \
  https://github.com/HashNuke/heroku-buildpack-elixir.git -a simple-app-elixir
Enter fullscreen mode Exit fullscreen mode

For this buildpack to work in production, we need to specify a default configuration on our Heroku app. In the root of the project go ahead and create a file named elixir_buildpack.config and fill it with your versions of the OTP and Elixir.

erlang_version=23.0
elixir_version=1.11
always_rebuild=false
runtime_path=/app
Enter fullscreen mode Exit fullscreen mode

The last thing we need to do is to tell Heroku what process to run when deploying the app. For this we can use a Procfile. In our Procfile we will specify that we want mix to run our app which in turn will run the Cowboy server.

Create another file at the root of your project called Procfile (without any file extension), and add the following inside it

web: mix run --no-halt
Enter fullscreen mode Exit fullscreen mode

Notice this is the same command that we use to run our server locally. The buildpack will actually use this same command as default if no Profile is found, but it's good practice to define it ourselves anyway.

5. Deploying and testing your app

Now we're ready to deploy. Heroku has a git based deployment system, which is really convenient because all we need to do to deploy our app is to git push our code.

In your shell, at the root of your project, go ahead and commit your code to Heroku as if it was another regular git repository.
You may need to add your Heroku remote using your own git URL

 ❯ git remote add heroku [https://git.heroku.com/simple-app-elixir.git](https://git.heroku.com/simple-app-elixir.git)
Enter fullscreen mode Exit fullscreen mode

Yours was printed when creating the app, or it can also be found in the settings of your Heroku app.

# If you haven't initialised the repository yet
❯ git init
Initialized empty Git repository

# Commit your code
❯ git add .
❯ git commit -m "SimpleApp initial commit. Coinbase API"
❯ git push heroku master
Enter fullscreen mode Exit fullscreen mode

This should've deployed your app if your setup is correct, SSH keys are set and there aren't any additional configuration issues.

Once your code is deployed, Heroku will assign a subdomain to your app, something like

https://simple-app-elixir.herokuapp.com/
Enter fullscreen mode Exit fullscreen mode

Depending on the time you're reading this article, mine may or may not be active. Either way, once yours is deployed you should be able to check it out.

You can go ahead and test our /bpi endpoint in the browser or via curl.

❯ curl https://simple-app-elixir.herokuapp.com/bpi
Enter fullscreen mode Exit fullscreen mode

This endpoint should return a json object with the Coinbase Bitcoin exchange rates and your deployment would be complete. Congrats! 👏

Conclusion

As you can see, it's fairly simple to deploy Elixir apps in Heroku. The platform allows for scaling as you go and it's able to handle Elixir's performance requirements very well.

Another advantage of Heroku is that you can have apps in multiple languages running all under the same umbrella platform, which lets you have complex microservices architectures servicing different needs without any additional platform knowledge required.

I've put a full working version of the code on this Github repository, in case you want to compare with it or simply clone it and test it.

For more topics, check my profile or visit rarias.dev.
Happy coding!

Discussion (0)

Forem Open with the Forem app