Deploying a Phoenix App to Ubuntu 16.04 with Edeliver, Distillery, and Nginx

When I first started doing live testing of Breakfast of Champions, I was using Heroku because it’s cheap, easy, and it’s what I was used to. Unfortunately, Heroku allots you very little CPU power regardless of your dyno size. Not to mention they restart every app once a day, hot swapping is not possible, nor is distributed clustering. So when it came time to actually deploy the game I had to look for something different.

Finally, after much procrastination, I cut the PaaS cord and decided to deploy my own cloud server using Vultr. I spun up an Ubuntu 16.04 instance based in Dallas with 2 virtual CPU cores (good enough for now and easily upgraded). I had looked at several alternatives, including Digital Ocean, AWS, and some smaller companies specializing in game servers, but Vultr seemed to benchmark pretty well against the competition and the price was right.

What I didn’t realize is how great the deployment process already is for Elixir apps. The language really benefits from standing on the shoulders of the Erlang giant. After you get past the setup, Edeliver and Distillery make it ridiculously simple.

Anyway, let’s get to the good stuff. After some experimentation and gathering disparate pieces of info (major props go to Digital Ocean’s Phoenix deployment guide), here are the steps to deploy a Phoenix application.

 

 


 

 

This guide assumes you have a Phoenix >=1.3 application with a default directory structure that uses Postgres and Brunch, that you’re using Git for version control, and that you have purchased a domain name. In this guide, I’ll be using example.com.

Step 1: spin up a Server

Create a server instance on Vultr with Ubuntu 16.04. It should have at least 1GB of RAM.

Step 2: SSH into your New Server

Once your server is spun up, click “Manage” to view your server’s details. From this page, copy your server’s root password. Then, in a local terminal:

$ ssh root@your_server_ip

Paste in the password when prompted.

Step 3: Update the Server

In SSH terminal:

$ apt-get update && apt-get upgrade && apt-get dist-upgrade
$ reboot
Step 4: Create a New User

Re-ssh into your server, then:

$ adduser nick

Enter a strong password, and optionally enter more info. Now give the new user sudo privledges:

$ usermod -aG sudo nick
Step 5: Enable Public Key Authentication

If you haven’t generated a local key-pair, do so now. You can use this guide: https://git-scm.com/book/en/v1/Git-on-the-Server-Generating-Your-SSH-Public-Key.

Copy the contents of ~/.ssh/id_rsa.pub from your local machine. You can use:

$ cat ~/.ssh/id_rsa.pub

Highlight and copy the output. Then, in a server terminal:

$ su - nick
$ mkdir ~/.ssh
$ chmod 700 ~/.ssh
$ nano ~/.ssh/authorized_keys

Paste in your public key. Then ctrl+X to exit, Y then Enter to save.

$ chmod 600 ~/.ssh/authorized_keys
$ exit
Step 6: Disable SSH Password Authentication

Still in server terminal, as root user:

$ nano /etc/ssh/sshd_config

Uncomment the line that says “Password Authentication yes” and change it to “Password Authentication no”. Again save and exit nano with ctrl+X then Y then Enter.

Apply new configuration:

$ systemctl reload sshd

Test the new configuration. In a local terminal:

$ ssh root@your_server_ip

This should fail with “Permission denied (publickey)”. Then, SSH with your new user:

$ ssh nick@your_server_ip
Step 7: Add Server to Local SSH Config File

On your local machine, create a file at ~/.ssh/config if it doesn’t exist already, and append this to it:

Host your_app_name
    HostName your_server_ip
    User nick
    IdentityFile ~/.ssh/id_rsa

Now, in a local terminal try:

$ ssh your_app_name

And bada-bing-bada-boom, you should be SSH’ed.

Step 8: Set Up a Firewall to Accept SSH Connections

In server terminal, as your new user:

$ sudo ufw allow OpenSSH
$ sudo ufw enable
Step 9: Install NGinx

Still in server terminal:

$ sudo apt-get install nginx
$ sudo ufw allow 'Nginx Full'
$ sudo ufw status

The output of the last command should look like:

Step 10: Configure DNS

Change your DNS settings to point your domain name (both @ and www) to your server’s IP address.

Step 11: Set up an Nginx Server Block for Your Site

In server terminal:

$ sudo nano /etc/nginx/sites-available/example.com

Then copy/paste the following into the file. Remember to replace “example.com” with your URL.

map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}

upstream phoenix {
    server 127.0.0.1:4000;
}

server {
    root /var/www/html;

    index index.html index.htm index.nginx-debian.html;

    server_name example.com www.example.com;

    location / {
        allow all;

        # Proxy Headers
        proxy_http_version 1.1;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header X-Cluster-Client-Ip $remote_addr;

        # WebSockets
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        proxy_pass http://phoenix;
    }
}

Save and close this file. Now create a symlink of this file in /etc/nginx/sites-enabled:

$ sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
Step 12: Change Hash Bucket Memory Config

In server terminal:

$ sudo nano /etc/nginx/nginx.conf

Uncomment the line “server_names_hash_bucket_size 64;”, then save and close the file.

Test the new config to make sure it works, then if all is good, restart Nginx:

$ sudo nginx -t
$ sudo systemctl restart nginx
Step 13: Install Certbot and Get an SSL Cert for Your Site

In server terminal:

$ sudo add-apt-repository ppa:certbot/certbot
$ sudo apt-get update
$ sudo apt-get install python-certbot-nginx
$ sudo certbot --nginx -d example.com -d www.example.com

Finish the prompts. When it asks about redirecting all requests through HTTPS, I select yes, because why not right? Now make sure certbot is good to auto-renew by running the following command and make sure no errors happen.

$ sudo certbot renew --dry-run

The python-certbot-nginx program installed a cron job at /etc/cron.d/certbot to check every day if the certs need to be updated, which is pretty cool.

You should now be able to go to https://example.com and view the default Nginx page, but, you know, securely.

Step 14: Install Erlang, Elixir, Hex, and NPM

In server terminal:

$ cd ~
$ 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
$ sudo apt-get install elixir
$ mix local.hex
$ sudo apt-get install npm
$ sudo npm install -g n
$ sudo n stable

This last command should install the latest stable version of node. It may be preferable to use NVM instead of n though. Not sure, leave a comment if you know better.

Step 15: Install Postgresql

In server terminal:

$ sudo apt-get install postgresql postgresql-contrib
$ sudo -u postgres createuser --interactive

For your postgres username, make sure to use the same thing as your Ubuntu username. In this guide, I’ve been using “nick”.

Also be sure to type Y to make the new user a superuser.

Next, create a db of the same name as the new user:

$ sudo -u postgres createdb nick

Create your app’s prod db. You can find the db name in config/prod.secret.exs file of your app.

$ sudo -u postgres createdb your_prod_db_name

Now open a pqsl session. If you did the previous steps correctly, this should work no problem:

$ psql

Set the password for your Postgres user then exit psql:

$ \password nick
$ \q

Add the credentials you just created to your config/prod.secret.exs file as your db username and password.

Step 16: Add Distillery and Edeliver to your App’s Dependencies

Have a look at the GitHub pages of Edeliver and Distillery to see what to add to your mix.exs file. Run mix deps.get when done.

Step 17: Set up your Prod Config

Change config/prod.exs to the following:

use Mix.Config

config :your_app, YourAppWeb.Endpoint,
  http: [port: 4000],
  url: [host: "yoururl.com", port: 80, scheme: "https"],
  cache_static_manifest: "priv/static/cache_manifest.json",
  server: true,
  code_reloader: false,
  root: ".",
  check_origin: false,
  version: Application.spec(:your_app, :vsn)

config :logger, level: :info

import_config "prod.secret.exs"
Step 18: Copy prod.secret.exs to your server

In server terminal:

$ cd ~ && mkdir app_config

In local terminal:

$ scp ~/your_app/config/prod.secret.exs example.com:/home/nick/app_config/prod.secret.exs

Run that command anytime you make changes to the prod.secret.exs file.

Step 19: Configure Distillery and Edeliver

In a local terminal in your app’s base directory, run the following command to generate Distillery’s config file.

$ mix release.init

Next, also locally, in the base directory of your app, create a directory named “.deliver”. Inside this directory make a file called “config”, and copy the following for its contents:

APP="your_app_name"

BUILD_HOST="example.com"
BUILD_USER="nick"
BUILD_AT="/home/nick/app_build"

PRODUCTION_HOSTS="example.com"
PRODUCTION_USER="nick"
DELIVER_TO="/home/nick/app_release"

pre_erlang_get_and_update_deps() {
  local _prod_secret_path="/home/nick/app_config/prod.secret.exs"
  if [ "$TARGET_MIX_ENV" = "prod" ]; then
    __sync_remote "
      ln -sfn '$_prod_secret_path' '$BUILD_AT/config/prod.secret.exs'
    "
  fi
}

pre_erlang_clean_compile() {
  status "Installing NPM dependencies"
  __sync_remote "
    [ -f ~/.profile ] && source ~/.profile
    set -e

    cd '$BUILD_AT/assets'
    npm install $SILENCE
  "

  status "Building static files"
    __sync_remote "
      [ -f ~/.profile ] && source ~/.profile
      set -e

      cd '$BUILD_AT'
      mkdir -p priv/static
      cd '$BUILD_AT/assets'
      npm run deploy $SILENCE
   "

  status "Running phx.digest"
  __sync_remote "
    [ -f ~/.profile ] && source ~/.profile
    set -e

    cd '$BUILD_AT'
    APP='$APP' MIX_ENV='$TARGET_MIX_ENV' $MIX_CMD phx.digest $SILENCE
  "
}

Add the line “.deliver/releases” to the end of your .gitignore file, then make a new commit, and push if you want to. Almost done!

Step 20: Build your first Release with Edeliver

In a local terminal from your app’s base directory:

$ mix edeliver build release

This will take a while. It uses the version number specified in your mix.exs file. If the build is successful, deploy it to your server, start up the app, and run your db migrations:

$ mix edeliver deploy release to production
$ mix edeliver start production
$ mix edeliver migrate production

Your site should be up and running at your url! If it’s not, you can check the logs on your server at ~/app_releases/your_app/var/log/erlang.log.1

Step 21: Hot Upgrades

If you want to push upgrades to your app that don’t require a restart, you can use the following commands:

$ mix edeliver build upgrade --with=<last version>
$ mix edeliver deploy upgrade to production --version=<new version>

Replace “<last version>” and “<new version>” with your actual last and new versions, like: mix edeliver build upgrade --with=0.0.1

Step 22: DO a Little Dance!

That’s it! Everything should be working swimmingly. If not, leave a comment, and I’d be happy to try to help you troubleshoot.

Leave a Reply

Your email address will not be published. Required fields are marked *