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.
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.
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 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:
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:
The container will execute
bundle exec jekyll serve -H 0.0.0.0 --future.
-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.
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
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:
After it’s complete, we can start up the containers.
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.
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.
To restart both containers:
Jekyll Compose gives you a nice set of scripts to use when creating and editing content for your blog.
Shell access to container
You can access the running container by:
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.
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.