Michael Blum

Developer from Chicago

Building a Blog Part 1 - Docker + Jekyll


Welcome to the inaugural post of mblum.me. I figured a good start to this blog would be a series of posts on how this blog can to be: from development to production.

Why use Docker?

A valid question. Looking at the Jekyll Quick Start it should be as simple as:

gem install jekyll && jekyll serve

What I found to be difficult as when more gems came into play or my version of Ruby on my host machine was wrong (my Linux workstation has an old Ruby install).. not so simple anymore.

This site is based off of an awesome Jekyll theme: Jekyll Now and while the Github README lays out in great detail how to get running on Github Pages.. there’s is a dearth of documentation on how to get this theme running locally.

I fork the repository and naively run gem install jekyll && jekyll serve and I get this:

  Dependency Error: Yikes! It looks like you don't have jekyll-sitemap or one of its dependencies installed. In order to use Jekyll as currently configured, you'll need to install this gem. The full error message from Ruby is: 'cannot load such file -- jekyll-sitemap' If you run into trouble, you can find helpful resources at http://jekyllrb.com/help/!
jekyll 3.1.0 | Error:  jekyll-sitemap

We find in the _config.yml these two gems:

# Use the following plug-ins
gems:
  - jekyll-sitemap # Create a sitemap using the official Jekyll sitemap gem
  - jekyll-feed # Create an Atom feed using the official Jekyll feed gem

so just running jekyll serve throws errors complaining about gems that aren’t installed. In the moment this can be solved by simply running gem install {{ ruby_gem_name }} and calling it a day. But this seems unusual for a Ruby application.

Generally speaking in the Ruby world we’d use a Gemfile to declare gems used by our project. But since Jekyll relies on a _config.xml, we need to reach for another tool for defining our Jekyll development environment.

To keep track of both the installed Gems and the ones used by our Jekyll site we want to put a Gemfile into our theme and then update the _config.yml accordingly:

_config.yml

# Use the following plug-ins
gems:
  - jekyll-sitemap # Create a sitemap using the official Jekyll sitemap gem
  - jekyll-feed # Create an Atom feed using the official Jekyll feed gem
  - jekyll-bootstrap-sass

Gemfile

source 'https://rubygems.org'
gem 'jekyll'
gem 'jekyll-sitemap'
gem 'jekyll-feed'

Containerization to the rescue

In containerizing our build environment we want to maintain the workflow of a normal Jekyll build experience. This includes:

  1. Watching of files for changes
  2. Being able to edit with any editor (vim, emacs, sublime, atom)
  3. Maintaining a project in version control
  4. viewing the site locally on port 4000

To accomplish this in a Docker container we want to observe a few Docker-isms:

  1. Volumes - we can load data from a host box into a container and changes on the host are reflected in the container without a restart. The difference between a Docker ADD and a Docker VOLUME being akin to cp vs rsync in Linux.

    The use of a volume also allows the developer to use any tools found on their host environment - no need to install those tools in the Docker image itself.

  2. Port Forwarding - one common gotcha with Docker containers is that by default, all ports exposed in a Dockerfile are partitioned from the host system. This means if you open port 4000 on a Docker container, you’d also have to know the IP address of that container (which is dynamic and liable to change as the container is stopped and started).

  3. User Privileges - by default, everything in a Docker container executes as root. This is not the best of situations and its a good practice to treat the user executing your application inside a container as you would a normal user on a bare/non-containerized environment.

To solve this in a portable way we’ll have our Dockerfile:

Dockerfile

FROM ruby:2.3.0
RUN addgroup jekyll &&  \
    adduser --ingroup jekyll --disabled-password --gecos '' jekyll && \
    chown jekyll:jekyll /home/jekyll
USER jekyll
RUN mkdir -p /home/jekyll/site \
    && chown -R jekyll:jekyll /home/jekyll/site
VOLUME /home/jekyll/site
WORKDIR /home/jekyll/site
EXPOSE 4000
CMD bundle install \
    && jekyll serve --host=0.0.0.0 --force_polling --watch

Lets break this down. We take a three-step approach to our Dockerfile.

  1. We stand up the default ruby environment (2.3.0 in this case as Jekyll 3 requires > Ruby 2.x)

  2. create our jekyll user and take ownership of the volume our Jekyll site is going to live in.

  3. finally start up the Jekyll watch server

I bring to your attention this line in particular:

CMD bundle install \
&& jekyll serve --host=0.0.0.0 --force_polling --watch

In a Docker container, we need to allow the outisde world to connect to the Jekyll server. If we use the default (127.0.0.1), we’d have to ssh tunnel into the Docker container to see our blog.. not ideal. To get around this, using 0.0.0.0 will expose the port to all incoming connections. Lastly we need to force Jekyll to watch for changes from the VOLUME directory that is daisy-chained from the host machine to the Docker container at /home/jekyll/site.

To build and start our Dockerized Jekyll we run the following commands:

Docker not installed? Installing Docker

docker build -t mblum/jekyll .

and then we start the container:

#!/bin/bash
docker run \
--name jekyll \
-d \
-v ${PWD}:/home/jekyll/site \
-p 4000:4000 \
-t mblum/jekyll

Note: ${PWD} is a useful command to get the current working directory as an ENV variable for passing into Bash commands - makes this script nice and portable.

-d: run in the background

-p 4000:4000: forward our exposed port to the host system. This isn’t quite true on a Mac as Docker Machine assigns an IP address that looks like this:

Docker is up and running!
To see how to connect your Docker Client to the Docker Engine running on this virtual machine, run: /usr/local/bin/docker-machine env default


                        ##         .
                  ## ## ##        ==
               ## ## ## ## ##    ===
           /"""""""""""""""""\___/ ===
      ~~~ {~~ ~~~~ ~~~ ~~~~ ~~~ ~ /  ===- ~~~
           \______ o           __/
             \    \         __/
              \____\_______/


docker is configured to use the default machine with IP 192.168.99.100
For help getting started, check out the docs at https://docs.docker.com

So on my Mac, my blog is at 192.168.99.100:4000

And We’re live!

Browsing to our host and port we see the site up and running:

blog up and running

If we open our editor of choice and make a change:

<nav>
	<a href="{{ site.baseurl }}/">Blog</a>
	<a href="{{ site.baseurl }}/about">About</a>
	<a href="{{ site.baseurl }}/change">Change!!</a>
</nav>

reload the page and we’re in business:

blog updated

Links: