• Home
  • Blog

Deploying to Dokku with Docker Images from GitHub Actions

2024-11-15T11:16:00-0800
  • #web
  • #github
  • #dokku
  • #docker

This website is deployed using Dokku.[1]

While it's possible to deploy to a Dokku host by pushing code directly from Git, the server running Dokku in my case doesn't have enough capacity to handle application builds. I wanted to avoid building the application directly on the Dokku host.

To solve this, I decided to use GitHub Actions to build Docker images and deploy those images to Dokku. After some trial and error, I’ve documented the process here for reference.

Below is the workflow file for the GitHub Action I currently use:

deploy.yml

The process flow is as follows:

  1. Check for code changes
    • If the app code is updated:
      • -> Set steps.changes.outputs.code == true
    • If entry files are updated:
      • -> Set steps.changes.outputs.entries == true
  2. If entry files are updated:
    • Sync entry files using rsync
  3. If the app needs an update:
    • Build a Docker image
    • Upload the built image to the GitHub Container Registry
    • Deploy the image to Dokku
  4. If only the entry files were updated:
    • Restart the service in Dokku to reload the entries

Filtering types of repository changes

The trickiest part of creating this workflow was detecting the type of code changes. This was necessary because not all commits require redeploying the application.

In my case, both application code and blog entry files are stored in the same repository. I wanted to support scenarios where only entries are updated without redeploying the application.

Initially, I attempted to handle this manually, but it almost drove me crazy. Thankfully, the dorny/paths-filter action saved the day. This is truly a fantastic action.

Handling this manually would have required detecting changes across various scenarios, such as single commits (I allow direct commits to the main branch for entry updates) and pull request merges and etc. I deeply appreciate the effort of those who made this action available.

Executing Dokku commands from GitHub Actions

Most Dokku commands can be executed over SSH, so enabling SSH access to the Dokku host from GitHub Actions is sufficient.

First, generate an SSH key pair:

$ ssh-keygen -t ed25519 -f dokku

This generates a private key (dokku) and a public key (dokku.pub).

Register the public key on the Dokku host:

$ echo "your pubkey here" | dokku ssh-keys:add github
$ echo "your pubkey here" >> ~/.ssh/authorized_keys

The first line is required for using Dokku commands, while the second is for rsync operations. If you only need to run Dokku commands, the first line alone is sufficient.

Next, register the private key on GitHub:

screenshot_20241115-120846

Setting -> Secrets and Variables -> Actions -> Repository Secrets

I registered it as DOKKU_SSH_KEY.

Finally, add a step to place the key in the appropriate location so subsequent steps can access the Dokku host via SSH:

- name: Set up SSH key
  run: |
    mkdir -p ~/.ssh
    echo "${{ secrets.DOKKU_SSH_KEY }}" > ~/.ssh/id_ed25519
    chmod 600 ~/.ssh/id_ed25519
    ssh-keyscan -H typester.dev >> ~/.ssh/known_hosts

Building and uploading Docker images to GitHub Container Registry

This step was straightforward and mostly followed official documentation.

Deploying Docker images to Dokku

The deployment step looks like this:

- name: Deploy image to production
  if: steps.changes.outputs.code == 'true'
  run: |
    ssh dokku@typester.dev git:from-image typester.dev ghcr.io/typester/typester.dev@${{ steps.push.outputs.digest }}

Initially, I set the image target as something like typester.dev:main, but if the image existed locally, it wouldn't pull the updated one. To fix this, I started specifying the digest value from the previous build step.

Restarting the service

When only entries are updated, restarting the service reloads the entries. The following step handles this:

- name: Restart production server to reflect entries update
  if: steps.changes.outputs.code != 'true' && steps.changes.outputs.entries == 'true'
  run: |
    ssh dokku@typester.dev ps:restart typester.dev

Both the image deployment and service restart steps ensure zero downtime by switching traffic to the new service only after it is up and running by Dokku. This is excellent!

Conclusion

Dokku is amazing.

[1] The source code for the entire site is available here: https://github.com/typester/typester.dev

Copyright © 2024 by Daisuke Murase.

Powered by org-mode.