Making websites the automatic (but hard) way

Generating my Jekyll website with Jenkins and Docker

Screenshot of Jenkins build of this website

Posted: August 2, 2020

In my previous blog post (from six months ago – so how you think I’m doing at this blogging thing?) I discussed why I used the Jekyll static site generator for this site. I also mentioned wanting a CI/CD pipeline to automate the publication of this site. That way I could author the post locally, push it to my git repo, and through the magic of webhooks it would automatically appear posted on the site some minutes later.

I’m back to report (six months later … have I mentioned it’s been six months since I’ve last blogged …) I have finally done that. It was not easy, and I thought I’d share how I did it.

The tools I used were Jenkins and Docker. This post probably won’t mean much unless you have some familiarity with at least the purpose of these tools.

Allow me to share up front an admission that forms the basis of this post: I made two decisions that made the task really hard.

My first bad decision

I planned to run Jenkins in a Docker container, and then run Jekyll in a Docker container launched from within the Jenkins container.

The first decision that made life difficult is that rather than install Docker into the Jenkins container, I would pass the host Docker service into the Jenkins container and just use that. So the Jekyll image would be launched from the Jenkins container, but it actually would be running on the original host.

And at this point, things get very Inception-like.

Here is my docker-compose file that does that Docker-inside-Docker thing:

jenkins:
    image: chezrufus/jenkins
    .
    .
    .
    volumes:
        - /usr/bin/docker:/usr/bin/docker
        - /var/run/docker.sock:/var/run/docker.sock
        (more volumes not shown ...)
    .
    .
    .

Note that I’m mapping both the Docker command and server socket from the host into the Jenkins container. That will allow the docker command to run inside the Jenkins container using the Docker server on the host.

But I’ll need to customize the jenkins image for this to work. Put a pin in that for the moment …

My second bad decision

The second decision that made things difficult was to use bind mounts, rather than data volumes.

Briefly, a bind mount maps a directory on the host server into the container. In this case, I want to map /data/chezrufus/jenkins/home into the container, so that it appears to Jenkins as /var/jenkins_home – the place where all the Jenkins data is stored. This way I’ll be able to easily access all the Jenkins data – and back it up – from the Docker host.

So, we’ll need another volume directive in the docker_compose:

jenkins:
    image: chezrufus/jenkins
    .
    .
    .
    volumes:
        - /usr/bin/docker:/usr/bin/docker
        - /var/run/docker.sock:/var/run/docker.sock
        # --- Added line below ---
        - /data/chezrufus/jenkins/home:/var/jenkins_home
        (one more volume not shown ...)
    .
    .
    .

Unfortunately, the directory from a bind mount cannot be passed into a nested container. So the the Jekyll container will be unable to access the workspace directory in the Jenkins container.

This would not be a problem if I had selected a data volume instead of a bind mount – data volumes are designed to allow sharing between containers – but I already explained my reasons for preferring bind mounts over data volumes.

So here’s my solution: use a data volume as a temporary space that can be shared between Jenkins and Jekyll. I’ll create a jenkins_tmp data volume and mount that in the Jenkins volume. When it comes time to build the website, I’ll have the Jenkins pipeline copy the files into the temporary workspace, call Jekyll to build the website in that space (which is can now access because it’s in a data volume), and then copy out the results when done.

That finally completes the volume mappings for my Jenkins container:

jenkins:
    image: chezrufus/jenkins
    .
    .
    .
    volumes:
        - /usr/bin/docker:/usr/bin/docker
        - /var/run/docker.sock:/var/run/docker.sock
        - /data/chezrufus/jenkins/home:/var/jenkins_home
        # --- Added line below ---
        - jenkins_tmp:/var/jenkins_tmp
    .
    .
    .

The plan comes together

Now, I’m finally ready to write the Jenkins pipeline code to do the Jekyll build:

environment {
    JEKYLL_VERSION = "4.1.0"
    
    // $JEKYLL_HOME is the directory where the "jenkins_tmp" storage volume was mounted (as defined in docker-compose.yml)
    JEKYLL_HOME = "/var/jenkins_tmp"
    
    // $JEKYLL_JOB_ID is a brief unique job id, taken from the base name of the workspace dir
    JEKYLL_JOB_ID = WORKSPACE.replaceFirst(~'^.*/', '')
    
    // $JEKYLL_JOB_DIR is the directory where job source will be staged
    JEKYLL_JOB_DIR = "${env.JEKYLL_HOME}/${env.JEKYLL_JOB_ID}"
    
    // $JEKYLL_CONTAINER_HOME is where $JEKYLL_JOB_DIR will be mounted inside the Jekyll container
    JEKYLL_CONTAINER_HOME = "/srv/jekyll/${JEKYLL_JOB_ID}"
}

.
.
.
    
stages {    
    .
    .
    .
    
    stage('jekyll build') {        
        steps {
            // copy the website into the jenkins_tmp Docker volume that Jekyll can access
            sh 'rm -rf ${JEKYLL_JOB_DIR} ; mkdir -p ${JEKYLL_JOB_DIR} ; rsync -a . ${JEKYLL_JOB_DIR}'
            
            // build the web site
            def cmd = [
                'docker run',
                '--rm',
                '--env JEKYLL_ENV=production',
                '--volume "jenkins_tmp:/srv/jekyll"',
                '--workdir ${JEKYLL_CONTAINER_HOME}',
                'jekyll/jekyll:${JEKYLL_VERSION}',
                '/bin/sh -c "jekyll build --strict_front_matter"',
              ].join(' ')
            sh cmd
            
            // copy the results out of the jenkins_tmp Docker volume
            sh 'rsync --archive --delete ${JEKYLL_JOB_DIR}/. .'
        }
    }
    
    .
    .
    .
}

.
.
.

post {
    always {
        sh "rm -rf ${JEKYLL_JOB_DIR}"
    }
}

Easy as pie, right?

Oh, wait, there’s more. I need to use a custom-built Jenkins image to support this. I’ll leave that as a topic for my next blog post. And I’ll try to take fewer than six months to write that

(Credit: I found this blog post helpful for figuring this out.)

It's Just This Little Chromium Switch Here

Latest blog posts

Making websites the automatic (but hard) way

Aug 2, 2020

In my previous blog post (from six months ago – so how you think I’m doing at this blogging...

The hot new blogging thing we did 20 years ago

Feb 25, 2020

Anybody who’s been doing IT long enough knows that old ideas – both good and bad – often get dressed...

Deprecate Facebook

Feb 23, 2020

I started blogging in 2002. I stopped blogging in 2018.