Deploy Early and Often: Deploying Phoenix with Edeliver and Distillery (Part Two)

If you’ve followed Part One of this guide, you’ll have:

  • A bare-bones Phoenix application
  • An Ubuntu server ready for compiling and deploying

Let’s proceed.

Edeliver Configuration

In your project, create the file .deliver/config

Update: The main differences between the config here and the default on the edeliver repo:

  • GIT_CLEAN_PATHS for faster builds. You should read the caveats here.
  • the pre_erlang_clean_compile() block for running phoenix.digestand brunch build —-production. You can explore other pre- and post-hooks on the edeliver wiki.

Change the APP, BUILD_HOST and PRODUCTION_HOSTS variables to your own. Then, add the release directory to your .gitignore file

echo ".deliver/releases/" >> .gitignore

Let’s commit to git and ensure dependencies are resolved.

git add -A && git commit -m "Setting up edeliver"
mix do deps.get, compile

In config/prod.exs, replace http: [port: {:system, “PORT”}] with http: [port: 8888], and uncomment the line config :phoenix, :serve_endpoints, true. It should look like this:

Now let’s set up Distillery.

mix release.init

This sets up a config file at rel/config.exs. We won’t need to change it for now. Let’s commit our changes and push to our repository.

git add -A && git commit -m "Setting up distillery"
git push origin master

Building Our First Release

Let’s see if building our release works.

mix edeliver build release production --verbose

We get the error: bash: line 10: mix: command not found. We haven’t installed Elixir on our server yet! Let’s start with installing Erlang, then we’ll install Elixir.

First, SSH into the server:

ssh deploy@188.166.182.170

Then, we’ll add the Erlang Solutions repo to install Erlang/OTP, followed by Elixir:

wget https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb && sudo dpkg -i erlang-solutions_1.0_all.deb
sudo apt-get update
sudo apt-get install esl-erlang=1:19.3
sudo apt-get install elixir

Now back in our shell (your own computer, not the VPS), we’ll try building the release again.

mix edeliver build release production --verbose

This time, we get the error ** (Mix.Config.LoadError) could not load config config/prod.secret.exs. Let’s create that file on the server. First, copy the contents of config/prod.secret.exs to your clipboard. Now we’ll create the file in the deploy user’s home directory.

ssh deploy@188.166.182.170
vim prod.secret.exs # you can also use `nano prod.secret.exs`

Paste in the contents of your clipboard. We’ll update the secret_key_base with a new secret. In your shell, run mix phoenix.gen.secret to generate a new secret key and replace the secret_key_base value with the newly generated one. Then, update the database details according to the credentials you’ve used in Part One. Here’s what the file should look like:

Once you’ve saved the file, try building the release again:

mix edeliver build release production --verbose

This time it succeeds! Now we can deploy it to production.

mix edeliver deploy release to production

Aside: In this guide, we’re using the same server, which is fine for small projects, but if your application is getting regular traffic, it’s advisable to do compilation and deployment on different servers to avoid competing for the same resources.

Viewing Our Application On The Server

Let’s see if we can view the site on our server. In a different shell window, we’ll SSH into the server and try to load localhost:8888:

ssh deploy@188.166.182.170
curl localhost:8888
# curl: (7) Failed to connect to localhost port 8888: Connection refused

Switch back to your local shell and run:

mix edeliver start production
# START DONE!

On the SSH shell, this time we’re able to load the page.

curl localhost:8888
# <!DOCTYPE html>
# <html lang="en">
# <head>
# ...

Great! But how do we access the application from the web? Let’s install nginx and configure it so that it serves our web app from a domain.

UPDATE: Before you continue, you should create a DNS A Record that points your domain to the public IP address of your server. In this example, you should point example.com and www.example.com to 188.166.182.170.

Nginx Setup

Install nginx:

sudo apt-get install nginx

We’ll update UFW to allow connections on port 80 (HTTP):

sudo ufw app list    # returns a list of available applications
sudo ufw allow 'Nginx HTTP'

It is recommended that you enable the most restrictive profile. Since we haven’t configured SSL for our server yet (I might do it in a future guide), we will only allow traffic on port 80 and not port 443.

Let’s SSH in and set up a configuration file.

ssh deploy@188.166.182.170
sudo vim /etc/nginx/sites-available/deploy_phoenix

Paste in the following and edit the server_name to one that you control.

Save the file. Now, we’ll symlink the file to the nginx sites-enabled folder.

sudo ln -s /etc/nginx/sites-available/deploy_phoenix /etc/nginx/sites-enabled/deploy_phoenix

We’ll run a test to check that the configuration is valid, then restart nginx:

sudo nginx -t    # ... test is successful
sudo service nginx restart

Now for the moment we’ve been waiting for. Visit the URL in the browser and you should see your application. Great job!

There’s still one more thing to do before we celebrate. If we restart our server, we’ll find that the application isn’t automatically brought to life (Try it now with sudo reboot). Let’s fix that.

Starting Phoenix on Boot with Systemd

Systemd is the standard init service in Ubuntu 16.04. Systemd will not only restart our app when the server reboots, but it will also detect if the app crashes and will attempt to restart it. Let’s get to it.

sudo vim /lib/systemd/system/deploy_phoenix.service

Paste in the following:

Save the file. Next, we will enable the deploy_phoenix service we just created.

sudo systemctl enable deploy_phoenix.service
sudo systemctl daemon-reload # reloads the systemd process

Let’s test it out. On your local shell, run

mix edeliver stop production

Refresh the URL. You should see a 502 Bad Gateway served by nginx. In a few moments, refresh again and you’ll see that the application has been revived! Do a sudo reboot just to be sure it will always be started on boot.

Hot Code Upgrades

Edeliver and Distillery give us the benefit of deploying hot code upgrades with zero downtime. Let’s give that a go.

Make a small change to index.html.eex:

Then, bump the version in mix.exs:

Commit the changes and push the changes.

git add -A && git commit -m “Testing hot upgrades”
git push origin master

In order to build an upgrade package, we need the current version of the application that’s living on the server.

mix edeliver version production # 0.0.1
mix edeliver build upgrade production --verbose --with=0.0.1
# UPGRADE BUILD OF DEPLOY_PHOENIX WAS SUCCESSFUL!

Once the upgrade package is ready, deploy it to production.

mix edeliver deploy upgrade to production
# DEPLOYED UPGRADE TO PRODUCTION!

Refresh the web page. Voila! The upgrade has been successfully deployed with no downtime at all.

Conclusion

Congratulations! You now have a solid base from which you can develop more complex features with the peace of mind that you can deploy often and catch deployment-related bugs before they pile up.

Next Steps

In order to have a more robust set-up, it’s a good idea to set up automated backups and HTTPS. If you’d like a guide on how I do backups and HTTPS, drop a comment below and I’ll consider doing one in the near future.

You may also consider using asdf for managing Erlang and Elixir versions. It’s similar to how rbenv works.

UPDATE: I’ve created a guide for setting up automated backups.

UPDATE 2: And here’s how to set up free SSL with Let’s Encrypt certificates.