I recently needed to write a blog post. Being a developer, this naturally amounted to spendinig half a day tinkering with the blog infrastructure. In lieu of the post I never wrote, here is a little overview of the blog infrastructure, which is a Jekyll instance, published to Github pages, running on Github Codespaces.

As a quick background, I migrated the blog from WordPress a few years ago as I wanted a simpler solution with pre-built pages. No more dealing with MySQL, malware, or comment spam. Github Pages has built-in Jekyll support, so it’s pretty easy to just compose posts in markdown and have it automatically compile them. It’s run well like this for a few years. Indeed, you don’t really need any development environment at all - you could just create files in Github’s web interface - or on your own PC connected to the Github repo - and Github will automatically publish them. This is all orchestrated using the Pages tab in the repo’s settings - there’s no need for custom workers.

So why have a dev environment for your Jekyll blog? Mainly so you can preview changes as you write blog posts or update the structure? I’m using a few Jekyll plugins and want to tweak them at times. Moreover, I want to preview blog posts before publication.

As I’m mainly using a Chromebook for dev nowadays, I wanted this environment in Codespaces. Furthermore, I wanted to automate the Codespace setup, so it can always be re-created (Github will delete it automatically by default after thirty days).

Prerequisite: Jekyll blog repo, preferably using relative paths

In this post, I’ll assume you already know Jekyll and have a Jekyll repo established. The only thing I’ll mention about this is I recommend your templates use relative paths, i.e. don’t prepend baseuri unless you absolutely have to (which might be necessary). This is because Github Pages is still using an older version of Jekyll (v3.9) which insists on prepending the URI in development even if you configure against it. Thus port-forwarding fails, which is necessary to follow links on browser when serving via Codespaces.

Codespace setup

To create a Codespace, you go into the Jekyll repo in your browser and hit “+” to launcht the new Codespace instance. (You can also do this stuff on command line with the gh cli tool if you wish.)

Now this will make a new Codespace using the default image. You would then need to manually install Ruby, Jekyll, bundle it, etc. So it’s not automated. We want to automate that.

Automating the Codespace setup

There are two key files to be aware of:

  • devcontainer.json
  • Dockerfile

Put these both in a top-level folder called .devcontainer in the Jekyll repo. Codespaces will look in this magic location for instructions on how to spin up the Codespace instance, whenever you click on it from the repo. Specifically it will look at devcontainer.json, which in turn should point to the [Dockerfile]

devcontainer

Here is how devcontainer.json looks:

    {
    "name": "Jekyll Blog Development",
    "build": {
        "dockerfile": "Dockerfile"
    },
    "customizations": {
        "vscode": {
        "settings": {
            "terminal.integrated.shell.linux": "/bin/bash"
        }
        },
        "extensions": [
        "streetsidesoftware.code-spell-checker"
        ]
    },
    "postCreateCommand": "sh /root/post-boot.sh",
    "forwardPorts": [4000]
    }

As mentioned, it points to the Dockerfile, which the Codespace will dutifully build for you. It also has some settings for the editing environment. It runs a script called post-boot.sh after the machine has spun up (I’ll get to that in a minute) and it passes browser requests through to port 4000, the default Jekyll environment.

Dockerfile

Here is how the Dockerfile looks:

    # Extend from the base image
    FROM bretfisher/jekyll-serve:stable-20231215-2119a31

    # Set bash as the default shell
    SHELL ["/bin/bash", "-c"]

    RUN echo "Building from SoftwareAs Dockerfile"

    # Install Vim and Tig
    RUN apt-get update && apt-get install -y vim tig

    RUN echo '[[ -f ~/.bash_profile ]] && source ~/.bash_profile' > /root/.bashrc

    RUN git clone https://github.com/mahemoff/dotfiles.git ~/dotfiles && \
        bash ~/dotfiles/make.sh

    COPY post-boot.sh /root/post-boot.sh

The first line says it derives from another Docker config by bretfisher, which is setup for Jekyll development (mainly ensures it has an appropriate Ruby version). In fact, I could have just used that one and not have a Dockerfile at all, but I wanted to do some customisation. The reset of the Dockerfile sets up my environment how I like it, with vim and tig apps, and my custom dockerfiles. It also copies that post-boot script to /root folder (the Jekyll repo itself appears elsewhere, in /workspaces/softwareas).

Post-Boot Script

The post-boot script runs bundle itself, ensuring Jekyll is ready to go. You could also do this in the Dockerfile or devcontainer.json, but it’s convenient to have a separate script. It makes it easier to experiment with it on the command-line.

    # Runs after container has been spun up
    # Adapted from https://github.com/devcontainers/images/blob/main/src/jekyll/.devcontainer/post-create.sh

    cd /workspaces/softwareas

    # Install the version of Bundler.
    if [ -f Gemfile.lock ] && grep "BUNDLED WITH" Gemfile.lock > /dev/null; then
        cat Gemfile.lock | tail -n 2 | grep -C2 "BUNDLED WITH" | tail -n 1 | xargs gem install bundler -v
    fi

    # If there's a Gemfile, then run `bundle install`
    # It's assumed that the Gemfile will install Jekyll too
    if [ -f Gemfile ]; then
        bundle install
    fi

Now you can go to the Codespace’s terminal tab, even if it’s new, and simply run bundle exec jekyll serve --incremental. The “ports” tab will include a link you can hit in your browser, which will point your browser to the Codespace and preview the blog for you.

Debugging and Troubleshooting

When changing the devcontainer and Dockerfile, and want to test it, you can open the command palette (ctrl-shift-p or cmd-shift-p) and filter for “rebuild”. When something goes wrong, the command palette can also be filtered for “creation log”, which is usually helpful in seeing what part of the devcontainer or Dockerfile failed. If you want to keep pon creating new Codespaces from the repo page, I made a script to quickly blast all but the most recent one.

And that’s it. Having previewed this post in the Codespace, now it’s time for me to push this to the repo so Github will automatically publish it.

Appendix: Fancy a prebuild?

The above setup is based on a Dockerfile, which means the server will have to be built whenever a new codespace is created. That’s totally fine for my purposes, since it’s not an elaborate container, it’s only me using it, and I rarely need to create a new codespace anyway. In other circumstances, it might be worth taking the time to prebuild the image, host it somewhere like Dockerhub, and then automate that process to allow for future changes. test