Replacing Heroku with Dokku (for Pet Projects)

August 25, 2022 was a sad day in cloud computing, when Heroku announced that they are discontinuing all free plans. Reason being too much time spent on fraud, which I guess translates to folks abusing Heroku for crypto mining.

Anyway, as of writing this, there is already no more free Heroku. Now the question is, should I pay Heroku or someone else from now on, to host my pet projects and experiments? There is definitely more competition in the non-free cloud offerings than in the free ones.

I could use AWS but that’s the opposite of fun, and pet projects should still be fun. I like DigitalOcean since they are better on terms of developer friendliness, but then that’s still not as simple as git push heroku main.

So I decided to have my own “Heroku”, in the form of a self-hosted Dokku server. It’s a tiny PAAS that you can host on a Linux machine and provides a Heroku-like command line interface to interact with.

But you need a server.

My first idea was to install it the NAS that is running 24/7 in my closet but it’s neither powerful enough nor do I want experimental stuff running as root right next to my precious private data.

I decided to use a dirt cheap (4.5 EUR / month) cloud server at Hetzner, not only because it’s cheap but also because pet projects are for trying out new things.

Below is a step-by-step guide to setting up Dokku in a (mostly) automated way.

Setting up the server

I’m a Believer of Configuration as Code, therefore after signing up to Hetzner and obtaining an API token, I did the rest of the setup using Ansible. The instructions below assume that you have a basic understanding how Ansible works.

I’ve put everything in an Ansible playbook named dokku.yml.

- name: Set up a server on Hetzner
  hosts: 127.0.0.1
  connection: local
  tags: server
  module_defaults:
    hcloud_server:
      api_token: "{{ hetzner_api_token }}"
    hcloud_ssh_key:
      api_token: "{{ hetzner_api_token }}"
  tasks:
    - name: Create an ssh_key
      hcloud_ssh_key:
        name: my-laptop
        public_key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
        state: present
    - name: Create a server
      hcloud_server:
        name: dokku
        server_type: cx11
        image: ubuntu-18.04
        location: fsn1
        ssh_keys:
          - my-laptop
        state: present
      register: hcloud_server_result
    - name: Print server values
      debug:
        msg: "{{ hcloud_server_result }}"
    - name: Add server to inventory
      ansible.builtin.add_host:
        groups: dokku_hetzner
        name: '{{ hcloud_server_result.hcloud_server.ipv4_address }}'
        ansible_user: root

Assuming you’ve set up an Ansible variable named hetzner_api_token holding your API token (preferably using a vault, running the following commands should get a server up and running:

# Install Ansible and dependencies
pip install ansible
pip install hcloud
ansible-galaxy install dokku_bot.ansible_dokku
ansible-galaxy collection install hetzner.hcloud

# Set up the server
ansible-playbook dokku.yml

It is assumed that you want to use the publish ssh key at ~/.ssh/id_rsa.pub for connecting to the server. If that’s not the case, tweak the playbook to your liking.

At this point ssh root@the-address-printed-under-server-values should get you a shell on the server.

Set up a domain

It is recommended (but not mandatory) that you set up a wildcard domain for the Dokku server. I’m using Cloudflare DNS, therefore this section depends on your provider. You can also decide to set up the domain manually.

Add the following to the playbook:


- name: Configure DNS entries for Dokku
  hosts: 127.0.0.1
  connection: local
  tags: dns
  tasks:
    - name: Create an A record for mydomain.com
      cloudflare_dns:
        zone: mydomain.com
        type: A
        record: 'cloud'
        value: '{{ hcloud_server_result.hcloud_server.ipv4_address }}'
        account_email: "{{ cloudflare_account_email }}"
        account_api_token: "{{ cloudflare_api_token }}"
        state: present
    - name: Create an A record for *.mydomain.com
      cloudflare_dns:
        zone: mydomain.com
        type: A
        record: '*.cloud'
        value: '{{ hcloud_server_result.hcloud_server.ipv4_address }}'
        account_email: "{{ cloudflare_account_email }}"
        account_api_token: "{{ cloudflare_api_token }}"
        state: present

You will have to set up Ansible variables for the DNS provider credentials.

Running the playbook again should set up the DNS entries:

ansible-playbook dokku.yml

Verify by running dig mydomain.com or ssh root@mydomain.com. It might take a while until the DNS changes are propagated.

Install Dokku

Because the Dokku folks are amazing, they created an Ansible role for setting it up. Add the following to dokku.yml:

- name: Set up Dokku on the server
  hosts: dokku_hetzner
  tags: dokku
  roles:
    - dokku_bot.ansible_dokku
  vars:
    dokku_hostname: mydomain.com
    dokku_key_file: /root/.ssh/authorized_keys
    dokku_plugins:
      - name: postgres
        url: https://github.com/dokku/dokku-postgres.git
      - name: letsencrypt
        url: https://github.com/dokku/dokku-letsencrypt.git

You can tweak dokku_plugins to your liking. Plugins can also be added later, keep letsencrypt if you want to use free SSL certificates.

Installing Dokku with this command will take a while (~10 minutes):

ansible-playbook dokku.yml

At this point the dokku command should be working on the server:

root@dokku:~# dokku domains:report --global
=====> Global domains information
       Domains global enabled:        true                     
       Domains global vhosts:         mydomain.com

Post-install

A few steps are necessary to enable using free Let’s Encrypt TLS certificates.

dokku config:set --global DOKKU_LETSENCRYPT_EMAIL=me@mydomain.com
dokku letsencrypt:cron-job --add

Use a valid email address :)

This should ideally be done with Ansible as well.

It is not mandatory, but the rest assumes you have the official Dokku client installed on your development machine (wherever you deploy your apps from).

Deploy an app

Now you are ready to deploy your Heroku-compatible apps to your own server using a few simple commands.

If you don’t have any ready, you can play with Heroku’s own example app, like this one:

git clone https://github.com/heroku/python-getting-started.git

Then in the app’s root folder (assuming a Django app using PostgreSQL):

dokku apps:create my-app
dokku postgres:create my-app-database
dokku postgres:link my-app-database my-app
dokku letsencrypt:enable
git push dokku master

Deploy a new version after making a git commit using:

git push dokku master

The app will be available at my-app.mydomain.com.

Note that if you are using the Django example app above, a few more tweaks are needed:

dokku config:set ALLOWED_HOSTS=my-app.mydomain.com SECRET_KEY=something-random
dokku run ./manage.py createsuperuser

Happy pushing, feedback welcome! 👇