Setting Up Your Own PAAS Using Dokku

These are notes about how we setup Dokku on EC2 with Vagrant, Docker and Dokku.

Note: an earlier version of this post mentioned that the maintainers of Dokku worked at Deis, however that's incorrect — previously Deis sponsored the project, but this has now finished, and none of the maintainers work at or have worked at Deis.

Also, it appears that a custom Buildpack is no longer needed for releasing a static site (though use of the plugin mentioned below won't break the deploy)

What is Dokku?

Dokku allows us to spin up our own Platform-as-a-Service (PaaS) and create a Heroku-style environment that's cheap, and that we can easily deploy to simply by performing a git push.

Under the covers, Dokku uses Nginx to serve sites, creating a reverse proxy Nginx config for each app that we deploy. Dokku also installs a command-line tool that allows us to configure and stop / start our applications.

We can deploy apps written using a range of technologies, including Node.js, PHP, Ruby-on-rails, and even static sites, and Dokku supports a variety of mechanisms to determine how to deploy an app, including:

  • the presence of a package.json file, letting Dokku know that it's dealing with a node.js app
  • Procfiles
  • Docker files
  • Heroku-style Buildpacks

Dokku versus Dokku-alt

Our approach described below is based on the tutorial by Alex MaCaw at ClearBit, but we'll amend this to use Dokku rather than Dokku-alt.

Dokku-alt arose because people wanted Dokku installed complete with a range of verified plugins, and because the Dokku project wasn't being maintained after the founder, Jeff Lindsay, stepped aside.

However, the situation has now reversed, with Dokku receiving active support from it's new maintainers, and Dokku-alt now a number of versions behind Dokku.

Jeff Lindsay described his experience with stepping aside from Dokku and he left the new maintainers a roadmap for future development which the maintainers have been following.

Recent Dokku features include:

  • zero-downtime deployment, with Dokku waiting a configurable amount of time before routing traffic to from your old container to your newly-deployed container
  • an increasing range of plugins
  • the ablity to specify deploy-time checks (such as text appearing at a given URL) to ensure that
    the deploy worked correctly
  • the ability to rename a deployed app

Amazon EC2 instance setup

We presume that you've already setup an AWS account, and have retrieved your AWS_ACCESS_KEY_ID and your AWS_SECRET_ACCESS_KEY.

We need to create our EC2 keypair as aws, and save the aws.pem to ~/.ssh/aws.pem.

Then, we create our EC2 Security Group as production, adding the rules to allow incoming traffic from port 22 (SSH) and port 80 (HTTP).

Next, set environment variables for our AWS_ACCESS_KEY_ID and our AWS_SECRET_ACCESS_KEY:

export AWS_ACCESS_KEY_ID=WHATEVER
export AWS_SECRET_ACCESS_KEY=WHICHEVER

These can be set in a .bash_profile or similar.

Create a new t2.micro instance — in our case we'll use the region ap-southeast-2.
Then, in the AWS dashboard, access the instance, and check the public IP address, which we'll need later to set our DNS:

52.62.179.58

Note that, depending on the apps you'll be deploying, you may need to upgrade to a larger EC2 instance.

Now, we need to determine the AMI instance to use. Use the Amazon EC2 AMI Locator
to determine the correct ami-id — scroll down to the bottom of the locator and use the dropdowns to filter the available instances.

We'll need one that has:

  • region: ap-southeast-2
  • version: 14.04 LTS
  • instance type: hvm:ebs

Originally, when I ran the Vagrantfile below, I received the following message:

InvalidParameterValue => Value () for parameter groupId is invalid

The problem turned out to be that I was specifying an aws.ami id that was not valid for the specified aws.region — using the Amazon EC2 AMI Locator allowed me to find a suitable AMI id.

Add an entry accordingly to our /etc/hosts file — here we presume we'll be using the domain dokku.me, as we'll be using our hosts file to specify our DNS, but you can use a real (i.e. registered) domain if you'd like to make your dokku box accessiable via the public web.

52.62.179.58	dokku.me

Vagrant setup

Install the vagrant plugin for aws:

vagrant plugin install vagrant-aws

We'll retrieve the following Vagrantfile:

curl https://gist.githubusercontent.com/jcdarwin/e1991320f1c4f3d1db2f/raw/d5e2796a3c632ec63e8a503501ead7af5744d6c2/Vagrantfile > Vagrantfile

Our Vagrantfile is as follows:

Vagrant::configure('2') do |config|
	config.vm.define :dokku, autostart: false do |box|
		box.vm.box      = 'trusty'
		box.vm.box_url  = 'https://github.com/mitchellh/vagrant-aws/raw/master/dummy.box'
		box.vm.hostname = 'dokku.me'

		box.vm.provider :aws do |aws, override|
			aws.access_key_id     = ENV['AWS_ACCESS_KEY_ID']
			aws.secret_access_key = ENV['AWS_SECRET_ACCESS_KEY']
			aws.keypair_name      = 'aws'

			# As per the Amazon EC2 AMI Locator
			aws.ami               = 'ami-4f32142c'
			aws.region            = 'ap-southeast-2'
			aws.instance_type     = 't2.micro'

			# Customize this to the id of the security group you're using
			aws.security_groups   = 'production'

			# To mount EBS volumes
			aws.block_device_mapping = [
				{
					:DeviceName => "/dev/sdb",
					:VirtualName => "ephemeral0"
				},
				{
					:DeviceName => "/dev/sdc",
					:VirtualName => "ephemeral1"
				}
			]

			override.ssh.username = 'ubuntu'

			# Customize this to your AWS keypair path
			override.ssh.private_key_path = '~/.ssh/aws.pem'
		end

		# To make sure we use EBS for our tmp files
		box.vm.provision "shell" do |s|
			s.privileged = true
			s.inline = %{
				mkdir -m 1777 /mnt/tmp
				echo 'export TMPDIR=/mnt/tmp' > /etc/profile.d/tmpdir.sh
			}
		end

		# To make sure packages are up to date
		box.vm.provision "shell" do |s|
			s.privileged = true
			s.inline = %{
				export DEBIAN_FRONTEND=noninteractive
				apt-get update
				apt-get --yes --force-yes upgrade
			}
		end

		# Install dokku as per http://dokku.viewdocs.io/dokku/getting-started/install/debian/
		box.vm.provision "shell" do |s|
			s.privileged = true
			s.inline = %{
				export DEBIAN_FRONTEND=noninteractive

				echo "install prerequisites"
				sudo apt-get update -qq > /dev/null
				sudo apt-get install -qq -y apt-transport-https

				echo "setup unattended installation"
				echo "dokku dokku/vhost_enable boolean true" | sudo debconf-set-selections
				echo "dokku dokku/hostname string dokku.me" | sudo debconf-set-selections

				echo "install docker"
				sudo apt-get install lxc wget bsdtar curl
				sudo apt-get install linux-image-extra-$(uname -r)
				sudo modprobe aufs
				sudo usermod -aG docker ubuntu
				wget -nv -O - https://get.docker.com/ | sh

				echo "install dokku"
				wget -nv -O - https://packagecloud.io/gpg.key | apt-key add -
				echo "deb https://packagecloud.io/dokku/dokku/ubuntu/ trusty main" | sudo tee /etc/apt/sources.list.d/dokku.list
				sudo apt-get update -qq -y > /dev/null
				sudo apt-get install -qq -y dokku
				sudo dokku plugin:install-dependencies --core
                sudo dokku plugin:install https://github.com/dokku/dokku-letsencrypt.git
			}
		end
	end
end

Note that our box_url is a blank Vagrant box to coax Vagrant into working with AWS — we can add our box manually:

vagrant box add dummy https://github.com/mitchellh/vagrant-aws/raw/master/dummy.box

Inspect the metadata:

cat ~/.vagrant.d/boxes/dummy/0/aws/metadata.json

We should see something like:

{
    "provider": "aws"
}

Create our EC2 instance using Vagrant

Start up our VM:

vagrant up dokku --provider=aws

Our Vagrantfile will create the EC2 instance, and run the provisioning script.

Once Vagrant finishes provisioning the box, you should be able to ssh into it using Vagrant:

vagrant ssh dokku

Note that if you experience any problems with the provisioning, you may need to check that Dokku and the core plugins installed correctly:

sudo apt-get install dokku
sudo dokku plugin:install-dependencies --core

While in our box, change /home/dokku/HOSTNAME and /home/dokku/VHOST from the Amazon-generated name (e.g. ip-172-31-3-31.ap-southeast-2.compute.internal) to our domain:

dokku.me

We can now exit from our ssh session.

Complete Dokku setup

We should now be able to finish the Dokku setup using Dokku's web interface at http://dokku.me

  • change our hostname from the IP address to our domain: dokku.me
  • choose Use virtualhost naming for apps

Setup direct ssh access

We want to be able to ssh into our box without relying on Vagrant.

First, register our normal public key with our box:

cat ~/.ssh/id_rsa.pub | vagrant ssh dokku -- sudo sshcommand acl-add dokku ${USER}

Check the ssh config for our box:

vagrant ssh-config dokku

Host dokku
  HostName ec2-52-62-179-58.ap-southeast-2.compute.amazonaws.com
  User ubuntu
  Port 22
  UserKnownHostsFile /dev/null
  StrictHostKeyChecking no
  PasswordAuthentication no
  IdentityFile /Users/jasondarwin/.ssh/aws.pem
  IdentitiesOnly yes
  LogLevel FATAL

We can use the reported HostName for direct ssh access to our box:

# This will print out the Dokku help menu
ssh dokku@ec2-52-62-179-58.ap-southeast-2.compute.amazonaws.com

# We can run commands over ssh
ssh dokku@ec2-52-62-179-58.ap-southeast-2.compute.amazonaws.com version

Deploy our app

We'll use the heroku node-js-sample app to prove that we can deploy an app to our dokku box:

# Clone the repo
git clone https://github.com/heroku/node-js-sample
cd node-js-sample

# Add our Dokku box as a remote:
git remote add dokku dokku@ec2-52-62-179-58.ap-southeast-2.compute.amazonaws.com:<our app name>

# Push our repo to our box and set off the deploy
git push dokku master

Add an entry to our hosts file for our app:

52.62.179.58	node-js-sample.dokku.me

We should now be able to access our app at http://node-js-sample.dokku.me

dokku config:set --no-restart node-js-sample DOKKU_LETSENCRYPT_EMAIL=<email>
dokku letsencrypt node-js-sample

More notes about application deployments can be found in the Dokku documentation.

Further

  • Dokku has support for plugins which allow
    functionality such as database setup and access.
  • Note also that web applications, like the sample nodejs app, are expected to be exposed through port 5000 unless an environment variable is specified.
  • We can use Florian Heinemann's custom
    buildpack
    to deploy a static site.
  • Note that we can run docker directly on our box, e.g.
    vagrant ssh dokku
    docker run hello-world
  • Check the Dokku docs for further information