Dockerizing my Jekyll Blog
Table of Contents
Over the last several months, I have been using Docker more and more at work.
I have been wanting to apply it to something personal, and dockerizing my blog just made sense. There were a number of weaknesses in the way my current blog works I wanted to address.
As I worked through it, I ran into enough "gotchas" that I thought a more thorough writeup was worth doing. 1 Hopefully I can help someone else who is new-ish to docker (or, more honestly, hopefully I stop making some of these mistakes myself).
1 Background
tl;dr basically I have two services that need to run, jekyll
and an org-mode
compiler service.
Over time, I have grown less-and-less satisfied with Jekyll (and Markdown, especially).
I really wanted author posts in org-mode
. Of course, there are several solutions that exist for this,
but the only one with the featues I wanted is the official implementation. So, I wrote a little org-mode
compilation service.
A while ago I wrote about the initial implementation. That ended up being unacceptably slow, so I hacked together a little TCP service
that vastly improved performance. 2
2 End Goal
I wanted to have two separate containers, one running Jekyll, and the other running the Emacs service. Additionally, I want the potential to add more services in the future.
The theoretical of attaining this is short to describe:
- Find or build an image for each services' container.
- Experiment to find the correct container configuration for the services to run together.
- Modify the application code in each service to facilitate the dockerization, if necessary.
- Combine these configurations into a
docker-compose
file. - Test, iterate, and use.
As I alluded to earlier, this is harder than it sounds.
3 Lessons
3.1 When building docker images, look for existing Dockerfile
examples
Sometimes, there might be a Docker image that does exactly what you need it to, and you won't have to build a custom image. For me, this has rarely been the case.
In the case of my Jekyll image, I couldn't find an existing image that did what I wanted. The mistake I made at this point was to try to figure it out without looking for any resources. I quickly ran into issues.
In fact, I have consistently found it surprisingly hard to dockerize software. I always run into some unexpected environment issues – probably largely because of "minimal" base images3, but there may be other issues in play.
Surprisingly (to me, at least), I found an emacs image that was perfect for my use. I used it as a base images to build the org compilation service.
3.2 Inter-application communication is trickier
When something runs on my machine, my default "go-to" solution is to use localhost
. This has tripped me up a few times, since services in separate containers
are not on the same localhost
. Components will often need to be reconfigured (for example,
a service may need to listen
on 0.0.0.0
instead of localhost
, and
a client may need to connect
to a the containers name, instead of localhost
).
This is good to keep in mind, because when you run into it, if this is the problem you will have a much easier time than going through all the steps to debug your network connectivity.
3.3 Use an entrypoint script
When building docker images, it is MUCH easier to have
an entrypoint script
than to attempt a complicated invocation within the Dockerfile
's CMD
or ENTRYPOINT
settings.
I have made this mistake several times. The syntax that CMD
and ENTRYPOINT
accept is weird.
It can be really non-obvious when these are not correct. Just use a run (or entrypoint) script to run your
container's application code.
3.4 Docker tools are weird
Docker is an awesome tool, but I find many of its design decisions surprisingly limiting. I don't know how else to say it besides that.
Why can't I start an existing, stopped container with a different command than what it was created with? Why is it so hard to encode compilcated
scenarios in docker-compose.yml
files?
In the end, I ended up with two separate docker-compose.yml
, and the only differences between each of them is that one uses a different
jekyll
command from the other. I also have three separate shell scripts in the bin
directory which provide a nicer interface for:
- Serving the site with Jekyll in drafts mode.
- Serving the production version of the site.
- Building and deploying the site.
Could I possibly have simplified this into fewer pieces? I am sure I will in the future. But at the moment I've hit enough problems figuring out how to do that that I just want to get some work done.
4 Conclusion
I don't want to come across as overly negative about Docker. It is an incredible tool that has rightly received a lot of hype. Even though parts of it are awkward to work with, containerized applications are just so nice that I believe the pain is well worth it.
Footnotes:
I keep telling myself that I need to write these things as I work on and learn things. And, I keep not doing it. Here I am trying to break that pattern.
At one point I wanted to write about that experience, which was actually kinda interesting (This was the first "raw tcp" service I had ever written that was useful (as opposed to being a learning experience). There are a few interesting pieces:
The Jekyll plugin written in Ruby which starts and stops the org-mode
compilation service, and
the org-mode service that receives the raw org-mode
text over a socket and responds with the compiled html version.
Part of that support was code for
automatically starting
the required emacs process, so the service would be ready when Jekyll needed it.
This was tricky though and had bugs. As I learned Docker, it quickly became clear that it would work better as a separate service
specified in a docker-compose
file.
with a small base image, you end up needing to specify everything for your app. In my experience, an app that runs easily on regular ubuntu 16.04 does not mean it works on the ubuntu:16.04 image just as easily, for example.
Eventually I looked at the repo I had evaluated ealier (https://github.com/envygeeks/jekyll-docker), which I was able to use as an example. What I ended up with (https://github.com/joelmccracken/joelmccracken.github.com/blob/writing/_jekyll-image/Dockerfile) was way more complicated than I initially expected! 4
Some of it is probably unnecessary and cargo-culted, but I tried pulling some of it out and ran into issues (e.g. not running as jekyll
user).