At PSPDFKit, we invested in Elixir very early. PSPDFKit Server is written completely in Elixir and delivers magical real-time collaboration features to our PSPDFKit clients. It was clear from the beginning that our customers wanted to host PSPDFKit Server within their existing infrastructure to retain complete control over their data. We obviously don’t want them to have to install and maintain Elixir and other dependencies on their server, so we decided to go with Docker, a software that provides an additional layer of abstraction of operating-system-level virtualization.

In this blog post, I will explain how you can run your Elixir application inside Docker. This is useful if you’re developing an API that your app developers are using: They don’t need to install Elixir; rather, they can simply install Docker and run your container. For simplicity’s sake, we will use the Phoenix framework to serve a website from the Docker container.

What Is Docker?

Docker makes it possible to package your application and use it in development and production by running it in a container. A container is like a virtual machine, but while the virtual machine actually runs its own OS, a container can reuse the underlying Linux kernel while still running completely isolated from its surroundings. This not only makes it possible to run many more containers on a single machine compared to virtual machines, but there’s the added benefit that containers start instantly.

Getting Started

For the purpose of this post, I will assume you already have a Phoenix project that you now want to run within a Docker container. To get started with Docker, you first need to install it for your environment.

Dockerfile

A container is an instance of a Docker image. You can create your own images and start from a clean Linux distribution of your choice and install all required packages on your own, or you can use predefined images from Docker Hub. For our Elixir project, we can use the official Elixir Docker image as our base image. The Elixir image itself uses an Erlang image as its base and extends it by installing the required Elixir dependencies.

We now create our own Dockerfile in our Elixir root directory:

Copy 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # ./Dockerfile # Extend from the official Elixir image FROM elixir:latest # Create app directory and copy the Elixir projects into it RUN mkdir /app COPY . /app WORKDIR /app # Install hex package manager # By using --force, we don’t need to type “Y” to confirm the installation RUN mix local.hex --force # Compile the project RUN mix do compile

In the Dockerfile, we specified what should be done when running the container:

We created a separate /app folder which we copied our code to

folder which we copied our code to We installed hex to fetch the dependencies

We compiled the project

While writing the Dockerfile, you may have noticed that we haven’t yet mentioned Postgres. Where should we run our database? If we want all of our infrastructure to run in containers, we should actually create our own container that runs Postgres. Luckily, there is also an official Postgres image that we can reuse. But all of this leaves us with more questions than before.

How can we create an image from our Dockerfile now? How can we start multiple containers at once? How can our Phoenix container communicate with the Postgres container?

The solution? Docker Compose.

docker-compose.yml

docker-compose.yml is our file that defines which containers we are going to run and which images we need to create. Here is the full docker-compose.yml we will use for our app:

YAML Copy 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 # Version of docker-compose version : ' 3' # Containers we are going to run services : # Our Phoenix container phoenix : # The build parameters for this container. build : # Here we define that it should build from the current directory context : . environment : # Variables to connect to our Postgres server PGUSER : postgres PGPASSWORD : postgres PGDATABASE : database_name PGPORT : 5432 # Hostname of our Postgres container PGHOST : db ports : # Mapping the port to make the Phoenix app accessible outside of the container - " 4000:4000" depends_on : # The db container needs to be started before we start this container - db db : # We use the predefined Postgres image image : postgres:9.6 environment : # Set user/password for Postgres POSTGRES_USER : postgres POSTGRES_PASSWORD : postgres # Set a path where Postgres should store the data PGDATA : /var/lib/postgresql/data/pgdata restart : always volumes : - pgdata:/var/lib/postgresql/data # Define the volumes volumes : pgdata :

Because a container is stateless and can be destroyed and recreated at any time, Docker allows you to create volumes. Volumes are separate from the container and you can easily define them in docker-compose.yml .

Up and Running… Nearly

Now we want to create our Docker image. We can do this by executing docker-compose build (it will take some time to download and install all the images). You should receive a success message, and via docker images , you will get a list of all Docker images available on your machine:

Shell Copy 1 2 3 4 REPOSITORY TAG IMAGE ID CREATED SIZE helloworld_phoenix latest be0fed013a6b 2 minutes ago 917MB elixir latest 9d6cef9afe13 13 days ago 889MB postgres 9.6 d3ac03f9698d 4 weeks ago 268MB

We now can even start our Docker container via docker-compose up . However, if we go to http://localhost:4000 we will see nothing. This is because, as you probably might have noticed, we never started our Phoenix application, nor did we create the database.

Start Phoenix and Create Our Database

To start our Phoenix server, create our database, and do our database migrations, we will create a separate shell script that we call entrypoint.sh . (When creating this file, don’t forget to give it execution rights via chmod +x entrypoint.sh ):

bash Copy 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 # entrypoint.sh #!/bin/bash # Docker entrypoint script. # Wait until Postgres is ready while ! pg_isready -q -h $PGHOST -p $PGPORT -U $PGUSER do echo " $( date ) - waiting for database to start" sleep 2 done # Create, migrate, and seed database if it doesn't exist. if [[ -z ` psql -Atqc " \\ list $PGDATABASE " ` ]] ; then echo "Database $PGDATABASE does not exist. Creating..." createdb -E UTF8 $PGDATABASE -l en_US.UTF-8 -T template0 mix ecto.migrate mix run priv/repo/seeds.exs echo "Database $PGDATABASE created." fi exec mix phx.server

Wait until Postgres starts

You might ask yourself: We already defined the Postgres container as a dependency of our Phoenix container, so why do we need to wait? Yes, docker-compose will wait until the Postgres container has started, but this does not mean that the Postgres server inside the container is already running. This is why we need wait until our Postgres server starts via pg_isready — I will explain later how we can install it. Inside the loop, we check if the Postgres server is already running. If that’s not the case, we wait two seconds and check the status of the server again. You can also see that we can use the environment variables defined in the docker-compose file for our pg_isready function.

Create the database

After we know that Postgres has started, we can create our database if it doesn’t yet exist and run our migrations and seed data.

Start our Phoenix server

In the end, we can execute mix phx.server and this will finally start our server.

Updating Our Dockerfile

We now need to execute the entrypoint.sh script in our Dockerfile. We also need to install the package postgresql-client to run pg_isready within that script:

Copy 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # Use an official Elixir runtime as a parent image FROM elixir:latest RUN apt-get update && \ apt-get install -y postgresql-client # Create app directory and copy the Elixir projects into it RUN mkdir /app COPY . /app WORKDIR /app # Install hex package manager RUN mix local.hex --force # Compile the project RUN mix do compile CMD ["/app/entrypoint.sh"]

Accessing the Postgres Server

One question still remains: How can we access the Postgres server within our Phoenix container? The Postgres container is available to us via the hostname db (the same name as the container). To actually use our environment variables in our configuration, we will overwrite the database configuration in our repo:

elixir Copy 1 2 3 4 5 6 7 8 9 10 11 12 13 defmodule YourApp . Repo do use Ecto . Repo , otp_app: :your_app def init ( _ , config ) do config = config |> Keyword . put ( :username , System . get_env ( "PGUSER" )) |> Keyword . put ( :password , System . get_env ( "PGPASSWORD" )) |> Keyword . put ( :database , System . get_env ( "PGDATABASE" )) |> Keyword . put ( :hostname , System . get_env ( "PGHOST" )) |> Keyword . put ( :port , System . get_env ( "PGPORT" ) |> String . to_integer ) { :ok , config } end end

Now we are finally done. We can run docker-compose build and docker-compose up again and our containers will start, create the database, and start the Phoenix server that is now accessible via http://localhost:4000 .

Useful Commands

Here is a small list of useful commands when dealing with Docker:

docker ps lists all containers that are currently running

lists all containers that are currently running docker container ls --all lists all containers that are available

lists all containers that are available docker logs <container> shows the log of the given container

shows the log of the given container docker start/stop <container> starts or stops a container

starts or stops a container docker rm <container> removes a container

removes a container docker images lists all available images

lists all available images docker rmi <image> removes an image

removes an image docker-compose down --volumes destroys the created volumes

Conclusion