Photo by CoWomen on Unsplash

Hosting hobby projects on Heroku is really easy to set up, and costs nothing, but there are a couple of problems with the free tier:

  • It takes time to restart a project that has gone "idle". I just checked a rails project of mine, and it took 23 seconds to respond. That’s fine for me, but if it’s a project I want to share with other people, that’s too long.
  • There are limitations on how long persistent connections will last, which can cause problems if you’re using websockets.
  • There are various other limits which can be problematic.

None of this is a criticism of Heroku – it’s great that they provide so much for free. It’s just that sometimes I want a bit more control, but I’m not always willing to spend $25 per month to host a hobby project.

Dokku

Dokku is a fantastic open-source option for hosting these projects.

It’s like "heroku in a box", so you can set up a single virtual machine (VM) that acts like your own personal Heroku, letting you manage your own apps.

Dokku applications don’t get put to sleep when they’re idle, so your response times should be the same, for the first request or the fiftieth.

The dokku installation instructions use an interactive install script. That’s great if you’re happy setting up your VM manually, but I like to have all my infrastructure defined and managed via code.

In this article, I’m going to show you how to set up a dokku server via Terraform.

I’m using DigitalOcean to host my VM (which they call a "droplet"), but it should be easy to adapt the code here for any other hosting platform that has a good terraform provider.

Pre-requisites

To work through this tutorial, you will need:

  • A Unix-like terminal environment (I’m on a Mac, which works fine. If you’re using a Windows machine, you’ll need something like cygwin, a Linux VM, or the Windows Bash Shell)
  • The Terraform CLI (I’m using version 0.14.3)
  • A DigitalOcean account

The DigitalOcean link above is a referral link which will give you $100 of credit to use over 60 days, and might earn me some credit eventually. If you don’t want to use that link, you can sign up via DigitalOcean.com

DigitalOcean API key

Before we can use terraform to manage DigitalOcean VMs, we need an API key.

Generate a new "Personal Access Token" (with both "read" and "write" scopes) via the DigitalOcean web interface.

Your API token can be used to create resources in your DigitalOcean account which will cost you money, so keep it secret like a password, and store it carefully.

Set up Terraform

In a new folder, create the following files:

versions.tf

terraform {
  required_version = ">= 0.14"
  required_providers {
    digitalocean = {
      source  = "digitalocean/digitalocean"
      version = "2.3.0"
    }
  }
}

This tells terraform that we’re using version 0.14 or higher, and to use version 2.3.0 of the DigitalOcean provider.

variables.tf

variable "api_token" {
  default = ""
}

This declares an api_token variable.

main.tf

provider "digitalocean" {
  token = var.api_token
}

This initialises the DigitalOcean provider with our API token.

At this point, you should be able to run:

terraform init

..and get a response something like this:

$ terraform init

Initializing the backend...

Initializing provider plugins...
- Finding digitalocean/digitalocean versions matching "2.3.0"...
- Using digitalocean/digitalocean v2.3.0 from the shared cache directory

...

Terraform has been successfully initialized!

...

Create a VM

We need a virtual machine to be our dokku server.

To create a VM, add this block to main.tf:

resource "digitalocean_droplet" "dokku-server" {
  image              = "ubuntu-20-04-x64"
  name               = var.dokku_hostname
  region             = "nyc1"
  size               = "s-1vcpu-1gb"
  monitoring         = true
  ipv6               = false
  private_networking = true
}

Add this block to variables.tf:

variable "dokku_hostname" {
  default = "dokku.me"
}

This creates the smallest VM ($5/month) in DigitalOcean’s New York datacentre, with the hostname dokku.me

The different values you can use for image and size are available via the DigitalOcean API.

I’ve disabled IPv6 because that can cause problems with [docker], which dokku uses.

Before we can run this, we need to supply the api_token variable to let Terraform manage resources in our DigitalOcean account. We’ll supply it as an environment variable.

export TF_VAR_api_token="<the DigitalOcean API token you created>"

Terraform makes any TF_VAR_whatever environment variables which exist available as the terraform variable whatever

Now run terraform plan, and you should see output like this:

$ terraform plan

...

Terraform will perform the following actions:

  # digitalocean_droplet.dokku-server will be created
  + resource "digitalocean_droplet" "dokku-server" {
      + backups              = false
      + created_at           = (known after apply)
      + disk                 = (known after apply)
      + id                   = (known after apply)
      + image                = "ubuntu-20-04-x64"
      + ipv4_address         = (known after apply)
      + ipv4_address_private = (known after apply)
      + ipv6                 = false
      + ipv6_address         = (known after apply)
      + locked               = (known after apply)
      + memory               = (known after apply)
      + monitoring           = true
      + name                 = "dokku.me"
      + price_hourly         = (known after apply)
      + price_monthly        = (known after apply)
      + private_networking   = true
      + region               = "nyc1"
      + resize_disk          = true
      + size                 = "s-1vcpu-1gb"
      + status               = (known after apply)
      + urn                  = (known after apply)
      + vcpus                = (known after apply)
      + volume_ids           = (known after apply)
      + vpc_uuid             = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

...

You can run terraform apply to actually create the VM, and you’ll see it in the DigitalOcean web interface. To destroy it again, run terraform destroy.

Running a VM on DigitalOcean costs money. Remember to destroy any resources you don’t need anymore, so that you don’t continue to get charged for them

SSH access

We will need SSH access to our new VM to run dokku commands.

Generate a new SSH keypair, with no password, like this:

ssh-keygen -t rsa -b 4096 -P "" -f ~/.ssh/terraform_dokku

You can set a password for your SSH key if you prefer. I find it easier to use passwordless keys for servers I’m going to manage via code

Change variables.tf to look like this:

variable "api_token" {
  default = ""
}

variable "dokku_hostname" {
  default = "dokku.me"
}

variable "public_key_file" {
  default = "~/.ssh/terraform_dokku.pub"
}

Change main.tf to look like this:

main.tf:

provider "digitalocean" {
  token = var.api_token
}

resource "digitalocean_ssh_key" "terraform_dokku" {
  name       = "Terraform Dokku"
  public_key = file(var.public_key_file)
}

resource "digitalocean_droplet" "dokku" {
  image              = "ubuntu-20-04-x64"
  name               = var.dokku_hostname
  region             = "nyc1"
  size               = "s-1vcpu-1gb"
  monitoring         = true
  ipv6               = false
  private_networking = true
  ssh_keys           = [digitalocean_ssh_key.terraform_dokku.fingerprint]
}

output "droplet_ip" {
  value = digitalocean_droplet.dokku.ipv4_address
}

This tells terraform to:

  • Create an SSH key on DigitalOcean using the public key from the pair we just created
  • Install it on the VM it creates
  • Output the IP address of the VM

If you run terraform apply you should see output ending in something like this (your droplet_ip value will be different):

...
digitalocean_droplet.dokku: Creation complete after 38s [id=225498688]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Outputs:

droplet_ip = "161.35.123.241"

Now, you should be able to SSH onto the new VM like this:

ssh -i ~/.ssh/terraform_dokku root@161.35.123.241

Installing Dokku

To install dokku, we’re going to use a modified version of the shell script from the debian install guide.

The shell script needs to know the hostname of the server it’s running on. We defined that as a terraform variable, so we don’t want to hard-code it into our script. We’ll use the Terraform template_file function to supply the hostname at runtime.

Create this file:

install-dokku.sh.tpl

#!/bin/bash

# Add 2G of swap memory
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile

# install docker
wget -nv -O - https://get.docker.com/ | sh

# setup dokku apt repository
wget -nv -O - https://packagecloud.io/dokku/dokku/gpgkey | apt-key add -

export SOURCE="https://packagecloud.io/dokku/dokku/ubuntu/"
export OS_ID="$(lsb_release -cs 2>/dev/null || echo "bionic")"
echo "xenial bionic focal" | grep -q "$OS_ID" || OS_ID="bionic"
echo "deb $SOURCE $OS_ID main" | tee /etc/apt/sources.list.d/dokku.list

apt-get update

# set options for non-interactive install
echo "dokku dokku/web_config boolean false" | debconf-set-selections
echo "dokku dokku/vhost_enable boolean true" | debconf-set-selections
echo "dokku dokku/nginx_enable boolean true" | debconf-set-selections
echo "dokku dokku/skip_key_file boolean true" | debconf-set-selections

# Use the same SSH key for `root` and the `dokku` user
echo "dokku dokku/key_file string /root/.ssh/id_rsa.pub" | debconf-set-selections

# We'll supply `hostname` via terraform
echo "dokku dokku/hostname string ${hostname}" | debconf-set-selections

# install dokku
apt-get install dokku -y
dokku plugin:install-dependencies --core

# add plugins
dokku plugin:install https://github.com/dokku/dokku-postgres.git  # For postgres databases
dokku plugin:install https://github.com/dokku/dokku-letsencrypt.git  # For SSL certificates
dokku letsencrypt:cron-job --add  # To auto-renew SSL certificates

# Enable ssh access for the `dokku` user
cat /root/.ssh/authorized_keys >> /home/dokku/.ssh/authorized_keys

As well as dokku itself, this script installs a couple of plugins so that we can create PostgresQL databases, and LetsEncrypt SSL certificates for our web applications.

We’ll use the user_data feature of the DigitalOcean terraform provider to execute our install script when the VM is created.

Change the main.tf file to look like this:

main.tf:

provider "digitalocean" {
  token = var.api_token
}

resource "digitalocean_ssh_key" "terraform_dokku" {
  name       = "Terraform Dokku"
  public_key = file(var.public_key_file)
}

resource "digitalocean_droplet" "dokku" {
  image              = "ubuntu-20-04-x64"
  name               = var.dokku_hostname
  region             = "nyc1"
  size               = "s-1vcpu-1gb"
  monitoring         = true
  ipv6               = false
  private_networking = true
  ssh_keys           = [digitalocean_ssh_key.terraform_dokku.fingerprint]
  user_data = templatefile("install-dokku.sh.tpl", { hostname = var.dokku_hostname })
}

output "droplet_ip" {
  value = digitalocean_droplet.dokku.ipv4_address
}

This line:

  user_data = templatefile("install-dokku.sh.tpl", { hostname = var.dokku_hostname })

…tells terraform to provide the content of install-dokku.sh.tpl as user_data to the server, but replace ${hostname} in the script with the value of the dokku_hostname terraform variable.

If you run this code with terraform apply it will return within about 30 seconds as before, but your new VM will take several minutes to finish running the dokku installation script.

When the script has finished, you should be able to SSH onto the VM and run this:

# dokku version
dokku version 0.22.7

In the next post, I’ll show you how to use your new dokku server to host a rails application with a database, and set up an SSL certificate using the LetsEncrypt plugin.

Don’t forget to clean up using terraform destroy, to ensure you don’t get charged for resources you’re not using.

One thought on “Setting up a Dokku server on DigitalOcean with Terraform

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s