In this post, I’ll containerize a small Jekyll application, that will compile Markdown pages into a a site. Jekyll will also render the site to the amazement (or not) of my fellow family.

This would be a good opportunity to discuss how to containerize different applications. Along the way, we’ll have to resolve both OS and package level dependencies for the application to work.

In our case, the application is our small site (this one is built with Jekyll too).

One of the reasons we’d like to containerize an application is gaining full controll of its dependencies (and as always, there are plenty). This will allow us to run the application independent of the tools installed on the host machine.

Prerequisites

  • Be familiar with Linux (comfortable with managing and installing different packages)
  • Being comfortable with the Linux comand line
  • Basic knowledge of Docker
  • No need to know about Jekyll and Ruby

What We’ll Do Here?

We’ll simulate a small part of the CI/CD process by using Docker. Here are our steps:

  1. Choose the base image for containerizing the application
  2. Install OS dependencies
  3. Install the needed packages
  4. Pass the source files to the Jekyll application
  5. Run the Jekyll application and make it compile the Markdown into a site that will be served locally

Withour further ado, let’s delve into the subject ahead of us.

The Cookbook

Preparing the Project

You can clone or download this repository. Cloning the repo will create a mysitedemo folder. You’re free to tinker with its contents.

Choose the Base Image for the Application

If we closely look at the “prerequisites” section at the Jekyll’s official documentation, we can easily see that Jekyll is written in Ruby. So, first thing we’d need a Ruby-based image in Docker. Here’s the list of official images available from the Ruby team.

We’ll choose an Alpine-based distribution, because Alpine is one of the most lightweight images in the Docker ecosystem.


While reading the quickstart in Jekyll’s documentation, we can see that if we run Ruby version 3.0.0 or higher, we’d need to install another package for Jekyll to render the site. This is a breaking change, which results in our inability to serve our site due to the changes introduced in the underlying packages/OS tools.


ruby:2.5.0-alpine3.7 would be the perfect fit for the task.

On the one hand, it’s the minimum Ruby version for running Jekyll.

On the other hand, breaking changes still weren’t introduced. It’s a good practice to choose as absolute versions as possible. For example, instead of version 2.5,I chose Ruby version 2.5.0. Instead of Alpine 3 I opted in for version 3.7.

So, let’s put it in our Dockerfile: FROM ruby:2.5.0-alpine3.7

Install Packages at the OS level

Jekyll depends on many packages that need other tools at the OS level. Let’s add the following instructions to the Dockerfile:

RUN apk update
RUN apk add g++ gcc make musl-dev

It’s a good practice to update the OS package repository by running the apk update command or something alike (yum update/apt update) We’d like to avoid upgrading the existing packages, since breaking changes can be introduced into our image.

Install Jekyll and Some Additional Packages

Now, after we sorted out the OS-level dependencies, it’s time to install all the packages that Jekyll needs in order to run. Let’s add the following statements:

RUN gem install jekyll bundler
WORKDIR /
RUN jekyll new myblog --blank
WORKDIR /myblog
COPY Gemfile ./
RUN bundle install

gem is the package manager for Ruby. It installs all the required gems for running a Jekyll-based site. Afterwards, we ask Jekyll to create an empty site with some basic folder structure in place. We’ll populate this empty scaffolding with the actual files in a later step.

The Gemfile contains the list of further packages that our site depends on. They will be installed with the bundle tool.

After the all the required gems were installed, we can move on to removing redundant files.

Removing the Redundancies

Let’s add the following lines to the Dockerfile in order to remove the redunancies:

RUN rm -f index.md
RUN rm -rf assets _data _layouts _includes _ssas

This will remove some files that might affect our site.

Copying the Source Files

In the following lines, we’re copying some of the source files we need for the site:

COPY _config.yml 404.html about.markdown index.markdown ./
COPY _posts/ ./_posts/

As the project grows, the number of the directories we need to copy will increase too.

The Entrypoint

In order to be able to run the site once the container finishes initialization, we’ll add an entrypoint and some complementary options via the CMD statement:

ENTRYPOINT ["bundle"]
CMD ["exec", "jekyll", "serve", "--host=0.0.0.0"]

In the case above, CMD provides additional options for the main command. The lines above make Jekyll compile the Markdown files into HTML files, build their dependencies (stylesheets, JS files, images), run the server, which eventually renders the files.

Running the Container

Note that the image should be built and run in the same directory as our site.

After we finished writing the image, it’s time to build it with the following command:

docker build -t mysite .

It might take some amount of time to build all the dependencies for the image, so be patient.

Once the build is completed, we can run the container:

docker run --name site -v $PWD:/myblog -p 4000:4000 -d mysite

Now you can go to http://localhost:4000/

Want to continue building the site?

No problem. Just execute the following commands:

  1. To connect to the container: docker exec -it mysite sh
  2. Once in the container, to rebuild the site: jekyll build

To wrap it up, when you’d like to containerize an application, you should:

  1. Read its documentation closely.
  2. Decide about the base Docker image. Always search for official images in the Dockerhub registry.
  3. Revisit the documentation and see what OS packages need to be installed.
  4. Check which packages need to be installed at the language-specific level.
  5. Additionally copy the package list (such as requirements.txt in Python).
  6. Install the missing packages from there with the language-specific package manager on top of the image.
  7. Pass the source code by using the COPY statements.
  8. Build the application in the image.
  9. At the ENTRYPOINT: something that will make the compiled and built source code run.

End result: the image will include all the compiled files that would be activated upon container initialization. Steps 4 & 5 might look the same, but in some cases, the package installed is a command-line tool, such as Jekyll. In our particular case, we first needed to install Jekyll and Bundle command-line tools with the Ruby package manager. Later, we passed an application-specific list of dependencies (i.e. the Gemfile) and installed them.

In some cases, step 4 can be omitted as the dependencies list may well include the needed dependencies.

Hope this post was helpful for all those who wish or need to containerize applications.

Happy Jekylling and Dockering!