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 runningphoenix.digest
andbrunch 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.