Dockerless, Elixir Web Application using Podman and Plug

2023-06-06

Demonstration of how to containerize an Elixir web app. Rootless, for security, inside of Podman.

Also we'll be using the Plug module to keep things simple. This module is at the heart of popular Elixir web frameworks such as Phoenix.

Plug lives at the heart of Phoenix's HTTP layer, and Phoenix puts Plug front and center.


Setup:

First, we need Podman so head on over to the official instllation instructions and then come back.

Next we need an image that we can install Elixir on.. I'll use Alpine Linux.

podman search alpine --filter is-official

Go ahead and pull it:

podman image pull docker.io/library/alpine

Check that it installed:

podman image list alpine

Now run it (more networking options here):

# -p host-port:container-port 
podman run -dit --name alp --network=bridge -p 7070:7070 alpine /bin/ash

Connect to container and install Elixir:

Make sure the container is running first:

podman ps 

If you don't see any output then you must start the container:

podman start alp

Attach to container:

podman attach alp

You should now be logged in as root (don't worry this root can't do harm outside of the container)

Add a user that has sudo (inside the container):

adduser alpine

# enter a password for this user

# you could also change root's password with this command: passwd

Install the sudo program:

apk add sudo

Use visudo to allow the group wheel to use sudo:

visudo

# uncomment the line
# %wheel ALL=(ALL:ALL) ALL

You'll need to know a few vim commands to use visudo. If you really don't know then search up a quick tutorial. Hey you might end up liking vim.

Now add the new user to the wheel group:

adduser alp wheel && su alp

You should be the alp user now and not root.

Install Elixir:

sudo apk add elixir && cd ~

Create an Elixir web app:

mix new myapp --sup

We'll now install the Plug dependency:

Edit mix.ex

vi mix.ex

# and make sure deps has plug as shown below
defp deps do
  [
    {:plug, "~> 1.14"},
    {:plug_cowboy, "~> 2.0"}
  ]
end

Now install plug:

mix deps.get 

# You'll be asked to install hex. Type Y

Great!

Now let's make the app fault tolerant by having the application supervise it:

Edit lib/myapp/application.ex

def start(_type, _args) do
    children = [
	{Plug.Cowboy, scheme: :http, plug: Myapp, options: [port: 7070]}
    ]

Lastly we'll add a router to the web app, Edit lib/myapp.ex

defmodule Myapp do
use Plug.Router

	# matches a route
	plug :match
	# then forwards it to a dispatch 
	plug :dispatch

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

	get "/hello/:name" do
		send_resp(conn, 200, "Hi #{name}")
	end

	# 404
	get _ do 
		send_resp(conn, 404, "404 not found")
	end
end

Start it up:

mix run --no-halt

Open up your browser and head over to localhost:7070/hello/world


Build an image from it

Now we should compile the web app into a binary and also create an image from the container to make it portable.

First let's compile the code.

In the container's terminal run:

MIX_ENV=prod 
RELEASE_NAME=myapp
mix release.init

compile:

mix release

You should now have a binary located at:

_build/dev/rel/myapp/bin/myapp

You can run it as so:

_build/dev/rel/myapp/bin/myapp start

We can now exit the container so that we can build an image with this binary installed.

exit && exit

In your host terminal run:

podman commit alp alp:v2

Now we can start up the new container with the binary running by default:

podman run -dit --network=bridge -p 7070:7070 --name alp2 alp:v2 /home/alpine/myapp/_build/dev/rel/myapp/bin/myapp start

Wrap up

The main benefits of using Podman over Docker are security related. I won't rehash it all here so if you want to learn more check out linode's explanation.

I suggest learning more about plug and its router from elixirschool.