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.
One Reply to “Deploying a Phoenix App to Ubuntu 16.04 with Edeliver, Distillery, and Nginx”