Björn Brynjúlfur

Deploying Elixir and Phoenix

Since starting to code around 4 years ago, I have used JavaScript to build all my web applications with NodeJS and Express. I am now familiar with the language and ecosystem, and want to learn a new programming language. This will keep the learning curve steep, help me improve, and just be more fun than doing what I already know.

I considered Python, PHP, Ruby, and Clojure, but ended up choosing Elixir for three reasons:

  1. Functional programming. Elixir is a functional language with immutable data structures. This is a different paradigm than in languages such as Python, PHP and Ruby. I belive that learning functional programming will help me write better code in any language in the future.
  2. BEAM/Erlang/OTP. Elixir runs on BEAM, which is a Virtual Machine with great characteristics for modern web development: concurrency, scalability and reliability. Furthermore, Elixir applications can run Erlang/OTP code, providing access to a rich ecosystem which has been around since the 90s.
  3. Phoenix. Elixir allows me to use the Phoenix framework, which is intended to make developers productive while preserving long-term code maintainability. I have always wanted to try using a web framework, instead of coding everything from scratch as in NodeJS/Express, and Phoenix has some great features like LiveView which makes me want to try it out.

I started by going through an online course to learn the basic syntax and patterns of Elixir and Phoenix. I now have a plan for a project which I want to write. But before starting on it, I want to make sure I can easily deploy and update Elixir applications.

Automatic VPS deployment for Elixir/Phoenix

I am a fan of the VPS approach as opposed to serverless/Platform-as-a-Service. I like learning about Linux and managing my own server, instead of learning vendor-specific APIs which do not translate between technologies. So my approach will be to set up a Linux VPS, set up a simple deployment strategy, and make it fully automated so I can update my application frequently without extra effort.

I have not found a comprehensive up-to-date guide for this approach, so I am writing down the steps I take, to remember them later and make them available to others in a similar situation.

This guide assumes that you have a working Elixir/Phoenix application ready on your local development machine. I followed the Up and running guide in the Phoenix docs to have a local "Hello world" app ready for trying the deployment.

1. Spin up a clean VPS

I chose DigitalOcean as my VPS host. I have used them for all my projects so far and am very happy with them. I went through this guide on their website in order to set up a production-ready Ubuntu VPS (or a droplet, as they call it).

The guide was simple to follow. After completing it, I can now spin up a new droplet with a single command from my terminal using their CLI tool:

$ doctl compute droplet create [NAME] --tag-names webserver \
--image ubuntu-20-04-x64 --region [REGION] --size [SIZE-OF-VPS] \
--ssh-keys [KEY-FINGERPRINT] --user-data-file [PATH-TO-USER-DATA-FILE] \
--enable-ipv6 --enable-monitoring --enable-private-networking

Once the droplet was set up, I SSH-ed into it, installed all updates and restarted it:

local:$ ssh [USERNAME]@[DROPLET-IP-ADDRESS]
vps:$ sudo apt-get update && sudo apt-get dist-upgrade
vps:$ sudo shutdown -r now

2. Configure git to push code to the VPS

With NodeJS, I used git to deploy applications directly to the VPS. After I make changes, all I need to do to deploy them is to push the git updates to the VPS, which takes care of the rest. I want the same workflow for my Elixir/Phoenix. So I will start by setting up git in the same way.

On the VPS, I do the following:

  1. Create a repo folder for git repositories
  2. Setup a bare repository to sync project files from my local machine
  3. Setup a hook to copy the files to the correct folder
  4. Make it executable
  5. Create a docs folder with a folder for the project files
$ mkdir repo && cd repo
$ mkdir [PROJECT-NAME] && cd [PROJECT-NAME]
$ git init --bare
$ cd hooks
$ cat > post-receive
#!/bin/sh
git --work-tree=/home/[USERNAME]/docs/[PROJECT-NAME] --git-dir=/home/[USERNAME]/repo/[PROJECT-NAME].git checkout -f
# Press ctrl + c here to stop writing to the file
$ chmod +x post-receive
$ cd .. && cd ..
$ mkdir docs && cd docs
$ mkdir [PROJECT-NAME]

Now I exit the VPS and do the following on my local machine:

  1. Go to the project folder
  2. Initialize a git repository
  3. Add the VPS as a remote repository
  4. Add, commit and push the initial files
$ cd [PROJECT-FOLDER]
$ git init
$ git remote add live ssh://[USERNAME]@[VPS-IP-ADDRESS]/home/[USERNAME]/repo/[PROJECT-NAME].git
$ git add .
$ git commit -m "Initial commit"
$ git push live master

Now, all the project files have been copied to the ~/docs/[PROJECT-NAME] folder on the VPS. SSH in and view the folder contents to verify this.

3. Install Elixir, Postgres, and Nginx on the VPS

I want the VPS to both build new versions of the app, and then deploy the new version. I have not seen the need to add Docker or other abstractions into this workflow, so I will just install what I need in order to make this work:

  1. I will need to install Elixir/BEAM to build and run the apps on the VPS
  2. The local app uses Postgres as a database, so I will need that on the VPS as well
  3. I will put the app behind Nginx as a reverse proxy, which allows me to serve files/assets directly and manage domains/SSL certificates in a way I am familiar with

3.1 Elixir

I went through Elixir's official installation guide to install Elixir and Erlang/OTP for Ubuntu. Additionally, the Phoenix installation guide recommends installing a filesystem-watcher for hot code reloading. The commands I used were as follows:

$ sudo wget https://packages.erlang-solutions.com/erlang-solutions_2.0_all.deb && sudo dpkg -i erlang-solutions_2.0_all.deb
$ sudo apt-get update
$ sudo apt-get install esl-erlang
$ sudo apt-get install elixir
$ sudo apt-get install inotify-tools

3.2 Postgres

Postgres also has an installation guide, which is confusing and seems outdated. So I followed one of DigitalOcean's community guides instead.

Additionally, I added a default password for the postgres user, which is expected by Phoenix. I could have skipped setting the password by changing Elixir's environment variables. But I don't know how to do that yet.

These are the commands I used:

$ sudo apt install postgresql postgresql-contrib
$ sudo -u postgres createuser --interactive
Enter name of role to add: [USERNAME]
Shall the new role be a superuser? (y/n) y
$ sudo -u postgres psql
psql:$ \password
Enter new password: postgres
Enter it again: postgres

I now try running the app to ensure that everything works correctly.I was prompted to fetch dependencies first, so apparently they are not included in the project folder sent through git. Later, I will need to understand how dependencies are managed by Elixir.

$ mix deps.get
$ mix ecto.create
The database for [PROJECT-NAME].Repo has been created
$ mix phx.server
[info] Running [PROJECT-NAME]Web.Endpoint with cowboy 2.9.0 at 127.0.0.1:4000 (http)
[debug] Downloading esbuild from https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.12.18.tgz
[info] Access [PROJECT-NAME]Web.Endpoint at http://localhost:4000
[watch] build finished, watching for changes...

I then opened a new terminal for the VPS and ensured that the app was running:

$ curl http://localhost:4000
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
# ...

Great! So everything works so far. My final step is to add Nginx in front of the app, to display it on my chosen domain.

3.3 Nginx

DigitalOcean has another community guide for installing Nginx on Ubuntu, which I followed:

$ sudo apt update
$ sudo apt install nginx
$ systemctl status nginx

Nginx is now fully installed. Now, when I visit the domain I pointed to the VPS' IP address shows a page with the following message:

Welcome to nginx!
If you see this page, the nginx web server is successfully installed and working. Further configuration is required.

Now I configure Nginx to pass the connections on to the Elixir app. I first create the following file /etc/nginx/sites-available/[DOMAIN] and insert the following config:

server {
listen 80;
server_name [DOMAIN] www.[DOMAIN];

location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://localhost:4000;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}

To simplify Nginx configuration, I only use the sites-available folder and not the sites-enabled, so I also make the following change to /etc/nginx/nginx.confg:

- include /etc/nginx/sites-enabled/*;
+ include /etc/nginx/sites-available/*;

Finally, I want to serve the website over HTTPS. I use Certbot for this, which automates certificate renewals. Let's install Certbot and enable HTTPS for the domain:

$ sudo apt install certbot python3-certbot-nginx
$ sudo certbot --nginx -d [DOMAIN] -d www.[DOMAIN]

To ensure that everything works correctly, I test the domain and see whether my app is running:

$ curl https://[DOMAIN]
<!DOCTYPE html>
<html lang="en">
<head>
...

Great! I now have an Elixir app running on the VPS, and I can update it via git.

4. Configure Elixir for production

Even though the app is now running on my domain, there are a few things I want to improve upon:

  1. Production mode. The app should be running in production mode/environment, which I haven't configured yet.
  2. Process manager: I should use a process manager to daemonize the app. Now, I need to open a terminal and run mix phx.server to keep it running. The process manager should also handle logging.
  3. Automatic deployments: When I push updates via git, I want the VPS to recompile and restart the app automatically
  4. Zero downtime: The app should not go down when it is updated. I have read that this should be possible with BEAM/Elixir apps, even without a load-balancer.

4.1 Production mode

After some Googling, I seem to set the following four environment variables in order to run the app in production mode:

MIX_ENV=prod
PORT=4000
DATABASE_URL=ecto://postgres:postgres@localhost:5432/postgres
SECRET_KEY_BASE=[SECRET-KEY]

Where the SECRET-KEY is obtained by typing mix phx.gen.secret in the terminal. I will do this through the process manager.

4.2 Process manager

It seems that systemd, which is built into Ubuntu, is a popular choice for daemonizing Phoenix apps. I stiched together a few guides and ended up creating two files.

First, I created a [PROJECT-NAME].env file in my Phoenix project, in the /config folder, and pasted the environment variables from above there in plaintext.

Then, I defined a systemd service by creating a phoenix.service file in /lib/systemd/system with the following text:

[Unit]
Description=[PROJECT-NAME] Phoenix App
After=network.target

[Service]
Type=simple
User=[USERNAME]
Restart=on-failure
EnvironmentFile=/home/[USERNAME]/docs/[PROJECT-NAME]/config/[PROJECT-NAME].env
WorkingDirectory=/home/[USERNAME]/docs/[PROJECT-NAME]
ExecStart=/usr/bin/mix phx.server

[Install]
WantedBy=multi-user.target

This service reads the environment variables from the .env file I already created, and starts the Phoenix app accordingly. To enable this service, I did the following:

$ sudo systemctl daemon-reload # make the new service available
$ sudo systemctl start phoenix.service # start the service
$ sudo systemctl enable phoenix.service # start the service when the server restarts
$ sudo journalctl -u phoenix.service # view the logs for the app
systemd[1]: Started [PROJECT-NAME] Phoenix App.

I then visited the domain and confirmed that the daemon works correctly and the app is available.

4.3 Automatic deployment

Now that everything is running correctly, the next step is to make the VPS update and restart the app when is push changes to it via git. For NodeJS apps, I use git hooks to do this, by fetching dependencies and then restarting the app whenever files change. I will try the same approach here.

I reopened the post-receive file in /home/[USERNAME]/repo/[PROJECT-NAME].git/hooks and updated it by adding the last line as follows:

#!/bin/sh
git --work-tree=/home/[USERNAME]/docs/[PROJECT-NAME] --git-dir=/home/[USERNAME]/repo/[PROJECT-NAME].git checkout -f
echo "[SUDO-PASSWORD]" | sudo -S -k /bin/systemctl restart phoenix.service

Now when I push new changes, the app restarts automatically.

I probably need to add commands to fetch dependencies and migrate the database before restarting the app, but I am not quite sure how to do that yet.

4.4 Zero downtime

I tested the previous step by making a change to my app and pushing it. Everything worked: the app restarted and reflected the changes afterwards. But there were 5-10 seconds of downtime during this process.

With NodeJS, I use PM2, which is a process manager, to spawn two instances of my apps. When I push an update, PM2 first restarts the first instance, sending all traffic to the second instance in the meantime. Once the first instance has finished restarting, PM2 directs all traffic to it while restarting the second instance. This ensures that updates are deployed without any downtime for end-users.

Eventually, I would like to deploy Elixir/Phoenix updates without any downtime as well. After a short research, however, it seems that this requires more involvement, so I will accept these few seconds of downtime for now.

Newer: 2021 work review: Moodup

Older: The Journey to Product-Market Fit