In this article we’ll look at how to deploy a Jekyll blog in a Docker container.

Why Jekyll?

In a previous article we looked at how to deploy a ghost blog in docker but that approach has some disadvantages:

  • Written posts are not very portable, they are stored internally in ghost
  • We need to regularly backup the ghost database to avoid losing posts

Jekyll is a blog alternative that generates static HTML from templates that you create. Because the output is HTML we can plug these into a Docker container that has a webserver and “hey presto!” we have a blog!

The biggest advantage of using Jekyll is that we can put our blog posts in a GIT repo. This means that all changes to the posts will be recorded and we don’t have to worry about backups when the site is live, since we can regenerate the entire site at any moment from the source code in the GIT repo. Also, it gives us complete portability, it will be easy to move the posts to another blogging platform if needed since we have all of the posts in markdown in the GIT repo.

Note: Jekyll is the engine behind Github Pages. This means that you can easily host the blog project in Github and Github will generate and host the resulting site. For simple sites this might be just what you need, in that case you should stop reading and check out this link. There is one caveat with doing this however: Github won’t allow you to run custom plugins for security reasons, so if you think you will use custom plugins you’ll have to host the blog yourself. However as we’re going to see in this blog post, it’s not hard to do.

Enough of the “why”, let’s jump into the “how”!

Create The Directory Structure

The final structure will look like this, for now feel free to recreate the directories that you see below, we’ll add the files and explain each one later:

├── app
│   ├── _drafts
│   ├── _includes
│   ├── _layouts
│   │   ├── default.html
│   │   └── post.html
│   ├── _posts
│   ├── css
│   ├── images
│   ├── index.html
│   ├── js
│   └── robots.txt
├── _config.yml
├── Dockerfile
├── Gemfile
└── web

A note about the structure, normally Jekyll suggest that you keep everything in the root (without the app/ directory) but when you start to complicate things a little by compiling less to css or other operations using gulp or grunt, then all of these files will get bundled into the final static generated website. Instead of adding a long list of files to Jekyll’s exclude list we’ll keep the Jekyll source together in the app/ directory which is cleaner.

Anything that you put into the app/ directory will get copied to the generated site too, so put your css, images, js files here.

Let’s have a look at the Jekyll config file _config.yml:

source: app
destination: web

permalink: pretty

encoding: utf-8

Fill in your domain in the “url” parameter.

Next up, have a look at the views, first up is app/_layouts/default.hml

<!DOCTYPE html>
<html lang="en" prefix="og:">
    <meta charset="utf-8">
    <link rel="stylesheet" type="text/css" href="/css/style.css">

    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>{% if page.title %}{{ page.title }} &mdash; {% endif %}Your Blog</title>

    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <meta property="og:url" content="{{ site.url }}{{ page.url | remove_first:'index.html' }}">
    <meta property="og:site_name" content="">

    {% if page.title %}
    <meta property="og:title" content="{{ page.title }}">
    {% endif %}

    {% if page.description %}
    <meta name="description" content="{{ page.description }}">
    <meta name="og:description" content="{{ page.description }}">
    {% else if page.excerpt %}
    <meta name="description" content="{{ page.excerpt | strip_html | truncatewords: 25 }}">
    <meta name="og:description" content="{{ page.excerpt | strip_html | truncatewords: 25 }}">
    {% endif %}

    {% if page.og_image_url %}
    <meta property="og:image" content="{{ page.og_image_url }}">
    {% else if page.photo_url %}
    <meta property="og:image" content="{{ page.photo_url }}">
    {% endif %}

    {% if page.keywords %}
    <meta name="keywords" content="{{ page.keywords | join: ', ' }}" />
    {% endif %}

    {% if %}
    <meta property="og:type" content="article">
    <meta property="article:published_time" content="{{ | date: "%Y-%m-%d" }}">
    {% endif %}

    <script src="//"></script>


<nav class="navbar navbar-default navbar-static-top">
    <div class="container">
        <!-- Brand and toggle get grouped for better mobile display -->
        <div class="navbar-header">
            <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            <a class="navbar-brand" href="/">Your Blog</a>
    </div><!-- /.container -->

<div class="container">

    {{ content }}



Most other views will extend from this one. Not complicated, you can of course change this to suit you.

Next up is app/_layouts/post.html:

layout: default

        <h1>{{ page.title }}</h1>
        <div class="post-info">
            <div class="author-published">
                by {{ }}
            <div class="date-published">
                <time datetime="{{ }}">{{ | date: '%B %d, %Y' }}</time>

    <div>{{ content }}</div>


Simple stuff. Notice that we are using Jekyll front matter to add some variables that apply only to this template, in this case we specify that this template uses the layout of the default template that we saw earlier.

Finally, the last template in our simple blog will be the homepage. It’s in a different location because it’s the homepage, it should be at app/index.html:

layout: default
description: Your Beautiful Blog

<h1>Recent Posts</h1>
{% for post in site.posts limit:50 %}
<h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
<div class="preview">
    {% if post.description %}
    <span class="body">{{ post.description | strip_html | truncatewords: 300 }}</span>
    {% else %}
    <span class="body">{{ post.excerpt | strip_html }}</span>
    {% endif %}
{% endfor %}

Again, simple stuff. On the homepage we’ll just list the most recent 50 articles with an excerpt of each one. Notice that we are again extending from the default layout.

We’re nearly ready to generate the site, we just need one more thing:


Remember we mentioned plugins? We’ll configure them now. Create a Gemfile and put this in it:

source ''

group :jekyll_plugins do
    gem 'jekyll', '~>3.0'
    gem 'kramdown'
    gem 'rdiscount'
    gem 'jekyll-sitemap'
    gem 'jekyll-redirect-from'

We’ll install these Gems later when we generate the site.

At this point you might want to write a few words of your first blog post and put it into app/_posts/, see this link for more info on how to write posts. Otherwise, let’s generate our site!

Generating the Site

Now comes the fun part, we’re going to run Jekyll so that the posts and templates that we’ve created will be converted into the HTML files of a full website!

Since we’re very into Docker around here, to help us we’re going to use a docker container with Jekyll installed on it. You can of course install Jekyll locally but using a Docker container makes it more portable, for example you might decide to build your blog continuously later on, by containing your tools inside Docker containers you don’t have to install them on every server you want to build the blog on.

Run this to install the Gems and build your shiny new blog:

$ docker run --rm \
        -v "$(pwd):/src" \
        -w /src \
        ruby:2.3 \
        sh -c 'bundle install \
            --path vendor/bundle \
            && exec jekyll build --watch'

We’ve added the “–watch” flag at the end, that’s handy when we’re making changes and we want to see them reflected immediately on the blog.

Voila! Have a look in the web/ folder, you should see lots of HTML files, those are the ones that Jekyll generated. If you were to FTP that entire web/ folder to a server you would have a working blog, but we’re going to put it into a Docker container, so…..

The Dockerfile

Our container will be super simple, we just need to serve the web/ folder that we just generated. Here’s our Dockerfile:

FROM nginx


COPY web/ /usr/share/nginx/html

That’s it! Now let’s build and run the image:

$ docker build -t my-shiny-blog .
$ docker run -d \
        -p 80:80 \
        -v "$(pwd)/web:/usr/share/nginx/html" \

Now hit in your browser to see your blog in action! Notice that we’ve mounted the source into the container as a volume, that will allow us to see updates in real time when Jekyll regenerates the site (since Jekyll is still running with “–watch”) without having to rebuild the image again. Before pushing the image to your registry just remember to rebuild ;)

That’s it! We can of course get fancy and minify Javascript or compile less to css using gulp but we’ll leave that as an exercise for the reader. The trick is to put the less and source code outside of the app/ directory and have gulp place the final versions in the app/ directory. That way Jekyll will copy them over when the site is generated.

Hope you enjoyed the article, feel free to comment below.