Setting up Jekyll, Part 1

7 minute read

You will find plenty of heavy blogging and content management systems available, including WordPress, Drupal, and Joomla just to name a few. These systems were developed as successors to the early days of the web where websites were all static content, with little to no JavaScript. This progressed to more dynamic, flashy content where those who lack computer skills could create and maintain a website. However, this requires additional software installed on a server or requires you to pay someone to provide these services. As with computer security, the more software installed the more attack vectors exist.

This is where Jekyll comes in as a static content generator, a system that allows you to generate web content on one machine to deploy remotely as a website. All of the configuration and fancy content creation is exported to HTML/CSS rather than being generated dynamically on a webserver. This reduces our attack vector and lessens the work required to maintain a simple blog.

After having been a developer for over a decade living in a pre-container world and transitioning to Docker, I’ve come to the current conclusion that dealing with containers for development is the way to go. Including tooling and specialized command-line tools as well.

For setting up Jekyll on our system, we’re not going to install any Ruby software on our local machine. Instead, we’re going to leverage Docker for running containers on our system that will do all the heavy lifting with the required software applications and libraries installed. This article is going to assume you already have Docker up and running on your favorite desktop Linux distribution. Though this guide should work on any other system with maybe a few modifications.

Every Docker project starts with a Dockerfile.

Dockerfile

FROM ruby:2.7.2

ENV APP_USER myuser
ENV APP_HOME /home/${APP_USER}/app
ENV PORT 4000

RUN useradd --create-home ${APP_USER}
USER ${APP_USER}
RUN mkdir ${APP_HOME}

WORKDIR $APP_HOME

COPY ./theproject/Gemfile $APP_HOME/

ENV BUNDLE_GEMFILE=$APP_HOME/Gemfile \
  BUNDLE_JOBS=2 \
  BUNDLE_PATH=/home/${APP_USER}/bundle

RUN bundle install

EXPOSE $PORT

ENTRYPOINT [ "bundle", "exec" ]
CMD [ "jekyll", "serve", "-H", "0.0.0.0", "--future" ]

FROM ruby:2.7.2 Right off the bat we see the usefulness of Docker in that we don’t have to specify an operating system for our container for which to install our development environment. Since Jekyll is a Ruby application, we use a Ruby docker container which happens to all include Rubygems. The bulk of our requirements are met within this one line of code, where we call out a version number to pin a specific version. When maintaining docker containers, it’s useful to specify versions in order to maintain compatibility.

The next lines set environmental variables that we will use for this build. Setting the container’s home directory so our process as a base directory.

Best practices for Docker configuration is to run your container as a non-privileged user. So while still root, we create a user and their home directory which will be our Jekyll application directory.

USER allow us to run the remaining code as the assigned user, in this case an unprivileged user. Then, create the application root directory.

Set the WORKDIR which is our base directory. We obviously didn’t need to utilize a variable, but for sake of portability and extension it’s a good habit to have.

You’ll need a Rubygems Gemfile that will pull from a repository the software libraries we’re looking to bundle to run Jekyll; there’s an example down below. We set more environmental variables which are important because we will be using a separate container exclusively for all of the modules that Rubygems/Bundler installs. This way it persists and we do not need to rebuild the entire list of gems when we make certain changes.

Per the Docker reference, the EXPOSE instruction is just a note to the person looking at the Dockerfile, it’s not until you explicitly publish the ports. You can use the -p flag on docker run to publish and map one or more ports, or the -P flag to publish all exposed ports and map them to high-order ports. Or in our case, we’ll be using the ports configuration in the Docker-compose file.

Entrypoint & CMD

When these two Docker commands are set in this order, the CMD configuration that you set gets appended to the ENTRYPOINT command.

ENTRYPOINT allows you to configure a container that will run similar to an executable. Since our bundle executable and all its packages are in a non-default location we have to use bundle exec for all our Ruby commands. The cool part is that the CMD command only runs if we don’t specify any commands.

So if we run something like:

~$ docker run --rm jekyll_dev jekyll compose "My Post" --date 2020-05-20

The docker container will execute bundle exec jekyll compose "My Post" --date 2020-05-20.

To serve our Jekyll blog when we boot the container, we issue the command to serve via WEBrick. Again, the CMD command gets appended to our Entrypoint command.

So if we run:

~$ docker-compose up

The container will execute bundle exec jekyll serve -H 0.0.0.0 --future.

The -H flag specifies the host url to run the server. Since we’re running WEBrick inside of a container, we need to have access to it from the outside since that’s technically where our host computer resides, and where the web browser we’ll use to access the container’s webserver. We set the server to bind to 0.0.0.0, which is a non-routable meta-address that tells WEBrick to listen on all interfaces in the running environment. Basically, it makes the webserver accessible from outside the container’s localhost, which means private IP addresses on your local machine.

Our Gemfile

source "https://rubygems.org"

gem "jekyll", "~> 4.2.0"
# This is the theme for my Jekyll site. You may change this to anything you like.
gem "minimal-mistakes-jekyll", "~> 4.22.0"
gem 'jekyll-archives', "~> 2.2.0"

group :jekyll_plugins do
  gem "jekyll-feed", "~> 0.12"
end

This is the bare minimum to get started, including the theme I personally use. Again, we’re pinning specific versions, so be sure to check the latest stable versions because it may be newer than they are at the time of this writing. I won’t go over this much more, check the Bundler Gemfile Docs.

Best practices note we should pin versions so that we can regenerate a consistent docker build and images. Use Ruby’s pessimistic operator to set the range for the version we’ll all on bundle install.

Docker Compose makes managing Docker containers easier, so we’ll have to define our YAML docker-compose file.

Docker Compose file

version: "3.8"
services:
  jekyll_dev:
    build: .
    container_name: jekyll_container
    ports:
      - "4000:4000"
    volumes:
      - ./theproject:/home/myuser/app
    # This tells the web container to mount the `bundle` images'
    # /bundle volume to the `jekyll_dev` containers ~/bundle path.
    volumes_from:
      - bundle
# Replace "PROJECT" with the name of the image created by "docker-compose build".
# Sometimes it's just the name of the directory that the docker-compose file is in
  bundle:
    # 'image' will vary depending on your docker-compose
    # project name. You may need to run `docker-compose build`
    # before this works.
    image: PROJECT_jekyll_dev:latest
    volumes:
      - /bundle

We have two services: jekyll_dev and bundle. The first creates a container named jekyll_container, exposes ports on your local system to map against the container port. It maps our local folder ./theproject to the container’s folder /home/myuser (change to whatever USER you specified in the Dockerfile).

volumes_from allows us to mount a directory from our other service (that’s in a separate container). The remainder of the file revolves around our bundle service. We see that the /bundle directory from the bundle container is mounted to the ~/bundle directory in the actually Jekyll container.

Building our Containers

Now we can build our docker containers. From the directory that stores our docker files run from the terminal:

~$ docker-compose build

After it’s complete, we can start up the containers.

~$ docker-compose up

We should see both containers starting, one that holds the rubygems, and other container that holds our Jekyll program.

If we go to http://localhost:4000 we should see our page load. Hopefully this gets you going on using Jekyll for your website or blog.

In the next segment, we’re going to work on setting up Amazon S3 and Cloudfront to host our static Jekyll site.

Considerations

For editing posts, pages, and some layout changes, the WEBrick server that Jekyll launches will reload your changes when it detects you’ve made an edit.

However, if you make changes to the _config.yml file, the site will usually not have the changes take effect until you restart the server.

Restart containters

To restart both containers:

~$ docker-compose restart

Jekyll Compose

Jekyll Compose gives you a nice set of scripts to use when creating and editing content for your blog.

Such as:

~$ bundle exec jekyll post "My New Post" --date 2021-03-24

Checking Logs

~$ docker logs jekyll_container --tail 50 -f

Shell access to container

You can access the running container by:

~$ docker exec -it jekyll_container bash

On our host machine, we can execute the above to get a bash shell on the running Jekyll container.

-i is an interactive option that allows us to maintain the shell connection to the container. The -t creates a pseudo-TTY (terminal).

You can use this to run commands on our container.

Gemfile.lock

I would much prefer to include the Gemfile.lock. There’s discussion on why there isn’t a simple solution, and what option you can take. I may revisit this in order to implement a similar workaround.