Dokku, Docker, and deployment

When you're starting out, it's good advice to not worry about build tooling and deployment. Create something worth shipping first.

At some point, however, I ended up with half a dozen projects strewn across various places on the internet: my original shared host (NearlyFreeSpeech), Heroku, Azure, and finally DigitalOcean, where this website lives. It was (and still is) messy, and potentially expensive. So when I found Dokku, which is advertised as your very own Heroku (Platform as a Service), I decided to dive in. I encountered a number of stumbling blocks which resulted in learning Docker and leveling up my bash skills, as Dokku is the largest bash project I've looked at.

First off, you probably need a virtual private server (VPS). I used the DigitalOcean Dokku one-click application. You might notice a dokku folder (/root/dokku) when you ssh into your VPS, but as of this post (June 2015) that is not the application: the application lives in /usr/local/bin with plugins in /var/lib/dokku/plugins. The repository in /root/dokku just allows for quick updates with git checkout, make install.

Knowing where the plugins really live is important because Dokku is made of plugins. Check out the source code if you don't believe me: the main app (source code permalink) has a bunch of calls to a "pluginhook" program with an argument that the docs call pluginhooks but is mostly lifecycle phases in the deployment process. The pluginhook program itself is a Go program (source code permalink) of less than 100 lines of code. It runs each file in the plugins folder with a filename equal to the argument that pluginhook is called with.

To illustrate this, running pluginhook install will run all the files in /var/lib/dokku/plugins/*/* named install. The command dokku plugins-install is nothing more than a wrapper around pluginhook install, as shown in line 180 of the source:

  plugins-install)
    pluginhook install
    ;; 

If you don't believe me, create a file named install, add echo 'hello world', insert it into any plugin folder, and enter the command pluginhook install (on your VPS) to see the echo. Insert a similar echo into another plugin folder to see both echos.1 To see which plugins have a given file, I run the following bash script with modifications.2

#!/usr/bin/env bash
# swap the "install" below for the appropriate name
for file in ./*; do  
  if ls "$file" | grep -q install; then
    echo "$file"
  fi
done  

Regardless of whether you understand how the program works, you can proceed to follow the official instructions to deploy an app and it might work. But those instructions say to check the configuration in the particular buildpack. As of June 2015 there is no link to buildpacks in the Dokku docs, but Heroku has a page on them. In my case I had to add a Procfile to get a node.js app to build properly. With another app, I added a bower plugin.

The next application was this ghost blog, which by default depends on an npm sqlite package, which requires a sqlite3 binary to install correctly. After some maneuvering, I determined that the buildpacks were simply too inflexible and moved to a Dockerfile. The buildpack method results in a Docker container, but as this post there is no official way to reverse engineer a container or image into a Dockerfile. But it's not too difficult to write one. This allows you to run whatever commands you need in creating the app such as bower install, sqlite3 installation, and so on. Be prepared to spend a few hours absorbing the official Docker documentation, but mine can get you started. Best practices include pinning packages to specific versions and minimizing the RUN commands, but you can check out one of mine in my Github repository.

The Dockerfile approach also allows you to set up persistent storage from your local Github repository. First, you specify a VOLUME in the Dockerfile which will be a folder in the host (your VPS), and be to sure to point your app to that folder as your storage location (in my case, the location of a sqlite database). By default, when you deploy dokku will execute docker run in its own way but Docker requires that you specify the storage when the Docker container is created using the run command with the -v flag (i.e., docker run [image] -v /host/dir:/container/dir). As noted in the Docker documentation on volumes and data management, customizing this is not available in a Dockerfile. But the docker-options default plugin (docs) allows you to customize how dokku runs the image.

By default, setting docker options requires that the app already be deployed. So you ssh in and run some dokku commands or create files such as DOCKER_OPTIONS_DEPLOY and DOCKER_OPTIONS_RUN. But I'd rather have these happen automatically when I push up. The solution is the plugin dokku-app-configfiles which will copy these config files to your app. I had to change the pluginhook from pre-build to post-build-dockerfile but after that it worked.

With all of these combined, you can destroy your app and ship it back up again with minimal work aside for waiting for it to build and perhaps using rsync to transfer a database file.

I didn't cover troubleshooting, which I should, as I ended up dropping my own logging into the program to figure it out and didn't have much luck using the output from bash's set -x. But for now this is more than enough to chew on. Good luck!

  1. The docs recommend that you run dokku plugins-install when adding a new plugin, but if the plugin does not have an install file (and many do not), that doesn't do anything. If you're wondering how to reverse changes made by the install, your best bet is to just look at the install file and figure out what it did.

  2. The most efficient way to use a script like this is to add it to your path, perhaps in a new folder ~/bin and execute it with a positional argument "$1" specifying the phase (eg, install). See the Bash Hackers Guide wiki for more on using bash.