Flexible blogging with Ghost, Docker, and CoreOS

When it comes to blogging software I can't but admit to being a profligate and a philanderer. Never content, I'm always hoping that that next platform is going to be The One (TM) that finally satisfies my discontent of the moment. Years ago I actually started with MovableType. I've used WordPress and Tumblr and Blogger and most of the rest. WordPress is a fine platform but it's just too much of everything any more. There's no focus to it because it tries to be all things to all bloggers. That's why I've completely revamped my blogging infrastructure based on CoreOS 1, Docker 2, and Ghost 3.

Ghost as a blogging platform

After ditching WordPress and deciding to give Ghost a try, my first decision was how to run the software. The company behind Ghost supports the development by selling a premium hosted platform; it's very reasonably-priced for what you get. I already had an account on Rackspace, however, so it didn't make sense for me to pay for a hosted service when I could just run the OSS version on my own cloud server.

I started out—surprisingly for me—by reading the documentation and getting started guide on the Ghost website. Although the docs didn't address it specifically, I knew I would be better off running this from a Docker container. As my investigation continued and I decided to base my infrastructure on CoreOS, using Docker became a must.

I could have built my own Docker custom image but I found that Ghost has an official image on Docker Hub. Since there didn't seem to be much in the way of custom configuration needed to run it, I decided to give the unaltered image a try. It was easy to test this on my local MacBook Pro, where I have the xhyve-based beta version of Docker running. It really was as simple as a single docker run command:

> mkdir blog && docker run -v $(pwd)/blog:/var/lib/ghost -p 2368:2368 ghost

I pulled up the admin console by navigating to localhost:2368/ghost/ in Chrome.

Ghost

I was taken to a screen that let me set up my user. In the default configuration, everything is saved to an sqlite database in the filesystem. This is perfect for my needs since I also set up an automagic backup system using GitHub and inotifywait. More on that later.

Setting up Ghost was super easy. I was immediately impressed by the immaculately clean layout and the simplicity of the design. I could quickly and easily find my way around the interface and I didn't have any problems finding the customizations I wanted for the theme I was using (Odin).

Ghost Admin

At this point I was sold. I didn't really need to see too much more. Markdown editing, simple administration, easy setup; it really was everything I'd been hoping for in a blogging platform.

I took the time to set up a nice-looking theme that would work well for just about any kind of content I care to publish: technical articles, photography, creative writing, or whatever.

Remote Sync and Backup

At this point I needed to figure out how to deploy and sync the changes I'd made while evaluating Ghost. I knew I wanted to keep my resources in GitHub so I could have a record of changes and be able to revert to a previous version of something should that be necessary. I created a repository in my GitHub account and moved my entire blog directory into that repo and added everything to git. This adds the sqlite database file which contains all my blog settings and content as well as the CSS and theme resources I tweaked. With a simple git clone on my server I could have my content ready to be served up by Ghost.

Running Ghost in the Cloud

Now I had to figure out how to host Ghost on my Rackspace cloud server. Having been a long-time user of Ubuntu, I first thought I might just run the server as an init process. But after taking a look at my rather outdated Ubuntu box (10.04), I decided it was time for the cobbler to make his kids some shoes and bring the system up-to-date.

I once read that one of my favorite authors Umberto Eco said you could learn a lot about someone by looking at the books in their library which they had't yet read. His reasoning was that people tend to buy books about things which they want to learn. Eco's principle holds true for me in technology matters as well. I chose to run this blogging platform on CoreOS because it's a technology I think is important for the future of data computing and I wanted to learn more about it. The best way to do that is to install it and try to do something meaningful.

Rackspace offers CoreOS as a standard image so I simply rebuilt my old server on CoreOS Alpha (I tend to prefer the latest-and-greatest over something more stable).

Systemd Unit

Now that I was ready to deploy Ghost to my CoreOS server, I needed to translate the docker run command I used in testing to a more production-level systemd unit file. The CoreOS documentation for this is top-notch, so it didn't take that long; I simply copied the example Unit file and modified it to suit my deployment. I decided to put the clone of my repository in /var/blogs and use 8081 as the port for this instance of Ghost since I planned to add another blog for our local historical society in the near future. The unit file turned out to be very simple.

/etc/systemd/system/jbrisbin.service

[Unit]
Description=jbrisbin.com Blog  
After=docker.service  
Requires=docker.service

[Service]
TimeoutStartSec=0  
ExecStartPre=-/usr/bin/docker rm --force jbrisbin  
ExecStartPre=/usr/bin/docker pull ghost  
ExecStart=/usr/bin/docker run --name jbrisbin -v /var/blogs/jbrisbin:/var/lib/ghost -p 8081:2368 ghost

[Install]
WantedBy=multi-user.target  

Since my content is all stored in git (more on that in a second), I decided to leave in the ExecStartPre instructions from the example that remove any existing image when the service starts. This has the side effect of potentially doing an upgrade of Ghost whenever the server is restarted. One of the problems I always had with blogging software pre-Docker was keeping things up-to-date. Thankfully this is simply not an issue any more. I may end up commenting this out at some point if it becomes a problem. At the very least I might put in a version tag for the Docker image to fix the version of Ghost I'm using. In a "real" production environment you'll probably want to be a little more conservative with your upgrade settings.

To enable and start the service, I used systemctl.

$ systemctl enable /etc/systemd/system/jbrisbin.service
$ systemctl start jbrisbin.service

Since I had previously set up my SSH keys and done a git clone of my repository, I pulled up the server and got the home page I had created on my local machine.

jbrisbin.com Home

I have a hard time seeing how this could have been easier!

Watching for Changes

Now that I was able to start making changes to my live blog, I needed to make sure that content gets backed up and that I can re-create the server easily if something untoward happens. Since all my content (and some of my config) is stored in git, I figured it would be pretty easy to create a file watcher service that could tell when changes happened to my application files and automatically do a git commit and push to ensure those changes are backed up. This simple systemd-based service is running right now and every time I stop typing to let Ghost save the draft of this article, the service is committing those changes to git for immediate backup!

The service is simply a Docker image based on Alpine Linux which means size-wise is itsy bitsy (that's a technical term for really small). That fits well with my goals of having a minimal infrastructure to care for. It also saves on disk space on my CoreOS server which is pretty small.

I did some research to find an efficient way to watch the filesystem for changes and inotify-tools quickly floated to the top of Google's search rankings. I wrote a simple bash script that used inotifywait to poll for changes to my application files and then either run a git add or a git rm, depending on the event.

I quickly discovered I needed to filter out some events because sqlite writes a *-journal file when it makes changes to your database. I didn't want these transitory files gumming up the works so I excluded them.

/usr/sbin/git-watch.sh

#!/bin/bash

WATCH_DIR=$1

echo "Watching $WATCH_DIR"

while true; do  
  inotifywait -r -e modify -e delete --exclude '-journal$' $WATCH_DIR | while read FILE; do
    event=$(echo $FILE | awk '{print $2}')
    file=$(echo $FILE | awk '{print $1$3}')

    cd $WATCH_DIR

    if [ "$event" == "MODIFY" ]; then
      git add $file
    elif [ "$event" == "DELETE" ]; then
      git rm -f $file
    fi

    git pull
    git commit -m "Change made to $file"
    git push
  done
done  

Admittedly this service is very naive. It doesn't even attempt to deal with conflicts. But the goal here was really just to provide an automated backup and give a minimum amount of flexibility in allowing me to make simple changes locally and deploy them to my server by pushing those changes to my git repo.

To run the service in CoreOS it has to be in a Docker image, so I created a simple one that uses this script as an ENTRYPOINT.

Dockerfile

FROM alpine  
MAINTAINER Jon Brisbin <jon@jbrisbin.com>

RUN \  
  apk update && \
  apk add inotify-tools git bash openssh-client

COPY git-watch.sh /usr/sbin/git-watch.sh  
RUN chmod a+x /usr/sbin/git-watch.sh

ENTRYPOINT ["/usr/sbin/git-watch.sh"]  

Finally, I created a unit file to run the service in systemd and enabled it per the documentation.

/etc/systemd/system/git-watch-blogs.service

[Unit]
Description=Watch Blogs  
After=docker.service  
Requires=docker.service

[Service]
TimeoutStartSec=0  
ExecStart=/usr/bin/docker run -v /root/.ssh:/root/.ssh -v /root/.gitconfig:/root/.gitconfig -v /var/blogs:/var/blogs git-watch /var/blogs  
Restart=always

[Install]
WantedBy=multi-user.target  

After enabling and starting the git-watch-blogs.service I was able to make a change to the content in the Ghost admin panel and see the automatic commit show up in my repo on GitHub.

$ systemctl enable /etc/systemd/system/git-watch-blogs.service
$ systemctl start git-watch-blogs.service

Conclusion

This lightweight container-based solution is far easier to maintain, cleaner to administer and easier to upgrade and synchronize with external changes than my previous WordPress and Tumblr blogging platforms. Ghost itself is a joy to use and is beautiful to look at. CoreOS and Docker are extremely versatile arrows to have in my quiver and using them to create ad-hoc services fits my needs perfectly.

Within the next couple of weeks I plan to add another Docker container to serve the website of our local historical society which is in dire need of an upgrade. My plan is to just add another directory to my git repository, deploy another Ghost service and enjoy the benefits of simple and free backup thanks to my git-watch sync service.


[1] - https://coreos.com/
[2] - https://www.docker.com/
[3] - https://ghost.org/ and https://hub.docker.com/_/ghost/

Jon Brisbin

Read more posts by this author.