JF's Dev Blog

Django, Vue, and other things, too

Build and Deploy a Static Site with Dokku

Once upon a time, publishing a website meant uploading HTML files to a public_html folder using an FTP client. I cringe to think of it now, but I can remember editing HTML files directly on a production server, with no version control to track changes. I'm glad those live-coding-over-FTP days are behind us.

These days, git is ubiqutious, and one-click deployments are commonplace. So why not use them?

In this post I'll detail how I use Dokku to deploy a static site—say, like this one. While I use Pelican, this deployment strategy would work for deploying a website built with any static site generator, like Hugo or VuePress.

Unlike a typical static site deployment, where the site is built on a development machine and then uploaded to AWS, GitHub pages, or somewhere else, the deployment strategy here involves building a production version of your static site on your production server, like Netlify does.

Why bother?

Greater control over your content

Hosting yor own site gives you control over how your content is used and whether it's monetized. Signal v Noise, a popular tech blog, recently left Medium to regain control of their content after Medium started to morph into a subscription-based service.

By using Dokku to publish your static site on your own VM or server, your content will be less dependent on third parties, whose business model could change—or shut down altogether.

Clean separation of draft and production content

By default, both Pelican and Hugo use the same directory for both development and production builds of a static site. As the Hugo docs make clear, it can be easy to "cross the streams" and end up with overlapping development and production content:

Running hugo does not remove generated files before building. This means that you should delete your public/ directory […] before running the hugo command. If you do not remove these files, you run the risk of the wrong files (e.g., drafts or future posts) being left in the generated site.

When Dokku builds the production version of your static site, it creates a new Docker container each time. The only content in your production site is content you've committed.

Info

This deployment strategy is also Continuous Delivery-friendly, because the production server builds the static site. You could edit articles in Markdown on GitHub or GitLab, and trigger a deployment when article changes are merged into master.

Why Dokku?

Dokku is my go-to deployment solution. It's a bring-your-own-server, command-line-only alternative to Heroku, capable of deploying applications that use Heroku's buildpacks or Dockerfiles. I've been using Dokku for at least three years, both for small personal projects—like this blog—and for heavier business deployments with multiple containers.

Dokku is easy to use, actively developed, and has a number of useful plugins, like dokku-letsencrypt, dokku-postgres, and dokku-redis. Best of all, with just a little setup, performing a Dokku deployment is as easy as git push dokku master.

Let's see how.

Dokku setup

(If you've used Dokku before, skip ahead to the next section.)

First, you'll need a server with Dokku. I use a Digital Ocean droplet running Ubuntu (they also have a one-click app). I've used AWS, too, so use whatever you're comfortable with. Follow the Dokku installation instructions.

Next, you'll need to configure a dokku git remote in your local project repository:

git remote add dokku dokku@yourserver.biz:your-app-name

Finally, create your static site so you have something to deploy.

Deploying HTML files with Dokku

Deploying a static site with Dokku is even easier than dusting off an FTP client.

First, add an empty file called .static to the root folder of a static site. This tells Dokku that your project is a static site, and Dokku will know to use its Nginx buildpack to build and serve your project.

If your project has an index.html in the root directory, that .static file is all the configuration necessary. Otherwise, the Nginx buildpack needs to know where to find your project's main index.html file.

Specify the Nginx root

By default, the Nginx buildpack will treat the repository root as the Nginx root, and it will copy your project into a /app/www/ directory inside a Docker container. As a result, the /app/www/index.html file is the homepage.

Static site generators, however, typically output production files into a subdirectory. Pelican places production builds in an output/ directory, for instance, and Hugo uses public/.

If you ask the Nginx buildpack nicely, you designate a different directory the web root:

dokku config:set blog NGINX_ROOT=/app/www/output

Now we're ready to deploy.

Deploy a static site

Now that Dokku knows your project is a static site, and the Nginx buildpack knows where to find your site's files, it's time to deploy your site to your Dokku server:

git push dokku master

Dokku will use the Nginx buildpack to build a Docker image and configure Nginx on the host server to make the site accessible to the Internet at large. It's a bit like magic.

Dokku can do even more magic, too. Instead of just deploying the generated HTML, it could invoke your static site generator and build that HTML, too.

Building a static site during deployment

Declaring multiple buildpacks

Dokku supports using multiple buildpacks to build and serve your application. One common usecase for this is to use Node.js and Python buildpacks for a Django project that uses npm to manage its frontend dependencies.

Telling Dokku that a repository uses multiple buildpacks is as easy as adding a .buildpacks file containing the necessary buildpacks. Because I use Pelican as my static site generator, I use the Heroku Python Buildpack alongside the Nginx buildpack:

# .buildpacks
https://github.com/heroku/heroku-buildpack-python.git#v146
https://github.com/dokku/buildpack-nginx.git#v10

For the list of buildpacks Dokku uses, see Herokuish buildpacks. Most buildpacks have a buildpack-url that points to a Heroku buildpack, with the notable exception of the Nginx buildpack mentioned in this article.

Specifying the build command

Dokku has pre-deploy and post-deploy deployment tasks to perform different operations during a deployment:

scripts.dokku.predeploy: This is run after an app's docker image is built, but before any containers are scheduled. Changes made to your image are committed at this phase.

scripts.dokku.postdeploy: This is run after an app's containers are scheduled. Changes made to your image are not committed at this phase.

These commands live in an app.json file. Here's the one I use to build this blog with Pelican:

{
  "scripts": {
    "dokku": {
      "predeploy": "cd /app/www && mkdir output && pelican ./content -o ./output -s ./publishconf.py"
    }
  }
}

Because files changed in a predeploy task are committed to the Docker image, HTML files generated by running a static site build command in this task will be available on the production site.

Info

As a safeguard against deploying a broken website, Dokku will cancel a deployment if a predeploy or postdeploy task fails.

Wrap-up

A multi-buildpack Dokku deployment makes publishing a static site a breeze. With a small amount of configuration, you get:

  • ownership of your content and how it's used
  • a clean separation of draft and production content
  • a git-based, CD-friendly deployment workflow

With a deployment this nifty, your static site's build will be ahead of the pack.