A simple Elixir webserver

In this article we will create a simple webserver in Elixir, run it locally, then bundle it ready for production.

Ensure that you have Elixir installed. If not, follow the steps for your setup here: https://elixir-lang.org/install.html

Create a barebones project

Run the following command to create a new Elixir project:

mix new simple_server --sup

simple_server is the name of our application.

--sup will provision the project as an Elixir Application. In Elixir, an "Application" is a running program. It has a life cycle where it can be loaded, started and stopped. Components of the application are started by adding them to the application.ex file. You can think of this as the entrypoint to any Elixir program.

You should see the following output:

  mix new simple_server --sup
* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating lib
* creating lib/simple_server.ex
* creating lib/simple_server/application.ex
* creating test
* creating test/test_helper.exs
* creating test/simple_server_test.exs
 
Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:
 
    cd simple_server
    mix test
 
Run "mix help" for more commands.

Let's follow the instructions of the mix command and run the tests in our new application to make sure everything is installed correctly:

cd simple_server
mix test

If successful, we will see the following output:

 mix test
Compiling 2 files (.ex)
Generated simple_server app
..
Finished in 0.01 seconds (0.00s async, 0.01s sync)
1 doctest, 1 test, 0 failures
 
Randomized with seed 57434

Install Plug and Bandit

We will be using two libraries to build our minimal webserver - Plug and Bandit.

Bandit is our http server - it will handle the HTTP socket connections for us.

Plug is a specification for composing web applications in Elixir. A Plug-based web application is a series of plugs, each of which does something with an http-request. Plug makes it easy for us to read and set the http headers, body & more. Its very similar to WSGI/ASGI in Python, or Rack in Ruby.

Add Plug and Bandit to the dependencies in mix.exs

defp deps do
  [
    {:bandit, "~> 0.7.7"},
    {:plug, "~> 1.14"}
  ]
end

The latest versions are currently 0.7.7 and 1.14 respectively, but you can check the latest versions on hex.pm and use those.

Fetch the dependencies using

mix deps.get

You should see something like the following in your terminal:

  mix deps.get
Resolving Hex dependencies...
Resolution completed in 0.064s
New:
  bandit 0.7.7
  hpax 0.1.2
  mime 2.0.5
  plug 1.14.2
  plug_crypto 1.2.5
  telemetry 1.2.1
  thousand_island 0.6.7
  websock 0.5.2
* Getting bandit (Hex package)
* Getting plug (Hex package)
* Getting mime (Hex package)
* Getting plug_crypto (Hex package)
* Getting telemetry (Hex package)
* Getting hpax (Hex package)
* Getting thousand_island (Hex package)
* Getting websock (Hex package)

Create our simple webserver

Create a new file in the lib/simple_server called router.ex and add the following code:

touch lib/simple_server/router.ex
defmodule SimpleServer.Router do
  use Plug.Router
 
  plug(Plug.Logger)
 
  plug(:match)
  plug(:dispatch)
 
  get "/" do
    send_resp(conn, 200, "Hello, world!")
  end
 
  match _ do
    send_resp(conn, 404, "Not found")
  end
end

use Plug.Router will turn this module into a Plug.Router - It gives us a nice way to match on routes and send responses.

plug(:match) instructs our router to match the incoming requests against the routes defined in the router.

plug(:dispatch) instructs our router to dispatch requests which it matches.

While in this simple example the match and dispatch plugs might feel like an extra complication, practically defining these plugs lets us hook into the router life-cucle by defining actions which can run before match, after dispatch or in-between. For example we might want to read an HTTP Authorization header and validate it before we match the request to a route. This logic could be defined as a new plug before the match plug.

We define a single route which will return the string "Hello World!" with an HTTP 200 response for any GET request at "/" .

We also define a catch-all match clause at the end - any request which does not match HTTP GET / will return an HTTP 404 response with the string "Not found".

Connect our Plug to Bandit

We're almost there! Now we need to tell Bandit to run our SimpleServer.Router as an HTTP webserver.

As mentioned previously, the application.ex file is the entrypoint for all Elixir applications so we will add Bandit and our router to the list of children:

@impl true
def start(_type, _args) do
  children = [
+   {Bandit, plug: SimpleServer.Router}
  ]
 
  opts = [strategy: :one_for_one, name: SimpleServer.Supervisor]
  Supervisor.start_link(children, opts)
end

This tells our Elixir application to start the Bandit webserver and use the SimpleServer.Router as our plug.

We can finally test our server:

mix run --no-halt

If all goes well, we should see the logs of our server running

11:10:14.931 [info] Running SimpleServer.Router with Bandit 0.7.7 at 0.0.0.0:4000 (http)

Visit localhost:4000 in the browser and you should see "Hello World!".

In the server logs we should be able to see request logs

11:13:08.541 [info] GET /
11:13:08.542 [info] Sent 200 in 1ms
11:13:08.665 [info] GET /favicon.ico
11:13:08.669 [info] Sent 404 in 4ms

Create our production build

In Elixir a packaged, production-ready build is called a Release.

Creating our production build is as simple as

mix release

We will see the following output in our terminal:

  mix release
* assembling simple_server-0.1.0 on MIX_ENV=dev
* skipping runtime configuration (config/runtime.exs not found)
 
Release created at _build/dev/rel/simple_server
 
    # To start your system
    _build/dev/rel/simple_server/bin/simple_server start
 
Once the release is running:
 
    # To connect to it remotely
    _build/dev/rel/simple_server/bin/simple_server remote
 
    # To stop it gracefully (you may also send SIGINT/SIGTERM)
    _build/dev/rel/simple_server/bin/simple_server stop
 
To list all commands:
 
    _build/dev/rel/simple_server/bin/simple_server

As the output tells us, this creates a directory in _build/dev/rel/simple_server with everything we need to run the server in production.

One gotcha worth keeping in mind is that the release can only be run on a server with the same operating system as where it was built. So if you want to run your application on a debian-linux server, you will need to run the mix release command on a debian-linux machine.

You can find all the code for this article here on Github: https://github.com/aej/simple-elixir-webserver.