Dockerize Your Octopress 3.0 Site With Heroku

Why don’t I just blog on Medium with all the cool kids? I have several responses. First, I’m a control freak and am only moderately repentant about it. I want to make my own website, not borrow space on somebody else’s. Second, I like writing code, and the Octopress/Jekyll platform makes me feel like a proper coder even when I’m just blogging. Those two items make the extra work worth it to me. And yet there is a third motivator that is more to the point of this post: I get an opportunity to play with Docker, a technology I have grown to love, and to boot, I get to continue my love affair with Heroku, my favored PaaS. It just so happens that a Dockerized website is not only fun, but also useful. Hence, I’m motivated to write this post detailing both the why and the how, especially since I had to fumble around a bit to get this to work right and thought others attempting this feat might benefit from my experience.

Why Heroku?

I may be a control freak who wants to make his own website - albeit with the help of a well-done publicly available Jekyll theme to compensate for my Drudge Report design skills. But configuring my own web server box and keeping it up to date? Ain’t nobody got time for that - not for a personal website with no specific readership ambitions anyways. Heroku allows me to provide some fairly trivial configuration, code my code, git push, and be done with it.

Since this is a static site with no backend components, there is a case to be made that Heroku is a wee bit of overkill. Using an AWS S3 bucket could end up being similarly convenient, cheaper, and might even result in a faster website. That may well be something I look into in the near future. But for now, Heroku is what I know and can work with without a learning curve, so Heroku wins the day. I wouldn’t necessarily say Heroku is superior to any other PaaS provider - it’s just the one I’ve used in a couple professional settings and have hence grown accustomed to. I tend to go with familiarity, all else being equal.

Why Docker?

Containerization via Docker yields all kinds of benefits for deploying and running software components at scale, many of which don’t apply here and could be better articulated by people who know better. My work here keys on one particular benefit: a predictable and repeatable execution environment. By encapsulating my website into a runnable, distributable unit that runs on my local machine just as well as on Heroku, I can be confident that the site I’m producing locally is precisely what will run in production.

Granted, for something like an Octopress site there isn’t a whole lot of complexity to contend with, and I would not expect to see production-only bugs in this context. If nothing else, consider this a low-risk, low-impact way to give the Docker toolset a spin along with a prominent PaaS provider. Use a free dyno if you don’t care about 24/7 uptime.

With all that said, let’s get down to brass tacks.

Adapt Your Octopress 3.0 Site to Run on Heroku

Before we check out the Docker toolset, there are a couple of tweaks that are helpful, and in one case outright required, in order for your Octopress site to run as a Heroku Ruby application.

First, the Heroku Ruby buildpack expects to run rake assets:precompile as part of the build process. If this were a Rails 3+ application, you’d have it out of the box. Here in non-Rails land, we have two options. We could simply run jekyll build and check in the generated site output (aka the _site folder), and then provide a no-op assets:precompile in a Rakefile. Personally, I dislike checking in generated code that I didn’t directly write if I can help it. The diffs become less readable due to being bloated with code that isn’t really meant to be evaluated by human reviewers.

Besides, we have a second option: we can put _site in .gitignore, roll our own assets:precompile rake task, and have the buildpack generate our site output for us.

Given the second option, here’s what your .gitignore could look like:

.sass-cache
.bundle
.idea
.DS_Store
_site

Now for a Rakefile that performs a jekyll build as a stand-in for assets:precompile:

namespace :assets do
  desc 'make Heroku build the jekyll site for us'
  task :precompile do
    `jekyll build`
  end
end

Now onto the optional, but recommended, task of eschewing the Ruby default WEBrick web server in favor of the Heroku-recommended Puma.

First, let’s start with a Gemfile that looks like this:

source 'https://rubygems.org'
ruby '2.2.3'
 
gem 'foreman'
gem 'jekyll', '~> 2.5.3'
gem 'puma'
gem 'rack-jekyll'
gem 'rake'
gem 'therubyracer'
 
group :jekyll_plugins do
    gem 'octopress', '~> 3.0.11'
    gem 'octopress-image-tag'
    gem 'octopress-video-tag'
    gem 'octopress-codeblock'
    gem 'octopress-quote-tag'
    gem 'octopress-codefence', '~> 1.6.1'
    gem 'octopress-gist'
    gem 'octopress-popular-posts'
    gem 'octopress-solarized'
end

We’re using rack-jekyll to facilitate running our Jekyll site as a Rack application. This requires a pretty simple configuration file:

require 'bundler/setup'
Bundler.require(:default)

run Rack::Jekyll.new(destination: '_site')

We’ll want a Procfile that declares the command necessary to kick off our webserver:

web: bundle exec puma config.ru -t $PUMA_MIN_THREADS:$PUMA_MAX_THREADS -w $PUMA_WORKERS -p $PORT -e $RACK_ENV

Just where are we initializing all those environment variables? Two places, depending on context. For local development, provide a .env file:

RACK_ENV=production
PORT=4000
PUMA_MIN_THREADS=2
PUMA_MAX_THREADS=5
PUMA_WORKERS=2

For production, you’ll want to set these variables in the “Config Variables” section of your Heroku application settings.

At this point, you’ll be able to plain-vanilla deploy to Heroku with git. The bundle will install, your site will build, and you’ll have it live within a minute or so.

Dockerize It

Now let’s take a look at how we can use Docker to obtain a consistent execution environment between local and production.

First, a suggestion. If you’re working from OSX or Windows, you’ll notice Docker provides you with docker-machine to enable you to run Docker processes without having to explicitly access a Linux environment. Feel free to use it if you feel up to it. In my personal experience, in addition to what has been relayed to me by colleagues, docker-machine is not particularly reliable. In Docker’s defense, they’re working on it. Until then, if you have to involve VirtualBox anyways, then you might as well just work with Docker in an explicit Linux environment. Use Vagrant to accomplish this. If Ubuntu suits you, feel free to use my Vagrant setup.

This post would get awfully long were I to go into the fundamentals of constructing a Docker image. Thankfully, Heroku does us another solid by providing a plugin and other tooling that will give us pretty much everything we’ll need.

Given we have the toolbelt and the Docker plugin installed:

wget -O- https://toolbelt.heroku.com/install-ubuntu.sh | sh
heroku plugins:install heroku-docker

We can bootstrap ourselves for Docker execution like so:

heroku docker:init

That adds a Dockerfile and a docker-compose.yml. The Dockerfile need not change at all - Heroku’s base ruby image will serve us perfectly well and encapsulates the same Cedar stack setup that runs in production. Note that Ruby 2.2.3 is required, hence the Gemfile directive.

I do propose you slightly alter your docker-compose.yml though:

web:
  build: .
  command: 'foreman start'
  working_dir: /app/user
  ports:
    - '4000:4000'
shell:
  build: .
  command: bash
  working_dir: /app/user
  ports:
    - '4000:4000'
  volumes:
    - '.:/app/user'

Because we have a handy-dandy .env file for local environment variables, there’s no need to declare environment variables in our docker-compose.yml. For .env to help us here, we’ll need to use foreman start as our web command.

Also, note one of the many benefits of docker-compose: defining multiple processes for a single container, in this case a bash shell into our container. So far this has been a YAGNI for me, but your mileage may vary.

One more piece we’ll need is an app.json file to provide some necessary information for the heroku-docker plugin:

{
  "name": "name-of-your-heroku-app",
  "description": "Describe your app here",
  "image": "heroku/ruby",
  "addons": ["some heroku addon", "another heroku addon"]
}

And boom, Dockerized.

Run It Locally, Docker Style

Given the above, we can build our site inside of the heroku/ruby container and then kick off a web process to serve up our site locally.

docker-compose build && docker-compose up web

Hit http://localhost:4000 in a browser and behold your website in a production-like local context.

Deploy It, Docker Style

The heroku-docker plugin also allows us to deploy and execute our site in production, using the very same Docker setup we just used locally.

heroku docker:release

Boom, deployed to production. And what’s more, you can be sure that you’re deploying exactly what you just ran locally.

Conclusion

Just like that, we have an Octopress/Jekyll site Dockerized and running on Heroku. Overkill for a fairly simple personal website? Perhaps. But as aforementioned, an inconsequential personal website is a great opportunity to get your feet wet with these tools, precisely because you might be the only one who cares if it goes poorly. Fortunately for me, it went swimmingly, and now all three of you readers of this blog are experiencing the awesomeness of a Dockerized static site. Can you feel it? Does the fresh Docker aura not just emanate from the screen?

Well, it was fun for me anyways.

Jason Hagglund

Jason Hagglund
Husband of Tami. Father of Roger, Juliet, and Tatiana. Autism parent. Presbyterian. Writer/tester of code. Eater of bacon.

Mobile Dev+Test 2016 Recap

[Mobile Dev+Test 2016](https://mobiledevtest.techwell.com/) was a great experience all around. I learned a ton and even had my mind blown...… Continue reading

Alive and Well

Published on March 23, 2016

Welcome

Published on April 26, 2014