loading...

Deploying Phoenix via Dokku

mplatts profile image Matthew Platts Updated on ・8 min read

cover

Deploy early and deploy often.

I like making MVPs that could potentially turn into a proper startup. Obviously I want to save on costs. Non tech entrepreneurs often end up with a server on Amazon EC2 with an accompanying Postgres instance and end up paying over $100 per month. This is because they've hired a developer to do it who doesn't care about costs.

You should only pay this much when your startup is validated and you have traction. Until then you are essentially in find-product-market-fit mode. It's unlikely many people will be using your site in this mode and you should ideally be on a $5/month VPS. Once you hit traction you can upgrade easy enough - export your DB and upgrade to Amazon or GCP. Phoenix on $5/month will handle a large amount of traffic if your app isn't overly complicated.

I don't really like dev ops work so I'm going to use Dokku, which basically mimics Heroku in that it acts as a PaaS and you just do git remote pushes to deploy your code. Note that Dokku supports any framework that Heroku supports - so can you can still kind of follow along if you're on Node or Ruby on Rails or whatever.

Deploying Phoenix on Vultr

Note that this could apply to any VPS (eg Digital Ocean). I use Vultr because it has an Australian location option.

Create a blank phoenix app using their install guide.

Run the server mix phx.server

Phoenix hello world screen

Get Ubuntu up and running somewhere in the cloud. For Vultr just register and click the big blue plus button.

  1. Server Location: Pick one closest to you or your target audience.
  2. Server Type: Ubuntu 16.04 (could probably try newer versions - up to you)
  3. Server Size: Might as well start with a $5/month one
  4. Additional Features: All unchecked
  5. Startup script: Leave it empty
  6. SSH Keys: Add your computers ssh key (cat ~/.ssh/id_rsa.pub) to see it. Or look up SSH keys if you don't have one.
  7. Server Hostname & Label: Whatever you want

Once that's installed, click on it and get the IP address and password so we can login.

Mine is 45.76.112.118.

In your terminal login:

ssh root@45.76.112.118

Are you sure you want to continue connecting (yes/no)? yes
root@45.76.112.118's password:

Copy and paste your password.

Now that we're inside the Ubuntu instance we should first ensure all the linux packages are up to date.

sudo apt update
sudo apt upgrade

Now we can install dokku. Make sure you check those docs as the version might be higher than what I'm using.

wget https://raw.githubusercontent.com/dokku/dokku/v0.20.4/bootstrap.sh;
sudo DOKKU_TAG=v0.20.4 bash bootstrap.sh

Now in your browser copy and paste your server's IP address into the address bar and follow the web installer.

Dokku web installer

Just add your ssh key like before - cat ~/.ssh/id_rsa.pub. And I just leave the rest as defaults. Click finish.

Now that Dokku is installed we can start using its command line interface (CLI) to setup a database and link it to our app.

Still on your remote server run these commands (note that I'm calling my app card-tracker).

# Create a dokku app called card-tracker
dokku apps:create card-tracker

# Dokku has lots of plugins - this one helps us create a postgres db
dokku plugin:install https://github.com/dokku/dokku-postgres.git

# Use the pluging to create a db instance called 'db'
dokku postgres:create db

# Linking just creates a global ENV config variable in card-tracker called 'DATABASE_URL`
dokku postgres:link db card-tracker

Your dokku app has settable global ENV config variables. You use these to set stuff you don't want to commit into git - important stuff like your database credentials, third party api keys, etc.

You can check the currently set ENV vars with dokku config:export card-tracker. There should be only one set - DATABASE_URL. This was set when you ran dokku postgres:link db card-tracker.

Go back to your phoenix app codebase and look at the file prod.secret.exs:

Prod secret file

We can see Phoenix by default will look for an ENV var called DATABASE_URL and set up your database to use it for the credentials. Handy.

The Endpoint config is oddly split over prod.exs and prod.secret.exs (when you write config :app, AppWeb.Endpoint, blah, it's just adding to the existing config, not overwriting it).

I ended up deleting the Endpoint config in prod.exs and putting it completely in prod.secret.exs for simplicity.

# I deleted this from prod.exs:
config :app, AppWeb.Endpoint,
  url: [host: "example.com", port: 80],
  cache_static_manifest: "priv/static/cache_manifest.json"

And in the file prod.secret.exs:

config :app, AppWeb.Endpoint,
  cache_static_manifest: "priv/static/cache_manifest.json",
  http: [port: String.to_integer(System.get_env("PORT") || "4000")],
  url: [
    scheme: "https",
    host: System.get_env("WEB_HOST"),
    port: 443
  ],
  force_ssl: [rewrite_on: [:x_forwarded_proto]],
  secret_key_base: secret_key_base

Now back to our remote server we can set the WEB_HOST global ENV variable:

dokku config:set --no-restart card-tracker WEB_HOST=45.76.112.118

Also looking back at prod.secret.exs down the bottom:

secret_key_base =
  System.get_env("SECRET_KEY_BASE") ||
    raise """
    environment variable SECRET_KEY_BASE is missing.
    You can generate one by calling: mix phx.gen.secret
    """

config :app, AppWeb.Endpoint,
  http: [:inet6, port: String.to_integer(System.get_env("PORT") || "4000")],
  secret_key_base: secret_key_base

We should generate a SECRET_KEY_BASE by running mix phx.gen.secret. Do that in your console on your local computer (not remote).

Then set it on the remote:

dokku config:set --no-restart card-tracker SECRET_KEY_BASE="TGISHf+6hJiQgEuroRY29k8IWnmY9MzggnPY86x16AYkJnMoPZDBcRuVgiUkT/Zu"

Now if you run dokku config:export card-tracker again you should have ENV vars for:

  • DATABASE_URL
  • SECRET_KEY_BASE
  • WEB_HOST

Now go back to local terminal inside your phoenix app. We need to add our server as a remote to push to.

# Add a remote called dokku. In the dokku setup it created a user called dokku
git remote add dokku dokku@45.76.112.118:card-tracker

We're still not quite ready to deploy though. Like I said Dokku is like Heroku - and even Heroku doesn't support all languages and frameworks. Instead it relies on "buildpacks", which are basically install scripts for different environments. There is one for Phoenix. If you are serving static content (like JS/CSS) then you also need a Phoenix static buildpack.

There are a couple of ways to tell Dokku what buildpacks to use, but we'll use the method where you create a .buildpacks file in the root of your Phoenix project.

In you .buildpacks file add:

https://github.com/HashNuke/heroku-buildpack-elixir.git
https://github.com/gjaldon/heroku-buildpack-phoenix-static

The elixir buildpack allows you to set the elixir and erlang versions. To do this create another file called elixir_buildpack.config in the root of your Phoenix project and add the contents:

elixir_version=1.9.0
erlang_version=21.1.1

The versions above worked for me as of 23 July 2019 - maybe google the latest or check yours with brew info erlang and brew info elixir. Although I found if I used the latest versions the deploy process failed. So if you get random deploy fails then move the versions back until it works.

Finally, create another file called Procfile in your root directory. Inside it add:

web: ./.platform_tools/elixir/bin/mix phx.server

Read up on Procfiles here. They are a Heroku thing that Dokku also honours. I'm not really sure why we need to add this but I found it in a forum somewhere because without it my server wasn't working. The elixir buildpack says this "If your app doesn't have a Procfile, default web task mix run --no-halt will be run." So maybe that mix run --no-halt isn't good enough anymore.

Anyway, commit those 3 files and then let's try deploying!

git add -A
git commit -m 'Add dokku config files'
git push dokku master

It will print out a bunch of install logs - might take a while. It should finish with something like:

=====> Application deployed:
       http://45.76.112.118:31894

Paste that URL into your browser (or CMD-click it if in iterm2) and you should see your app online.

Migrations

By default our elixir buildpack doesn't run Phoenix migrations. To run them, we'll need to utilise Dokkus post deploy hook. To do this we add another file in the root directory of our Phoenix folder called app.json:

{
  "scripts": {
    "dokku": {
      "postdeploy": "mix ecto.migrate"
    }
  }
}

Commit that and deploy and you should see that command being run in the logs.

Domains

By default Dokku is just using the IP address and using ports to show your app to the world.

You could add another app dokku apps:create blah and it would just live on another port.

http://45.76.112.118:31894 => card-tracker
http://45.76.112.118:32939 => blah

Anyway I registered cardtracker.com.au on Godaddy and I'd like this domain to point to my IP address.

To do that I go into DNS settings in Godaddy and make sure there are no A records except for one:

Type: A
Name: @
Value: 45.76.112.118

This means cardtracker.com.au is now pointing to my remote server. Since I want www to also point there I can create a cname record to point to my root record:

Type: CNAME
Name: www
Value: @

Now on the remote server add the domain to Dokku:

dokku domains:add card-tracker cardtracker.com.au

Wait a bit of time for the changes to propagate. You can check what's happening in the terminal with host -a cardtracker.com.au.

Trying "cardtracker.com.au"
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 20867
;; flags: qr rd ra; QUERY: 1, ANSWER: 4, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;cardtracker.com.au.        IN  ANY

;; ANSWER SECTION:
cardtracker.com.au. 599 IN  A   45.76.112.118
cardtracker.com.au. 3599    IN  NS  ns69.domaincontrol.com.
cardtracker.com.au. 3599    IN  NS  ns70.domaincontrol.com.
cardtracker.com.au. 3599    IN  SOA ns69.domaincontrol.com. dns.jomax.net. 2019050807 28800 7200 604800 600

Remember to update the WEB_HOST environment variable with your new host.

dokku config:set card-tracker WEB_HOST=cardtracker.com.au

SSL

SSL is the norm these days. Dokku makes it easy with the dokku-letsencrypt plugin:

dokku plugin:install https://github.com/dokku/dokku-letsencrypt.git
dokku config:set --no-restart card-tracker DOKKU_LETSENCRYPT_EMAIL=your_email@mail.com

# Free SSL for 90 days
dokku letsencrypt card-tracker

# Add a monthly CRON job to refresh your free SSL certificate each month
dokku letsencrypt:cron-job --add

# View your SSL status
dokku letsencrypt:ls

You should now be able to view your site with https. Make sure you check in Chrome incognito - mine wasn't working on my normal Chrome, probably due to some caching issue.

Reading the database

You can use any database tool to connect to your remote database. I use Table Plus.

Run these commands:

dokku postgres:expose db
dokku postgres:info db

In the output you can find these lines:

Exposed ports:       5432->24321  
postgres://postgres:xxxxxxxxxxxxxxxx@dokku-postgres-db:5432/db

This is in the format:

postgres://USERNAME:PASSWORD@dokku-postgres-db:5432/DATABASE_NAME

So overall we have the information:

  • IP Address: the IP address of your server
  • Port: 24321
  • Username: postgres
  • PW: xxxxxxxxxxxxxxxx
  • Database name: db

Just plug that into your database app and you're away.

Security (coming soon)

As soon as your VPS goes live you usually start getting hit with login attempts (likely from China). You can disable login entirely and rely on ssh keys, along with setting up a basic firewall (which blocks all ports except 80/443).

I'll add this one when I have time.

Posted on by:

Discussion

markdown guide
 

Nice writeup. I've also recently started using Dokku, and it is just perfect for low-maintenance software deployments. You mentioned sercuring your server. I use Linode for mine and they have a page just about this that I found very helpful: linode.com/docs/security/securing-... I really regret not using Dokku until now. I used to have my hobby software running in detached tmux terminals! the shame!

 

Nice thanks.

I recently deployed to google app engine flex... seems good and means I don't have to worry about security / maintenance (phew). Might write a post on it