2020 / 07 / 27
Deploy a Phoenix application to a VPS

Short guide on how to deploy a Phoenix app to Linode/DigitalOcean by hand.

elixir
devops
phoenix

Prerequisites

First, we need a Phoenix app.

The application we’ll be deploying is the one from:
Building a JSON API in Elixir with Phoenix.

I’ll show the steps needed to have this app available at: http://lobotuerto.com:4000

We’ll go with Ubuntu for the OS.
Also, we should have a domain already configured on that machine.

So, when trying something like this, it should respond as expected:

curl -H "Content-Type: application/json" -X POST \
-d '{"email":"asd@asd.com","password":"qwerty"}' \
http://lobotuerto.com:4000/api/users/sign_in \
-c cookies.txt -b cookies.txt -i

OK, SSH into your VPS then follow the steps below.

Configure the firewall

sudo ufw enable

sudo ufw allow ssh
sudo ufw allow http
sudo ufw allow https

Install PostgreSQL on the VPS

Follow this guide: How to install PostgreSQL in Linux.

Make sure to create a DB user for your app —it’s on the guide.
In this example the DB user will be called deployer and will have a password deployer123.

Install dependencies for Erlang and Elixir

sudo apt install build-essential autoconf m4 libncurses5-dev libwxgtk3.0-dev \
libgl1-mesa-dev libglu1-mesa-dev libpng-dev libssh-dev unixodbc-dev

Create a user for deployment

sudo adduser deployer

Install Erlang and Elixir

Impersonate the user with:

sudo su deployer -l

This is done so the deployer user have access to the Elixir executable.

Follow this guide: How to install Elixir in Linux.

Clone your app repo

Or transfer the code using scp.

git clone https://github.com/lobo-tuerto/my-phoenix-json-api

Install the app dependencies

cd my-phoenix-json-api
mix deps.get

Adjust the config files

Comment out this code in config/prod.exs:

-config :my_app, MyAppWeb.Endpoint,
-  url: [host: "example.com", port: 80],
-  cache_static_manifest: "priv/static/cache_manifest.json"
+
+# config :my_app, MyAppWeb.Endpoint,
+#   url: [host: "example.com", port: 80],
+#   cache_static_manifest: "priv/static/cache_manifest.json"

Uncomment this code from config/prod.secret.exs:

-#     config :my_app, MyAppWeb.Endpoint, server: true
+config :my_app, MyAppWeb.Endpoint, server: true

Setup the DB

Execute the following three commands:

SECRET_KEY_BASE="`mix phx.gen.secret`" \
DATABASE_URL="ecto://deployer:deployer123@localhost/myapp_prod" \
MIX_ENV=prod mix ecto.create

SECRET_KEY_BASE="`mix phx.gen.secret`" \
DATABASE_URL="ecto://deployer:deployer123@localhost/myapp_prod" \
MIX_ENV=prod mix ecto.migrate

SECRET_KEY_BASE="`mix phx.gen.secret`" \
DATABASE_URL="ecto://deployer:deployer123@localhost/myapp_prod" \
MIX_ENV=prod mix release

Let’s create a couple of users to try it out.

_build/prod/rel/my_app/bin/my_app start_iex

iex> MyApp.Account.create_user(%{email: "asd@asd.com", password: "qwerty"})
iex> MyApp.Account.create_user(%{email: "some@email.com", password: "some password"})

This could have been done using the priv/repo/seeds.exs file too, but I wanted to showcase how to open an IEx session on the production app.

Now, without exiting that IEx session, go ahead and try this:

curl -H "Content-Type: application/json" -X POST \
-d '{"email":"asd@asd.com","password":"qwerty"}' \
http://lobotuerto.com:4000/api/users/sign_in \
-c cookies.txt -b cookies.txt -i

You should see:

HTTP/1.1 200 OK
cache-control: max-age=0, private, must-revalidate
content-length: 48
content-type: application/json; charset=utf-8
date: Mon, 29 Jul 2019 22:15:13 GMT
server: Cowboy
x-request-id: FbYAie3B0hrQ5-gAAAPF
set-cookie: _my_app_key=SFMyNTY.RgcVe-7fJ-NqSO5DQL; path=/; HttpOnly

{"data":{"user":{"email":"asd@asd.com","id":1}}}

Awesome! :tada:

Further reading on releases and deployments