Runway
Runway is built and operated in the EU!

Elixir/Erlang

First steps

Let’s deploy a 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:

$ cd my_app

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 web server 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.

Create the app on Runway (this also initializes a git repository, if none exists yet), then commit your changes:

$ runway app create
$ git add -A
$ git commit -m "initial commit"

Deploy to Runway

runway app deploy

Run runway app open to see it live, or check the Runway UI. Check runway app logs if something’s off.

Congrats, your Elixir app is now on Runway! 👊👊👊