Elixir and Phoenix with Docker Tutorial


In this video we are going to cover how to go from ‘zero’ to running the outcome of a mix phx.new hello in Docker.

The reason I put ‘zero’ in speech marks is because I'm going to assume zero as being you have at the very least, got Docker installed. If you have not, please follow the official guide for your OS.

mkdir docker-phoenix && cd $_
touch {Makefile,Dockerfile}

This is a lazy way of saying:

Make me a new directory - docker-phoenix - and change directory into it (cd $_).

Then, once inside, create me two new files:

  • Dockerfile
  • Makefile

We will touch (no pun intended) on both of these as we go through these videos.

To begin with, we need to open the Dockerfile. Here's the starting point:

FROM elixir:1.5.1
ENV DEBIAN_FRONTEND=noninteractive

# Install hex
RUN mix local.hex --force

# Install rebar
RUN mix local.rebar --force

# Install the Phoenix framework itself
RUN mix archive.install https://github.com/phoenixframework/archives/raw/master/phoenix_new.ez --force

# Install NodeJS and the NPM
RUN curl -sL https://deb.nodesource.com/setup_8.x | bash -
RUN apt-get install -y -q nodejs

# Suggested https://hexdocs.pm/phoenix/installation.html
RUN apt-get update && apt-get install -y \
    inotify-tools \
 && rm -rf /var/lib/apt/lists/*

# When this image is run, make /app the current working directory
WORKDIR /app

Whenever we do a docker build against this Dockerfile, a new image will be created for us that is just as if we had created a Virtual Machine inside VMWare, or VirtualBox or similar, and painstakingly installed all these dependencies by hand.

In other words, it's not only a massive time saver, it's also a code-representation of a small part of your larger system architecture. Docker does have a strong upfront cost that you must accept, though the longer term rewards are, in my opinion, worthwhile.

Let's break down what the Dockerfile is telling us:

FROM elixir:1.5.1

We're going to pull from elixir:1.5.1. This is the latest at the time of recording.

This will also give us access to Erlang 20. This is basically trivia to us at this point, as we won't be doing anything beyond the complexity of spinning up the equivalent of "hello, world!".

We can find out this image will give us access to Erlang 20 by reading the Dockerfile for the elixir:1.5.1 release.

This Dockerfile itself is an extension of erlang:20, and so on. Again, trivia.

ENV DEBIAN_FRONTEND=noninteractive

This is an environment variable to automatically accept prompts in the affirmative.

RUN mix local.hex --force

As per the Phoenix installation guide, we must install Hex.

Hex is the package manager for the Elixir and Erlang ecosystem. Think of this as composer, and hex.pm as being the equivalent of Packagist.org.

RUN mix local.rebar --force

Rebar is an Erlang tool that makes it easy to create, develop, and release Erlang libraries, applications and systems in a repeatable manner. Yes, that's right I stole this directly from the Readme.

Actually what we need is Rebar3. However, mix local.rebar will install both Rebar, and Rebar3, which is a touch unintuitive, but does exactly what we need.

Without Rebar / Rebar3 we will get errors on build, particularly around installing a library for handling Mime types. There may be other issues found if Rebar isn't available, but that's as far as I got.

Next, we install the Phoenix framework using the newly installed mix command line tool:

RUN mix archive.install https://github.com/phoenixframework/archives/raw/master/phoenix_new.ez --force

This would be the equivalent of a:

npm install express --save in Node JS, or:

composer create-project zendframework/zend-expressive-skeleton expressive in PHP.

Both Symfony and Laravel differ here as they favour their own custom installers.

Taken directly from the docs:

A Mix archive is a Zip file which contains an application as well as its compiled BEAM files.

If you were diligent and followed the FROM path all the way back to the base image, you will have found that this image is based on Debian.

The next two commands take advantage of this knowledge:

RUN curl -sL https://deb.nodesource.com/setup_8.x | bash -
RUN apt-get install -y -q nodejs

You can open https://deb.nodesource.com/setup_8.x in your browser right now and see what this URL contains. It's a bash script, and we use a unix pipe (|) to take the contents of this command and pass it to bash, which interprets it and as a result, install the very latest version of Node JS. Nice.

You may wish for a more stable approach than this. We are only exploring here, but feel free to tweak up as needed.

With our system now aware of an application called nodejs, we can install it with the appropriate apt-get install command.

Calling Time

Here's the thing.

This process takes ages.

Well, comparatively ages by computer standards.

It's not the sort of thing I want to be doing every time I spin up my dev environment.

In the real world, what I do is build this image and push it up to my private GitLab instance, and then base the image I use in docker-compose.yml from this image.

We fortunately don't need to set up a private GitLab or anything so drastic. Instead, we will use Docker Hub.

If you do not already have Docker Hub credentials, you can sign up for free here.

Signing up will give you a single private repository. You may create as many public repositories as you like.

We will use a public repository as we have nothing to hide. Personally I don't use Docker Hub, as mentioned, I use my own GitLab, so have no further knowledge beyond this.

We're going to want to push (as in docker push) our image up to Docker Hub.

We will initially do this manually:

docker build .

Sending build context to Docker daemon   51.2kB
Step 1/8 : FROM elixir:1.5.1
 ---> 9603b9288585
Step 2/8 : ENV DEBIAN_FRONTEND noninteractive
 ---> Running in a19c852445ba
 ---> 7272f0111930
Removing intermediate container a19c852445ba
Step 3/8 : RUN mix local.hex --force
 ---> Running in 833783e8436b
* creating root/.mix/archives/hex-0.16.1
 ---> b784a4cfa3f4
Removing intermediate container 833783e8436b
Step 4/8 : RUN mix archive.install https://github.com/phoenixframework/archives/raw/master/phoenix_new.ez --force
 ---> Running in 0a55d8417a7d
* creating root/.mix/archives/phoenix_new
 ---> cc00d9efb61f
Removing intermediate container 0a55d8417a7d
Step 5/8 : RUN curl -sL https://deb.nodesource.com/setup_8.x | bash -
 ---> Running in b10015410aad

## Installing the NodeSource Node.js v8.x repo...

## Populating apt-get cache...

+ apt-get update
Get:1 http://security.debian.org jessie/updates InRelease [63.1 kB]
Ign http://deb.debian.org jessie InRelease
Get:2 http://deb.debian.org jessie-updates InRelease [145 kB]
Get:3 http://deb.debian.org jessie Release.gpg [2373 B]
Get:4 http://deb.debian.org jessie Release [148 kB]
Get:5 http://security.debian.org jessie/updates/main amd64 Packages [549 kB]
Get:6 http://deb.debian.org jessie-updates/main amd64 Packages [17.8 kB]
Get:7 http://deb.debian.org jessie/main amd64 Packages [9063 kB]
Fetched 9988 kB in 4s (2322 kB/s)
Reading package lists...

## Installing packages required for setup: apt-transport-https lsb-release...

+ apt-get install -y apt-transport-https lsb-release > /dev/null 2>&1

## Confirming "jessie" is supported...

+ curl -sLf -o /dev/null 'https://deb.nodesource.com/node_8.x/dists/jessie/Release'

## Adding the NodeSource signing key to your keyring...

+ curl -s https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add -
OK

## Creating apt sources list file for the NodeSource Node.js v8.x repo...

+ echo 'deb https://deb.nodesource.com/node_8.x jessie main' > /etc/apt/sources.list.d/nodesource.list
+ echo 'deb-src https://deb.nodesource.com/node_8.x jessie main' >> /etc/apt/sources.list.d/nodesource.list

## Running `apt-get update` for you...

+ apt-get update
Hit http://security.debian.org jessie/updates InRelease
Get:1 https://deb.nodesource.com jessie InRelease [4634 B]
Get:2 http://security.debian.org jessie/updates/main amd64 Packages [549 kB]
Ign http://deb.debian.org jessie InRelease
Hit http://deb.debian.org jessie-updates InRelease
Hit http://deb.debian.org jessie Release.gpg
Get:3 https://deb.nodesource.com jessie/main Sources [759 B]
Hit http://deb.debian.org jessie Release
Get:4 https://deb.nodesource.com jessie/main amd64 Packages [969 B]
Get:5 http://deb.debian.org jessie-updates/main amd64 Packages [17.8 kB]
Get:6 http://deb.debian.org jessie/main amd64 Packages [9063 kB]
Fetched 9636 kB in 4s (2111 kB/s)
Reading package lists...

## Run `apt-get install nodejs` (as root) to install Node.js v8.x and npm

 ---> 6eae16e079cc
Removing intermediate container b10015410aad
Step 6/8 : RUN apt-get install -y -q nodejs
 ---> Running in 0a96ec4a86ad
Reading package lists...
Building dependency tree...
Reading state information...
The following NEW packages will be installed:
  nodejs
0 upgraded, 1 newly installed, 0 to remove and 3 not upgraded.
Need to get 12.7 MB of archives.
After this operation, 65.8 MB of additional disk space will be used.
Get:1 https://deb.nodesource.com/node_8.x/ jessie/main nodejs amd64 8.2.1-2nodesource1~jessie1 [12.7 MB]
debconf: delaying package configuration, since apt-utils is not installed
Fetched 12.7 MB in 2s (4850 kB/s)
Selecting previously unselected package nodejs.
(Reading database ... 21884 files and directories currently installed.)
Preparing to unpack .../nodejs_8.2.1-2nodesource1~jessie1_amd64.deb ...
Unpacking nodejs (8.2.1-2nodesource1~jessie1) ...
Setting up nodejs (8.2.1-2nodesource1~jessie1) ...
 ---> 6c8c61664497
Removing intermediate container 0a96ec4a86ad
Step 7/8 : RUN apt-get update && apt-get install -y     inotify-tools  && rm -rf /var/lib/apt/lists/*
 ---> Running in 32babb0f97c9
Hit http://security.debian.org jessie/updates InRelease
Hit https://deb.nodesource.com jessie InRelease
Get:1 http://security.debian.org jessie/updates/main amd64 Packages [549 kB]
Ign http://deb.debian.org jessie InRelease
Hit http://deb.debian.org jessie-updates InRelease
Hit http://deb.debian.org jessie Release.gpg
Get:2 https://deb.nodesource.com jessie/main Sources [759 B]
Hit http://deb.debian.org jessie Release
Get:3 https://deb.nodesource.com jessie/main amd64 Packages [969 B]
Get:4 http://deb.debian.org jessie-updates/main amd64 Packages [17.8 kB]
Get:5 http://deb.debian.org jessie/main amd64 Packages [9063 kB]
Fetched 9631 kB in 4s (2161 kB/s)
Reading package lists...
Reading package lists...
Building dependency tree...
Reading state information...
The following extra packages will be installed:
  libinotifytools0
The following NEW packages will be installed:
  inotify-tools libinotifytools0
0 upgraded, 2 newly installed, 0 to remove and 3 not upgraded.
Need to get 42.6 kB of archives.
After this operation, 106 kB of additional disk space will be used.
Get:1 http://deb.debian.org/debian/ jessie/main libinotifytools0 amd64 3.14-1+b1 [17.8 kB]
Get:2 http://deb.debian.org/debian/ jessie/main inotify-tools amd64 3.14-1+b1 [24.8 kB]
debconf: delaying package configuration, since apt-utils is not installed
Fetched 42.6 kB in 1s (35.2 kB/s)
Selecting previously unselected package libinotifytools0.
(Reading database ... 26476 files and directories currently installed.)
Preparing to unpack .../libinotifytools0_3.14-1+b1_amd64.deb ...
Unpacking libinotifytools0 (3.14-1+b1) ...
Selecting previously unselected package inotify-tools.
Preparing to unpack .../inotify-tools_3.14-1+b1_amd64.deb ...
Unpacking inotify-tools (3.14-1+b1) ...
Setting up libinotifytools0 (3.14-1+b1) ...
Setting up inotify-tools (3.14-1+b1) ...
Processing triggers for libc-bin (2.19-18+deb8u10) ...
 ---> b90106a2456e
Removing intermediate container 32babb0f97c9
Step 8/8 : WORKDIR /app
 ---> 95b9eddb5f6b
Removing intermediate container bb34d3917207
Successfully built 95b9eddb5f6b

From your terminal, being in our docker/phoenix directory, we tell Docker to do a build using the Dockerfile from this directory:

docker build .

Docker obliges and creates us a new image:

95b9eddb5f6b

We can see this by running:

docker images | grep 95b9eddb5f6b
<none>    <none>  95b9eddb5f6b        About a minute ago   895MB

I've shortened the output here so it fits.

The first <none> means no configured Repository.

The second <node> means no set Tag.

The string 95b9eddb5f6b is as per the built image ID.

About a minute ago is when the image was Created.

And 894MB is unsurprisingly, its size on disk. Which is pretty big.

To get our image up to Docker Hub we need to tag it, and configure the repository:

docker tag 95b9eddb5f6b codereviewvideos/docker-phoenix

docker images | grep 95b9eddb5f6b
codereviewvideos/docker-phoenix     latest    95b9eddb5f6b        8 minutes ago       895MB

Again, I've played with the spacing for formatting purposes.

If this were pushing to a private repo, we would need to set a repository more like:

gitlab.whatever.com:5000/docker/phoenix

Either way, we now need to log in from Docker:

docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username: codereviewvideos
Password: 
Login Succeeded

We must now push up our tagged image to Docker Hub:

docker push codereviewvideos/docker-phoenix

The push refers to a repository [docker.io/codereviewvideos/docker-phoenix]
9e2572e8838b: Pushed 
729d08d790a8: Pushing [===>                                               ]  4.429MB/64.28MB
6aee3eacca07: Pushing [========>                                          ]  1.925MB/11.35MB
4d968484b9e5: Pushing [==================================================>]    193kB
39b8a9109e52: Pushing [==================================================>]  615.9kB
34191b71a711: Waiting 
729d08d790a8: Pushing [====>                                              ]  5.543MB/64.28MB
6aee3eacca07: Pushing [=========>                                         ]  2.187MB/11.35MB
502bfa3770b7: Waiting 
5616a6292c16: Waiting 
f3ed6cb59ab0: Waiting 
654f45ecb7e3: Waiting 
2c40c66f7667: Waiting 

Depending on your connection, this might take a while. It does on mine.

Whilst we are waiting, let's remove the burden of having to remember this long winded commands. Let's make use of a Makefile to shorten these steps down somewhat:

build:
    @docker build \
        -t codereviewvideos/docker-phoenix .

push:
    @docker push codereviewvideos/docker-phoenix

bp: build push

Here we define three commands inside our Makefile.

We define build, which we can call with make build.

This is short hand for running a docker build and docker tag against the Dockerfile in the same directory as our Makefile.

Secondly, we define a push command, called with make push, which pushes the image up to Docker Hub.

Lastly, we define bp, which can be called with make bp to run both commands in one. This is shorthand for "make build, and make push, plz". It's a nice time saver.

You don't need to do this, but I find it saves me remembering or typing any of that stuff. Also, it's easier to share :)

That's our web server sorted. Let's get on to hooking this up to a Postgres database.

Episodes