DEV Community

Matthew Platts
Matthew Platts

Posted on • Edited on

Deploying Phoenix via Dokku

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. I use this for all the projects I build with my Phoenix boilerplate template.

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:
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
dokku domains:add card-tracker www.cardtracker.com.au
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

dokku config:set card-tracker WEB_HOST=cardtracker.com.au
Enter fullscreen mode Exit fullscreen mode

Redirect www to root

When someone hits www.cardtracker.com.au we want the response to be a 301 redirect to cardtracker.com.au. We need a server to be able to give this response - luckily Dokku has a plugin to manage this for us.

dokku plugin:install https://github.com/dokku/dokku-redirect.git
dokku redirect:set card-tracker www.cardtracker.com.au cardtracker.com.au
Enter fullscreen mode Exit fullscreen mode

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:enable 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

In the output you can find these lines:

Exposed ports:       5432->24321  
postgres://postgres:xxxxxxxxxxxxxxxx@dokku-postgres-db:5432/db
Enter fullscreen mode Exit fullscreen mode

This is in the format:

postgres://USERNAME:PASSWORD@dokku-postgres-db:5432/DATABASE_NAME
Enter fullscreen mode Exit fullscreen mode

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.

Backing up the database to S3

Sign up to Amazon.
Create a bucket "card-tracker-backups" - no public access.
Go to IAM and create a new user with programmatic access and full access to S3.
Grab the access key and secret.

dokku postgres:backup-auth db AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY
Enter fullscreen mode Exit fullscreen mode

Test backing up the db service to the BUCKET_NAME bucket on AWS.

dokku postgres:backup db BUCKET_NAME

=> 2021-05-06-00-59-06: The backup for xxx finished successfully.
Enter fullscreen mode Exit fullscreen mode

Schedule a backup. CRON_SCHEDULE is a crontab expression, eg. "0 3 * * *" for each day at 3am

dokku postgres:backup-schedule db "0 3 * * *" BUCKET_NAME
Enter fullscreen mode Exit fullscreen mode

File uploads

I found that if you allow file upload then you need to increase the file upload max limit in the nginx configuration. You can do it by sshing into the server and running:

echo 'client_max_body_size 50m;' > /home/dokku/<APP NAME>/nginx.conf.d/upload.conf

dokku ps:restart <APP NAME>
Enter fullscreen mode Exit fullscreen mode

So for me:

echo 'client_max_body_size 50m;' > /home/dokku/card-tracker/nginx.conf.d/upload.conf

dokku ps:restart card-tracker
Enter fullscreen mode Exit fullscreen mode

Security

Note: do the following at your own risk. I recommend researching security yourself instead of trusting this.

Check out VPS Harden, which will make your server much harder to crack for hackers.

I ran the install command on the remote server:

sudo git clone https://github.com/akcryptoguy/vps-harden.git && cd vps-harden && sudo bash get-hard.sh
Enter fullscreen mode Exit fullscreen mode

When asked I created a non-root user and disabled password login and root access. I kept the SSH port on 22 though as I was worried it might interfere with pushing code to dokku.

Afterwards I changed to my new login su - newuser.
Then I added my ssh public key:

mkdir ~/.ssh && touch ~/.ssh/authorized_keys
sudo chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys
sudo vim ~/.ssh/authorized_keys
Enter fullscreen mode Exit fullscreen mode

You can get your ssh key with cat ~/.ssh/id_rsa.pub locally.

Then you run this to restart ssh:

sudo systemctl restart sshd
Enter fullscreen mode Exit fullscreen mode

Keep this tab open and now try and ssh in from your local computer. ssh newuser@ipaddress. If this fails, keep troubleshooting until you sure you can login... since if you close the other open tab you've lost access to the server forever thanks to you disabling password login.

Once you can ssh into your newuser then you can just swap back to the root user with su - and run dokku commands.

Server monitoring

If you want to monitor your servers with netadata: https://www.vultr.com/docs/installing-netdata-on-linux-multiple-distros

Top comments (3)

Collapse
 
nickfun profile image
Nick F

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!

Collapse
 
mplatts profile image
Matthew Platts

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

Collapse
 
wdiechmann profile image
Walther Diechmann

"just what the doctor ordered" 😁

really great writeup with super detail level!