Flavio Poletti bio photo

Flavio Poletti

Irreducible Perler.

Email Comics Twitter LinkedIn MetaCPAN Github Stackoverflow Pinterest

Do you like Platform as a Service? Ever wondered about rolling your own, especially if you have some capacity that you don’t use and you are always struggling deploying your stuff? Meet Dokku, [t]he smallest PaaS implementation you’ve ever seen.

Table of Contents

Platform as a Service?

Wikipedia describes Platform as a Service (abbreviated PaaS) like this:

Platform as a service (PaaS) is a category of cloud computing services that provides a platform allowing customers to develop, run, and manage applications without the complexity of building and maintaining the infrastructure typically associated with developing and launching an app.

One of the most widely used PaaS platforms that I know of is Heroku. Once you set up your project on Heroku (which takes very little energy with a few commands and configuration files), the promise is that your development/deployment workflow will be this:

# hack on your code
$ vi app.pl

# commit your changes
$ git commit app.pl -m 'Add killer feature'
[master 7bb308e] Add killer feature
1 file changed...

# push new commit(s) to the repository in Heroku
$ git push heroku master
Counting objects: 6, done.
# ... several lines of automated deployment...
=====> Application deployed:
       http://sample-mojo.example.com

So yes, the promise is that all your deployment effort is just the git push command. Nifty, uh?

Is it right for me?

Should you use this? It depends on you, what your goals are and what level of control you want to retain. Do you have a personal project, or are just starting a new one and you want to get something up and running before you lose momentum? Then it’s probably for you. Do you have a well-established project with some complex gating rules for going in production? Chances are you already have something in place, and switching would not be beneficial.

Unless… you want to have a sandbox/staging environment where you want to be able to hack quickly. So, there are plenty of occasions in which PaaS can be beneficial, especially if you want to concentrate on the coding part and have little to no resources to take care of the system management part.

Public PaaS services like Heroku come with a cost, of course. You might be a shiny new startup in Silicon Valley and got a voucher for one of them, of course; in this case you just have to reclaim your voucher. Like other cloud services (e.g. IaaS), it’s a nice way for investors to give you money that they know will be invested in the infrastructure for your services, instead of parties.

For the average people in the rest of the world, anyway, the story can be different. Whether it’s the right choiche to shell out money for a PaaS service or not is something that only you can assess; it’s good to know about it anyway. And also know what the alternatives are.

I like it, but I have this server…

Doubts about using PaaS are especially fit if you already have capacity that you are using, or planning to use, for your project. It feels like a waste of resources, doesn’t it? This is where DIY PaaS projects come to the rescue: they allow you to setup a PaaS-like workflow but leveraging on infrastructure that you might already have (or that you feel more comfortable with).

One use case I find particularly useful is for test services or very little personal projects. Dynos in Heroku each have a cost, independently of how much resources they actually consume: in my private PaaS I can easily stuff a lot of them inside a single VPS, spending a lot less money. The drawback is more administration on my side and less robustness, of course.

Dokku

Dokku is a tiny PaaS platform that you can install on your server. Note that I used the singular form: it’s meant for very restricted environments where all you have and need is one single server; nothing important or production that you care too much, so.

It is described as:

The smallest PaaS implementation you’ve ever seen

and rightly so: it’s a smart integration of other tools that can keep the project itself up in a few (well, a couple thousand) lines of Bash code.

It’s easy to get started so I will not repeat the official steps here.

If you want to give it a try, you can also head to Digital Ocean and use one of their one-click apps. I don’t completely like this approach because their deployment is Ubuntu (I tend to like Debian more) and their Dokku version is 0.6.5 (there’s 0.7.2 around) right now.

You can find a small project dokku-boot on GitHub that will help you set up 0.7.2 on a new Debian instance (it should work just fine on Ubuntu too). It’s more or less a simple wrapper around the main installation instructions, but it also installs a couple of handy extensions for Dokku, like support for Let’s Encrypt (for free and hassle-free SSL certificates), Redis and Postgres.

From the README:

  • Spin up a new VPS somewhere, e.g. Digital Ocean. I usually choose the latest Debian release. You can select the smallest size if you just want to give it a try. (And please… set up and use SSH keys, it’s 2016 or later!). Let’s say we save the IP address of this VPS in variable DOKKU_IP

  • Log in a shell in the VPS as user root and run:

apt-get update
apt-get install -y curl perl
curl -LO https://github.com/polettix/dokku-boot/raw/master/dokku-boot.pl
perl dokku-boot.pl
  • Wait for installation to complete, then go to http://$DOKKU_IP/ and complete the setup of Dokku.

You end up with a reasonably close system (only ports 22, 80 and 443 will be open for incoming traffic). The last point can be somehow commented further.

You will land in a page where the SSH keys you defined for user root are used by default to initialize access to user dokku too. If you defined more keys, only the first one will actually be used, so ensure that the one you will use is actually the first one.

Next, you can define the hostname. Here we will assume that you do own something registered directly after a public suffix. You can do most of what we describe in the following if you don’t (actually, the only thing you will struggle with is the Let’s Encrypt plugin). As of October 2016, owning one such domain can cost as low as about 3$ (ClouDNS usually has a few offers in their pricing list). In this example we will assume to own the ever-present example.com.

After this, you get to decide how your application will appear to the world. There are two alternatives:

  • Take the form application-name.example.com. This is probably the most elegant, but it is available only if you actually set a hostname and not an IP address. We will stick with this as we are assuming to own example.com (which we don’t… of course!)
  • Take the form <domain-or-ip>:<port>, where Dokku will allocate a port for each application. This will not be described here, as it requires some additional settings on the iptables firewall rules for letting the ports to be accessible from the outside.

After all of this, you can just submit the form in the page and you will be all set (you will be redirected to Dokku’s documentation page after this step).

In the case you actually own a domain and you have control over its DNS settings (as in our example), you should also set up a wildcard resolution towards your Dokku installation, with a A record like this:

*.example.com IN A $DOKKU_IP

where $DOKKU_IP must be expanded with your installation’s IP address, of course. This setting will guarantee that a resolution for someapp.example.com will actually be pointed towards your personal PaaS system.

Work On A Project

Now you have your personal PaaS instance up and running, waiting for your code to come. I’m a Perl enthusiast, so my example will be in Perl.

In the following sub-sections, we’ll see:

  • a few assumptions about how your project is structured
  • what you have to do for connecting it to your Dokku instance
  • enjoy!

Starting point

Our starting point is a project that is tracked with git. In this example, we will assume that we want to work on a simple Hello World! application (what else) built with Mojolicious, and we will see a couple of directions for evolving to a more complex application. You can find the whole example in this repository in GitHub if you want to skip its construction.

The application itself (in file app.pl) is quite straightforward:

#!/usr/bin/env perl
use Mojolicious::Lite;
get '/' => sub {
my $c = shift;
   $c->res->headers->content_type('text/plain');
   $c->render(text => "Hello, World!\n");
};
app->start;

You are probably already using something to track the automation of installing the dependencies, we will use a cpanfile because it’s used by both cpanm and carton (the former being quite important for our later automation, as we will see). It’s quite straightforward in this case:

requires 'Mojolicious', '7.08';

We can install the module locally and check that it works actually:

$ cpanm -L local --notest --quiet --installdeps .
Successfully installed IO-Socket-IP-0.38
Successfully installed Mojolicious-7.08
2 distributions installed

$ alias ploc="perl -I'$PWD'/local/lib/perl5"

$ ploc app.pl get /
[Sat Oct  8 18:40:34 2016] [debug] GET "/"
[Sat Oct  8 18:40:34 2016] [debug] Routing to a callback
[Sat Oct  8 18:40:34 2016] [debug] 200 OK (0.000391s, 2557.545/s)
Hello, World!

We only lack tracking it with git at this point:

# https://hackernoon.com/lesser-known-git-commands-151a1918a60
# see hint about `git it`
$ git init . && git commit -m Root --allow-empty

$ git add . && git commit -m 'Import initial files'

Up to this point there was almost no overhead related to Dokku: we would have written or application, installed the needed modules and tracked changes with git anyway.

PaaS project setup in Dokku

Dokku mostly works sending commands via ssh to the user dokku in the machine where it is installed. For this reason, it’s useful to define the following alias:

$ alias dokku="ssh 'dokku@$DOKKU_IP'"

The following steps are needed whatever application you want to deploy via

# we will call our application "sample-mojo"
$ dokku apps:create sample-mojo

# we assume to be in our project sample-mojo directory
$ git remote add dokku "dokku@$DOKKU_IP:sample-mojo"

Are we ready for the first push? Well… not yet. We still have to fix a couple things:

  • how will Dokku figure out how to build our project? Like installing the right modules, etc.?
  • how will it know what to run?

It turns out the heroku-buildpack-perl-procfile project on GitHub can ease our life for both steps, let’s see how.

How to build: .buildpacks

Heroku and Dokku don’t support Perl applications out of the box. I know, it’s a shame, but it’s easy to fix this via so-called custom buildpacks (which are the same as Heroku’s) and many different clever persons did this in slightly different ways.

The philosophy under heroku-buildpack-perl-procfile is to make no assumption on your application, apart that it’s written in Perl and will need modules from either CPAN or from some project-local directory.

If you mostly stick with web projects and you want to run them via Starman, you will probably want to look into some other package. I wanted to keep control, so evolved the awesome original heroku-buildpack-perl-procfile by kazeburo to suit my needs.

To tell Dokku that you want to use a buildpack you have a couple options (see custom buildpacks for some alternatives), in our case we will just add a .buildpacks file in our project’s root directory:

# in the project's root directory
$ echo https://github.com/polettix/heroku-buildpack-perl-procfile.git \
     > .buildpacks

$ git add .buildpacks && git commit -m 'Add .buildpacks for Dokku builds'

How to run: Procfile

The standard way to describe an application in Heroku is via a so-called Procfile (so-called in this context means that you MUST call it with the initial uppercase an the rest, otherwise it will not work!). It contains a line for each component of your application, so that you can have e.g. a web part, a database part, some workers and so on.

You can name components with almost any reasonable name (e.g. use alphanumerics or look for the rules). Dokku assign no particular semantic to the names, except to web which has (at least) two attached strings:

  • one instance of a web service is always started initially (unless explicitly configured differently or no web is defined)
  • web instances are accessible from the outside, there’s an nginx reverse proxy that is set-up automatically.

We will start with one single web component, so our Procfile is quite simple:

web: perl ./app.pl daemon --listen "http://*:$PORT"

As you can see, when Dokku starts a new web instance, it also passes the PORT environment variable so that your application can bind to the correct port. How this parameter is used is dependent on the specific application and/or framework that the application is using.

As for the .buildpacks file, we have to add Procfile to our git project and commit:

$ git add Procfile && git commit -m 'Add Procfile for Dokku runs'

Ready!

At this point, we are ready for our first push to our Dokku instance, YAY! In our first push we will also set the --set-upstream so that our following pushes of master will be automatically sent to the remote dokku:

$ git push --set-upstream dokku master
Counting objects: 6, done.
# ... several lines of automated deployment...
=====> Application deployed:
       http://sample-mojo.example.com

To dokku@example.com:sample-mojo
* [new branch]      master -> master
Branch master set up to track remote branch master from dokku.

Now we are ready to send the very first HTTP request towards our new service. As you can see, there’s a couple of lines that tells us where the service lives:

=====> Application deployed:
http://sample-mojo.example.com

So, it’s easy to send the request:

$ curl http://sample-mojo.example.com
Hello, World!

Seems to work!

At this point you can start hacking on your application and follow the pattern we described in the beginning:

# hack on your code
$ vi app.pl

# commit your changes
$ git commit app.pl -m 'Add killer feature'
[master 7bb308e] Add killer feature
1 file changed...

# push new commit(s) to the repository in Heroku
$ git push heroku master
Counting objects: 6, done.
# ... several lines of automated deployment...
=====> Application deployed:
http://sample-mojo.example.com

Moar! Moar! Moar!

You can get tired of your single-service application pretty soon. Want to add a database and cannot live with SQLite for too long? Need to add some worker process to handle housekeeping or long-running jobs? Concentrating all in a single instance of a web thingie is clearly not the right way to go unless you’re out of headaches.

This is where your Procfile comes to the rescue. For every kind of additional service, you just need to add a line with the name of the service and the command line to start it.

First, evolve the code…

In our example, we will add support for a Minion worker trying to replicate the example in the DESCRIPTION, with just a little twist in the database used (we’re going to use SQLite in this example). This is how we transform our app.pl into:

#!/usr/bin/env perl
use Mojolicious::Lite;

plugin Minion => {SQLite => 'sqlite:test.db'};

# Slow task
app->minion->add_task(poke_mojo => sub {
 my $job = shift;
 $job->app->ua->get('mojolicious.org');
 $job->app->log->debug('We have poked mojolicious.org for a visitor');
});

# Perform job in a background worker process
get '/' => sub {
 my $c = shift;
 $c->minion->enqueue('poke_mojo');
 $c->render(text => 'We will poke mojolicious.org for you soon.');
};

app->start;

Of course we will need to make sure that the needed plugins are correctly installed, so our cpanfile becomes:

requires 'Mojolicious', '7.08';
requires 'Minion', '6.0';
requires 'Minion::Backend::SQLite', '0.007';

Again, we can install them locally and make sure that everything works fine:

$ cpanm -L local --notest --quiet --installdeps .
...

Now, let’s make a little local test: we first generate a get to the regular web application, then we start the minion (we might make it in two different shells but it will work anyway because Minion is decoupled through the database):

$ alias ploc="perl -I'$PWD'/local/lib/perl5"

$ ploc app.pl get /
[Sat Oct  8 22:53:42 2016] [debug] GET "/"
[Sat Oct  8 22:53:42 2016] [debug] Routing to a callback
[Sat Oct  8 22:53:42 2016] [debug] 200 OK (0.012938s, 77.292/s) 
We will poke mojolicious.org for you soon.

$ ploc app.pl minion worker
[Sat Oct  8 22:53:47 2016] [debug] Worker 18861 started
[Sat Oct  8 22:53:47 2016] [debug] Checking worker registry and job queue
[Sat Oct  8 22:53:47 2016] [debug] Performing job "1" with task "poke_mojo" in process 18862
[Sat Oct  8 22:53:47 2016] [debug] We have poked mojolicious.org for a visitor

Then, add a new service…

Now, we make sure that the Minion worker is defined in Procfile:

web:    perl ./app.pl daemon --listen "http://*:$PORT"
minion: perl ./app.pl minion worker

Then, learn about scaling…!

At this stage, we just need to commit and push, right? Let’s see:

$ git commit -am 'Evolve example to use Minion'
[master af4ba9d] Evolve example to use Minion
3 files changed, 18 insertions(+), 3 deletions(-)

$ git push
Counting objects: 9, done.
#... some building happens here...
-----> Discovering process types
       Procfile declares types -> web, minion
#... something else happens...
=====> web=1
-----> Attempting...

So, it seems that Dokku knows about our minion service type, but it starts none. Now you understand what we meant before when we said that web is special because Dokku starts one by default!

A quick look to the current scaling setup is worth the time as a double check:

$ dokku ps:scale sample-mojo
-----> Scaling for sample-mojo
-----> proctype           qty                                                                          
-----> --------           ---                                                                          
-----> web                1

So… no minion, no party? Fortunately, ps:scale is not only for querying the current status, but for changing it as well; let’s bump minion then:

$ dokku ps:scale sample-mojo minion=1
-----> Scaling sample-mojo:minion to 1
#... some lines...
=====> web=1
=====> minion=1
#... some lines...
=====> Application deployed:
       http://sample-mojo.example.com

Now that’s right!

Then, we think we’re ready but

Are we ready now? Well… let’s see!

$ curl http://sample-mojo.example.com
We will poke mojolicious.org for you soon.

# now we wait a few seconds to give the worker process the time get hold of the
# new task. The number of seconds might be reduced but let's play safely
$ sleep 5

$ dokku logs sample-mojo
2016-10-08T21:10:42.160743389Z app[minion.1]: [Sat Oct  8 21:10:42 2016] [debug] Worker 8 started
2016-10-08T21:10:42.182393541Z app[minion.1]: [Sat Oct  8 21:10:42 2016] [debug] Checking worker registry and job queue
2016-10-08T21:10:30.978550240Z app[web.1]: [Sat Oct  8 21:10:30 2016] [info] Listening at "http://*:5000"
2016-10-08T21:14:44.107362361Z app[web.1]: [Sat Oct  8 21:14:44 2016] [debug] GET "/"
2016-10-08T21:14:44.108955921Z app[web.1]: [Sat Oct  8 21:14:44 2016] [debug] Routing to a callback
2016-10-08T21:14:44.127066080Z app[web.1]: [Sat Oct 8 21:14:44 2016] [debug] 200 OK (0.019002s, 52.626/s)

Uhm… the worker seems still unaware of the request from the frontend… what’s happening?

It turns out that it’s a database issue here. When we run the example in our development box, both the web service and the minion worker were running in the same directory of the same box. On the other hand, in this case the two are running inside two separate Linux containers created via Docker, so it’s like they are running in separate hosts.

Fact is that SQLite is a single file database: for our system to work, both the web and the minion services MUST operate on the same file! Now you understand why dokku-boot includes Postgres and Redis plugins, don’t you?

It will not take long now!

The problem still remains though: how do I manage to share files/directory across services? That’s simple… use persistent storage! They are directories created in the Dokku node that will be mounted in your services, let’s see how to do this.

First, we need a place where our shared directory will live. It MUST be accessible with full permissions by user dokku, so we will stick with the best practice (as of October 2016, at least) of using /var/lib/dokku/data/storage and we will ask Dokku to use sub-directory sample-mojo inside it:

$ dokku storage:mount sample-mojo \
     /var/lib/dokku/data/storage/sample-mojo:/app/shared

The command above tells us that the host directory /var/lib/.../sample-mojo will be mapped to directory /app/shared inside the containers (all of them) where the application will run.

As of version 0.7.2, you don’t need to create the shared directory beforehand: Dokku will take care to create one for you. The directory will be created only when the first container will need it.

Now we just have to tell our application where to put/look for the shared database, which is a single-line change:

$ git diff
diff --git a/app.pl b/app.pl
index 70b7418..25183d8 100755
--- a/app.pl
+++ b/app.pl @@ -1,7 +1,7 @@ #!/usr/bin/env perl use Mojolicious::Lite;
 
-plugin Minion => {SQLite => 'sqlite:test.db'};
+plugin Minion => {SQLite => 'sqlite:/app/shared/test.db'};
 
 # Slow task
 app->minion->add_task(poke_mojo => sub {

$ git commit -am 'Change position of Minion database'
[master 8a139bc] Change position of Minion database
 1 file changed, 1 insertion(+), 1 deletion(-)

Now let’s push and try again:

$ git push
#... wait for it...

$ curl http://sample-mojo.example.com/
We will poke mojolicious.org for you soon.

# now we wait a few seconds to give the worker process the time get hold of the
# new task. The number of seconds might be reduced but let's play safely
$ sleep 5

$ dokku logs sample-mojo
2016-10-08T21:53:17.079481775Z app[minion.1]: [Sat Oct  8 21:53:17 2016] [debug] Worker 7 started
2016-10-08T21:53:17.106477103Z app[minion.1]: [Sat Oct  8 21:53:17 2016] [debug] Checking worker registry and job queue
2016-10-08T21:54:42.166105068Z app[minion.1]: [Sat Oct  8 21:54:42 2016] [debug] Performing job "1" with task "poke_mojo" in process 143
2016-10-08T21:54:42.245450605Z app[minion.1]: [Sat Oct  8 21:54:42 2016] [debug] We have poked mojolicious.org for a visitor
2016-10-08T21:53:05.831553141Z app[web.1]: [Sat Oct  8 21:53:05 2016] [info] Listening at "http://*:5000"
2016-10-08T21:54:41.207238916Z app[web.1]: [Sat Oct  8 21:54:41 2016] [debug] GET "/"
2016-10-08T21:54:41.210001387Z app[web.1]: [Sat Oct  8 21:54:41 2016] [debug] Routing to a callback
2016-10-08T21:54:41.221802741Z app[web.1]: [Sat Oct  8 21:54:41 2016] [debug] 200 OK (0.013645s, 73.287/s)

It works! As you can see, there are two logs line from the minion.1 app component that say:

[...] [debug] Performing job "1" with task "poke_mojo" in process 143
[...] [debug] We have poked mojolicious.org for a visitor

which tell us that the minion received the task and executed it.

It’s instructive at this point to take a look at the directory that was created as /var/lib/dokku/data/storage/sample-mojo; we will need to impersonate root on the Dokku node this time:

$ ssh "root@$DOKKU_IP" ls -l /var/lib/dokku/data/storage
total 8
drwxr-xr-x 2 32767 32767 4096 Oct  8 21:53 sample-mojo

What’s this thing with user and group ids 32767? Simple: when Docker is instructed to run containers by Dokku, it is told to start them as these user and group id. They are different enough from what you have in the machine (the highest user id is dokku’s at 1000, the highest group id is still dokku’s at 1000 except for a few service group ids that are at 65534, so still very distant from 32767) so that you can be reasonably sure there will be no clash or overlapping by chance.

Backing Services

As we saw in a previous section, each service instance runs inside its own container and is quite isolated from the other ones, even though they actually run on the same node. That’s what makes Dokku powerful: it allows you to run multiple services, which might each have their own quirk about filesystem, configurations, running processes, library versions and so on, but still guarantee that they will play nicely in the same host.

One of the consequences is that you have to take explicit actions to have them share files/directories: in our case, we wanted the web and minion services to share a common directory where they would be able to operate on the same SQLite file. We were lucky that there is the storage management to help us with this.

What if we want to connect services differently? For example, we might need to use a different database technology, and choose PostgreSQL instead. Heroku and Dokku can get you covered for a lot of technologies, and lucky for us there’s the Postgres plugin that will help us right out of the box.

The initial Dokku tutorial help us here, so we will just take the relevant command without too many comments. First, we take care to see whether the plugin is really installed or not:

$ dokku help
Usage: dokku [--quiet|...

Primary help options, ...

Commands:

    apps             L...
    certs            M...
    ...

Community plugin commands:

    letsencrypt <app> ...
    ...
    postgres          ...
    redis             ...

Fine, we do have plugins for Let’s Encrypt, Redis and Postgres, listed in the Community plugin commands section.

These services provided out of the box are called backing services; there are quite a number of them (e.g. see the official ones) and they should get you covered in most situations.

Let’s define one backing service for our application then, by first creating it and then linking to our application:

$ dokku postgres:create sample-mojo-pg
       Waiting for container to be ready
       Creating container database
       Securing connection to database
=====> Postgres container created: sample-mojo-db
=====> Container Information
       Config dir:          /var/lib/dokku/services/postgres/sample-mojo-db/config
       Data dir:            /var/lib/dokku/services/postgres/sample-mojo-db/data
       Dsn:                 postgres://postgres:1b8c1fb63db2cbee3c407c8fd815152a@dokku-po...
       Exposed ports:       -                        
       Id:                  ecaf7be1f12cc146da63c2da1b4f9c737e26ccce6f3aef27d7af302feb1d674d
       Internal ip:         172.17.0.5               
       Links:               -                        
       Service root:        /var/lib/dokku/services/postgres/sample-mojo-db
       Status:              running
       Version:             postgres:9.5.4

$ dokku postgres:link sample-mojo-pg sample-mojo
-----> Setting config vars
       DATABASE_URL: postgres://postgres:1b8c1fb63db2cbee3c407c8fd815152a@dokku-postgres-...
-----> Restarting app sample-mojo
... other lines about application restart..

As we can see, the plugin took care to set the environment variable DATABASE_URL in our application to the right value for consumption by the application itself. We can now use it in our code then, so the app.pl file is modified as follows:

#!/usr/bin/env perl
use Mojolicious::Lite;

my $dsn = $ENV{DATABASE_URL} || 'sqlite:/app/shared/test.db';
my $type = ($dsn =~ m{^postgres:}mxs) ? 'Pg' : 'SQLite';
plugin Minion => {$type => $dsn};

# Slow task
app->minion->add_task(poke_mojo => sub {
 my $job = shift;
 $job->app->ua->get('mojolicious.org');
 $job->app->log->debug('We have poked mojolicious.org for a visitor');
});

# Perform job in a background worker process
get '/' => sub {
 my $c = shift;
 $c->minion->enqueue('poke_mojo');
 $c->render(text => 'We will poke mojolicious.org for you soon.');
};

app->start;

This will make sure that we can continue to use SQLite locally and PostgreSQL remotely (even though you should strive to have perfect alignment across all your deployment environments!).

We just have to make sure that our Perl application will be able to use PostgreSQL now, so the cpanfile becomes as follows:

requires 'Mojolicious', '7.08';
requires 'Minion', '6.0';
requires 'Minion::Backend::SQLite', '0.007';
requires 'Mojo::Pg', '2.30';

Commit, then push:

$ git commit -am 'Add support for PostgreSQL backing service'
[master 4ad17b9] Add support for PostgreSQL backing service
 2 files changed, 4 insertions(+), 1 deletion(-)

$ git push
Counting objects: 7, done.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (4/4), 578 bytes, done.
Total 4 (delta 2), reused 0 (delta 0)
-----> Cleaning up...
-----> Building sample-mojo from herokuish...
-----> Adding BUILD_ENV to build environment...
-----> Multipack app detected
=====> Downloading Buildpack: https://github.com/polettix/heroku-buildpack-perl-procfile.git
=====> Detected Framework: Perl/Procfile
-----> Installing dependencies
       Successfully installed DBD-Pg-3.5.3
       Successfully installed Mojo-Pg-2.30
       2 distributions installed
... all good up to now...
remote: App container failed to start!!
=====> sample-mojo web container output:
       Invalid PostgreSQL connection string "postgres://postgres:1b8c1fb63db2...
... last message repeated a few times...
 ! [remote rejected] master -> master (pre-receive hook declined) error:
 failed to push some refs to ...

Ouch! Well, I think Mark Jason Dominus got it right at this point:

You can't just make...

Simply put, the DATABASE_URL environment variable will need some adaptation before we can use it, right? This error at least told us one thing though: the application we are trying to deploy is actually attempting to use the PostgreSQL database backing service after all!

It turns out that we just have to force the URL scheme to postgresql (with the final ql part):

#!/usr/bin/env perl
use Mojolicious::Lite;

my $dsn = $ENV{DATABASE_URL} || 'sqlite:/app/shared/test.db';
$dsn =~ s{^postgres.*?:}{postgresql:}mxs;
my $type = ($dsn =~ m{^postgresql:}mxs) ? 'Pg' : 'SQLite';
plugin Minion => {$type => $dsn};

# Slow task
app->minion->add_task(poke_mojo => sub {
 my $job = shift;
 $job->app->ua->get('mojolicious.org');
 $job->app->log->debug('We have poked mojolicious.org for a visitor');
});

# Perform job in a background worker process
get '/' => sub {
 my $c = shift;
 $c->minion->enqueue('poke_mojo');
 $c->render(text => 'We will poke mojolicious.org for you soon.');
};

app->start;

As of release 2.31 of Mojo::Pg this should not be necessary any more, although it’s still not out as of 2016-10-16. See Updates for details.

Again, commit, push and check:

$ git commit -am 'Adapt DATABASE_URL to Mojo::Pg URL scheme'
[master c7f2d50] Adapt DATABASE_URL to Mojo::Pg URL scheme
 1 file changed, 2 insertions(+), 1 deletion(-)

poletti@Polebian2:sm (master)$ git push
...
=====> Application deployed:
       http://sample-mojo.example.com
...

# deployment was fine, let's send a GET
$ curl http://sample-mojo.example.com
We will poke mojolicious.org for you soon.

# take some time
$ sleep 5

# check logs
$ dokku logs sample-mojo
... app[minion.1]:... Worker 8 started
... app[minion.1]:... Checking worker registry and job queue
... app[minion.1]:... Performing job "1" with task "poke_mojo" in process 145
... app[minion.1]:... We have poked mojolicious.org for a visitor
...

The fact that it’s performing job 1 again is another hint that it is actually using PostgreSQL, because the jobs numbering was restarted. Anyway, we don’t need the persistent storage any more at this time, so we can double check by just removing it:

$ dokku storage:list sample-mojo
sample-mojo volume bind-mounts:
     /var/lib/dokku/data/storage/sample-mojo:/app/shared

$ dokku storage:unmount sample-mojo /var/lib/dokku/data/storage/sample-mojo:/app/shared

$ dokku storage:list sample-mojo
sample-mojo volume bind-mounts:

$ dokku ps:restart sample-mojo
... usual stuff here...

$ curl http://sample-mojo.example.com
We will poke mojolicious.org for you soon.

# take some time
$ sleep 5

# check logs
$ dokku logs sample-mojo
... app[minion.1]:... Worker 7 started
... app[minion.1]:... Checking worker registry and job queue
... app[minion.1]:... Performing job "2" with task "poke_mojo" in process 145
... app[minion.1]:... We have poked mojolicious.org for a visitor
...

YAY, it’s still working indeed!

Encryption Anyone?

Up to this point, all our traffic has been left into the wilderness of plain text. If you think that encrypting communications can be a good thing, though, you probably already started wondering how to add TLS to the lot.

Dokku provides you means for managing certificates via the certs group of commands:

$ dokku certs:help
Usage: dokku certs:COMMAND

Manage Dokku apps SSL (TLS) certs.

Additional commands:
    certs:add <app> CRT KEY             Add an ssl endpoint to an app. Can also import from a tarball on stdin
    certs:chain CRT [CRT ...]           [NOT IMPLEMENTED] Print the ordered and complete chain for the given certificate
    certs:generate <app> DOMAIN         Generate a key and certificate signing request (and self-signed certificate)
    certs:info <app>                    Show certificate information for an ssl endpoint
    certs:key <app> CRT KEY [KEY ...]   [NOT IMPLEMENTED] Print the correct key for the given certificate
    certs                               Manage Dokku apps SSL (TLS) certs
    certs:remove <app>                  Remove an SSL Endpoint from an app
    certs:rollback <app>                [NOT IMPLEMENTED] Rollback an SSL Endpoint for an app
    certs:update <app> CRT KEY          Update an SSL Endpoint on an app. Can also import from a tarball on stdin

This is definitely the way to go if you are using a domain where you can’t define a wildcard resolution or it’s not immediately below a public suffix, or more simply you want to use your homebrewn self-signed certificates (which is perfectly secure if you have full control on all clients, of course!).

If you instead:

  • have control over your domain DNS configurations
  • the domain is immediately below a public suffix

then you can enjoy the services of the Let’s Encrypt plugin. Let’s see how.

The only real setup you have to do is define an email address; this is used by Let’s Encrypt to notify you about certificates that are about to expire (this will be our safenet, because we can setup the plugin to renew the certificates automatically). Then you just have to activate the plugin.

If you’re lazy like me, you can set the email address up as a global variable, so that every project will use the same. You can still override this on a per-project basis, anyway.

$ dokku config:set --global DOKKU_LETSENCRYPT_EMAIL=you@example.com
-----> Setting config vars
       DOKKU_LETSENCRYPT_EMAIL: you@example.com

Then, you just have to activate the plugin for your app:

$ dokku letsencrypt sample-mojo
=====> Let's Encrypt sample-mojo
-----> Updating letsencrypt docker image...
latest: Pulling from dokkupaas/letsencrypt-simp_le
420890c9e918: Already exists
e4a2ae244258: Already exists
5c6ac6d1c950: Already exists
Digest: sha256:18a19b34beceba79dd5be458abe7e132fc7486da1da19cc4d0395ad4578031ef
Status: Image is up to date for dokkupaas/letsencrypt-simp_le:latest
       done updating
-----> Enabling ACME proxy for sample-mojo...
-----> Getting letsencrypt certificate for sample-mojo...
        - Domain 'sample-mojo.example.com'
darkhttpd/1.11, copyright (c) 2003-2015 Emil Mikulic.
listening on: http://0.0.0.0:80/
2016-10-09 07:05:42,620:INFO:__main__:1211: Generating new account key
2016-10-09 07:05:44,514:INFO:requests.packages.urllib3.connectionpool:758: Starting new HTTPS connection (1): acme-v01.api.letsencrypt.org
2016-10-09 07:05:44,762:INFO:requests.packages.urllib3.connectionpool:758: Starting new HTTPS connection (1): acme-v01.api.letsencrypt.org
2016-10-09 07:05:45,019:INFO:requests.packages.urllib3.connectionpool:758: Starting new HTTPS connection (1): acme-v01.api.letsencrypt.org
2016-10-09 07:05:46,032:INFO:requests.packages.urllib3.connectionpool:758: Starting new HTTPS connection (1): letsencrypt.org
2016-10-09 07:05:46,762:INFO:requests.packages.urllib3.connectionpool:758: Starting new HTTPS connection (1): acme-v01.api.letsencrypt.org
2016-10-09 07:05:47,015:INFO:requests.packages.urllib3.connectionpool:758: Starting new HTTPS connection (1): acme-v01.api.letsencrypt.org
2016-10-09 07:05:47,283:INFO:requests.packages.urllib3.connectionpool:207: Starting new HTTP connection (1): sample-mojo.example.com
2016-10-09 07:05:47,359:INFO:__main__:1305: sample-mojo.example.com was successfully self-verified
2016-10-09 07:05:47,381:INFO:requests.packages.urllib3.connectionpool:758: Starting new HTTPS connection (1): acme-v01.api.letsencrypt.org
2016-10-09 07:05:47,749:INFO:__main__:1313: Generating new certificate private key
2016-10-09 07:05:48,211:INFO:requests.packages.urllib3.connectionpool:758: Starting new HTTPS connection (1): acme-v01.api.letsencrypt.org
2016-10-09 07:05:52,438:INFO:requests.packages.urllib3.connectionpool:758: Starting new HTTPS connection (1): acme-v01.api.letsencrypt.org
2016-10-09 07:05:52,693:INFO:requests.packages.urllib3.connectionpool:758: Starting new HTTPS connection (1): acme-v01.api.letsencrypt.org
2016-10-09 07:05:53,007:INFO:requests.packages.urllib3.connectionpool:758: Starting new HTTPS connection (1): acme-v01.api.letsencrypt.org
2016-10-09 07:05:53,260:INFO:__main__:391: Saving account_key.json
2016-10-09 07:05:53,261:INFO:__main__:391: Saving fullchain.pem
2016-10-09 07:05:53,262:INFO:__main__:391: Saving chain.pem
2016-10-09 07:05:53,262:INFO:__main__:391: Saving cert.pem
2016-10-09 07:05:53,263:INFO:__main__:391: Saving key.pem
-----> Certificate retrieved successfully.
-----> Installing let's encrypt certificates
-----> Unsetting sample-mojo
-----> Unsetting DOKKU_NGINX_PORT
-----> Setting config vars
       DOKKU_PROXY_PORT_MAP: http:80:5000
-----> Setting config vars
       DOKKU_PROXY_PORT_MAP: http:80:5000 https:443:5000
-----> Setting config vars
       DOKKU_NGINX_PORT: 80
-----> Setting config vars
       DOKKU_NGINX_SSL_PORT: 443
-----> Configuring sample-mojo.example.com...(using built-in template)
-----> Creating https nginx.conf
-----> Running nginx-pre-reload
       Reloading nginx
-----> Configuring sample-mojo.example.com...(using built-in template)
-----> Creating https nginx.conf
-----> Running nginx-pre-reload
       Reloading nginx
-----> Disabling ACME proxy for sample-mojo...
       done

So, after a bunch of back and forth with Let’s Encrypt, the plugin took care to reconfigure the reverse proxy to:

  • redirect all traffic from port 80 to port 443
  • accept traffic on port 443

Let’s see the redirection part:

$ curl -v http://sample-mojo.example.com/
* About to connect() to sample-mojo.example.com port 80 (#0)
...
> GET / HTTP/1.1
> User-Agent: curl/7.26.0
> Host: sample-mojo.example.com
> Accept: */*
> 
...
< HTTP/1.1 301 Moved Permanently
< Server: nginx
< Date: Sun, 09 Oct 2016 07:09:55 GMT
< Content-Type: text/html
< Content-Length: 178
< Connection: keep-alive
< Location: https://sample-mojo.example.com:443/
...

Let’s follow the redirection now, note that curl does not complain at all about the certificate:

$ curl -L http://sample-mojo.example.com/
We will poke mojolicious.org for you soon.

Last thing we want to do is to set up automatic renewal of the certificates for all applications, because they expire every 90 days (this short time is to encourage automation):

$ dokku letsencrypt:cron-job --add
-----> Added cron job to dokku's crontab.
no crontab for dokku

Done! Don’t worry about the last line, it appears if our Dokku instance is brand new and no previous crontab setting was present for user dokku.

Summing Up

After this (admittedly) long journey, we got to this point:

  • we have a Dokku node where we can easily deploy our applications - the Perler, the better in my opinion, but you’re not necessarily limited to it;
  • we know how to share a directory across different components of an application
  • we know how to connect to backing services, like a PostgreSQL database for example
  • we secured our application web frontend communications via TLS

This is probably something that should get you more than started! For going beyond, you should definitely check the excellent documentation in Dokku’s website.

Have fun!

Updates

  • 2016-10-09 a change for supporting the postgres:// url scheme in addition to postgresql:// is on its way (see this commit). This makes using environment variable DATABASE_URL a breeze (see Backing Services) as it will be immediately consumable by Mojo::Pg and the associated Minion backend, YAY! (We will have to wait for the new release of Mojo::Pg though!).
  • 2016-10-16 most of the stuff in this article has been compressed in a cheatsheet available in this wiki. Nice thing about GitLab is that it’s possible to also whip up a few snippets.
  • 2016-10-16 added a Table of Contents for better navigation of the document.
  • 2016-12-23 added note on Debian 8 in Vultr.
  • 2017-09-16 fixed a few missing links and typos.