Runway

Elixir/Erlang

Let’s deploy an small HTTP service written in Elixir on Runway!

Setup

Create a new project for your Elixir app. In this example, the module is called Example, which you have to take into account with the code presented further down:

$ mix new --module Example my_app

Then go into the my_app directory, initialize a git repository (git init).

Elixir uses mix to maintain dependencies, so let’s add what we need — most notably Cowboy (an HTTP-framework for Erlang/Elixir) into the mix.exs file:

defmodule Example.MixProject do
  use Mix.Project

  def project do
    [
      app: :example,
      version: "0.1.0",
      elixir: "~> 1.14",
      start_permanent: Mix.env() == :prod,
      deps: deps()
    ]
  end

  def application do
    [
      extra_applications: [:logger],
      mod: {Example.Application, []}
    ]
  end

  defp deps do
    [
      {:plug_cowboy, "~> 2.6"} # Plug with Cowboy server
    ]
  end
end

Then run mix deps.get to download all dependencies.

The app

Define the overall application in lib/example/ directory. example is the module name we selected earlier when we created the project.

The application.ex is the overall entry point for our app and configures the webserver through a PORT environment variable (with a default of 3000):

defmodule Example.Application do
  use Application

  def start(_type, _args) do
    children = [
      # Start the Plug Cowboy HTTP server
      {Plug.Cowboy, scheme: :http, plug: Example.Router, options: [port: port()]}
    ]

    opts = [strategy: :one_for_one, name: Example.Supervisor]
    Supervisor.start_link(children, opts)
  end

  # Fetch the port from the environment or default to 3000
  defp port do
    try do
      System.get_env("PORT") |> String.to_integer()
    rescue
      _ -> 3000
    end
  end
end

… followed by the route definitions and the handlers to send a response to a client. Create/open (lib/example/) router.ex and insert the following:

defmodule Example.Router do
  use Plug.Router
  use Plug.Logger

  plug :match
  plug :dispatch

  get "/" do
    send_resp(conn, 200, "Hello")
  end

  get "/hello-world" do
    send_resp(conn, 200, "Hello World")
  end

  match _ do
    send_resp(conn, 404, "Oops, not found!")
  end
end

Finally, let’s configure the logger outside the lib/example directory and create config/config.exs with the following contents:

use Mix.Config

config :logger, :console,
  format: "[$level] $message\n",
  metadata: [:request_id],
  level: :info

You created/edited the following files:

Now that all is configured and setup, let’s continue with the build.

Build

To build our application, we use the following multi-stage Dockerfile which we’ll add to the root of the repository:

FROM elixir:1.17-alpine AS build

WORKDIR /app

# Install build dependencies
RUN mix local.hex --force && mix local.rebar --force

# Copy the mix files and install dependencies
COPY mix.exs mix.lock ./
RUN mix deps.get

# Copy the source code and compile the application
COPY lib ./lib
RUN MIX_ENV=prod mix release

FROM elixir:1.17-alpine

# Set the user and group to nobody:nobody (in numeric)
USER 65534:65534

WORKDIR /app
ENV HOME=/app

# Configure the writable directories
ENV ERL_COOKIE_PATH=/data/.erlang.cookie
ENV ERL_CRASH_DUMP=/data/crash.dump

# Copy the compiled app
COPY --from=build /app/_build/prod/rel/example .

EXPOSE 3000

CMD ["/app/bin/example", "start"]

The build stage will setup the dependencies needed to compile your application and ultimately produce a release in /workspace/_build/prod/rel/example. The next (and final) stage copies the results and runs it.

Deploy to Runway

Create an application on Runway:

$ runway app create
INFO    checking login status                        
INFO    created app "assistant-scientist"                 
create app assistant-scientist: done
next steps:  
* commit your changes  
* runway app deploy  
* runway open

And deploy:

$ runway app deploy && runway open

Congrats, you’re Elixir app is now on Runway! 👊👊👊