Syncing a Dockerised Ghost blog to DigitalOcean with automated backups

Update: This blog series has been updated for Ghost 2.x. If you've landed here looking to setup a new Ghost blog, you should follow the updated version.

In Part 1 we setup a Ghost blog running locally in Docker containers, wired together with Docker Compose. In Part 2 we deployed it to a Droplet (VPS) on DigitalOcean.

Along the way we've seen how building up our stack using Docker is a little like playing with lego. We join together a bunch of useful, single-purpose, bricks to make something bigger.

We now have a local and remote Ghost environment ready, but we're missing something- a way to keep them in sync; It's time to add the final piece in our lego pie!

Photo by billward / CC BY

While we're at it, we'll also setup automated backups to Dropbox. It probably won't help us in the event of a giant solar storm, but in less extreme scenarios might stop us from losing all those posts, gifs and memes!

This is the 3rd and final part of the series:

Cool, let's finish our setup!

Guide

  1. Why have a local environment?
  2. Taking a manual backup
  3. Adding automated backup with ghost-backup
  4. Putting our backups in Dropbox
  5. Restoring a backup
  6. Manual sync
  7. Using ghost-sync
  8. Testing the workflow
  9. Wrapping Up

Why have a local environment?

Before we continue, you might wonder why are we bothering with the local environment at all? We could use the Compose file we have already to up the stack on our Droplet, write and publish our posts there, and be done.

While that would be a valid approach (and might be all you need), there are some benefits to setting up our local environment:

  • We can publish posts locally to check the formatting and how they will look in the wild (Ghost has a great live-preview of Markdown, but it is still not as good as a complete rendering, especially if you have custom CSS)
  • We can modify our theme and detect problems before pushing it live
  • We can work offline (or on a terrible Internet connection!)

Taking a manual backup

As we created a separate data-only container for our data, we could take a manual backup by running:

docker run --rm --volumes-from data-coderunner.io -v ~/backup:/backup ubuntu tar cfz /backup/backup_$(date +%Y_%m_%d).tar.gz /var/lib/ghost

This would fire up a new container using the ubuntu image, mount our data container volumes, and then create a compressed and dated tarball of the entire /var/lib/ghost folder into the ~/backup folder mounted on our host. Nice.

Taking a file dump of the database in this way should only be done while it is shutdown or appropriately locked, or you risk data corruption

Once we have our tarball, we could restore it later with a similar method. This is okay, but we could do better.

We want to avoid having to shutdown our database for the backup, and we would be better off using dedicated tools to handle it. If you're running Ghost with sqlite there is the online backup API and for mysql/mariadb there is mysqldump. Also, it would be nice to have it automated.

For that purpose, I created ghost-backup, a separate container for managing backup and restore of Ghost.

Adding automated backup with ghost-backup

The ghost-backup image is published on Docker hub. We can use it by adding this to our docker-compose.yaml file:

# Ghost Backup
backup-blog-coderunner.io:
 image: bennetimo/ghost-backup
 container_name: "backup-blog-coderunner.io"
 links:
  - mariadb-coderunner.io:mysql
 volumes_from:
  - data-coderunner.io

This will create a ghost-backup container linked to our database container, taking a snapshot of our database and files every day at 3am and storing them in /backups.

The database link needs to be named mysql as shown, as this becomes the hostname that the container uses to communicate with the database

To change the defaults, for example changing the directory or backup schedule, you can customise the configuration

Because the Docker linking system exposes all of a source containers environment variables, the container can authenticate with mariadb without us having to configure anything.

Putting our backups in Dropbox

At the moment our backups are just being created on the Droplet. We should have a copy stored offsite to at least get us the '1' in a 3-2-1 backup strategy.

Seeing as we're dealing with a blog and not mission-critical data, one simple thing we can do is just push our backups to Dropbox, and there is a headless Linux client which makes this trivial.

We just need to set it up, and then change our backup location to use the Dropbox folder. Of course we're using Docker, so we should use a container for it! I put together a simple one in docker-dropbox.

We just need to add it to our docker-compose.yaml:

# Dropbox
dropbox:
 image: bennetimo/docker-dropbox
 container_name: "dropbox"

And then add the Dropbox container volume to our ghost-backup container:

 volumes_from:
  - data-coderunner.io
  - dropbox

The first time you launch the container, you'll see a link in the logs that you need to visit to connect with your Dropbox account.

Finally, we tweak our ghost-backup config to use the Dropbox folder as it's location:

environment:
  - BACKUP_LOCATION=/root/Dropbox/coderunner

And we're done. All our backups will now make there way to Dropbox.

By including the backup container in docker-compose.yaml it will be part of our local and live setup. As we'll see below that's what we want, but it probably makes sense to disable automated backups locally in docker-compose.override.yaml

Restoring a backup

A backup is no use if we can't restore it, and we can do that with:

docker exec -it backup-blog-coderunner.io restore -i

This will present a choice of all the backup archives found and ask which to restore. Alternatively, we can restore by file or by date.

Manual sync

Essentially a sync is a snapshot of our local environment, restored onto our live environment. As we now have our ghost-backup container configured on both, we could:

  1. Take a manual backup on the local environment (we can use docker exec backup-blog-coderunner.io backup for that)
  2. scp the created database and files archive to the Droplet
  3. Restore the archives on the Droplet with docker exec backup-blog-coderunner.io restore -f /path/to/file

For step 3 we would need to either use docker cp to put the archives into the ghost-backup container, or mount a directory from the host to the container for our restore archives

This approach would work, but it's a bit cumbersome and manual. With Dropbox setup we avoid step 2, but also have to check our sync folder until our files are ready for restore.

If our posts use a lot of images, our ghost files archive will also quickly become quite large to keep shipping around.

For simpler, 'one button' sync I created ghost-sync.

Using ghost-sync

ghost-sync uses rsync to transfer the images, so it's incremental, only copying accross anything new. For the database sync, it piggybacks off ghost-backup.

To set it up, we first need to add it to our docker-compose.override.yaml (as we only want it locally):

sync-blog-coderunner.io:
 image: bennetimo/ghost-sync
 container_name: "sync-blog-coderunner.io"
 entrypoint: /bin/bash
 environment:
  - SYNC_HOST=<dropletip>
  - SYNC_USER=<dropletuser>
  - SYNC_LOCATION=<syncfolder>
 volumes:
  - ~/.ssh/<privatekey>:/root/.ssh/id_rsa:ro
  - /var/run/docker.sock:/var/run/docker.sock:ro
 volumes_from:
  - backup-blog-coderunner.io
 links:
  - backup-blog-coderunner.io:backup

There's a few things going on here, so let's break it down.

We have overridden the entrypoint, which is the command that is run when the image is started, to prevent a sync happening when we up.

This issue tracks potential support for services that can be configured not to auto-start in Compose

We also mount an appropriate ssh private-key, and also set some environment variables so we can make a connection to the Droplet. The syncfolder is where ghost-sync will rsync all of the images to.

Finally for the database sync we need to be able to talk to the ghost-backup container, so we add it as a link and a volume, and mount the docker socket.

Now we just need to make two small additions in docker-compose.live.yaml:

data-coderunner.io:
 volumes:
  - /sync/coderunner.io/images:/var/lib/ghost/images

backup-blog-coderunner.io:
 volumes:
  - /sync/coderunner.io:/sync/coderunner.io:ro

We mount syncfolder/images as the Ghost images folder in our data-only container, so we can rsync directly to it. And we mount the syncfolder again in the backup container, so that we'll be able to initate a restore of our database archive from there.

ghost-sync can also sync the themes and apps folders with the -t and -a flags

At this point we have a way to sync between our environments, let's test it out!

Testing the Workflow

If you have followed everything up to this point, then you now have everything in place to enable our desired workflow.

  1. Create content at http://coderunner.io.dev/ghost
  2. Once happy, run docker-compose run --rm sync-blog-coderunner.io sync -id to push the content live by syncing the database and images
  3. View the content on http://coderunner.io

And we're done!

Wrapping Up

Now that you have a nice new Ghost blog setup, here's a few other things you might want to explore.

  • Customising your theme
  • The default Casper theme is a nice starting point, but there are loads of great free (and paid) themes available at places like Ghost Marketplace, All Ghost Themes or Theme Forest. I have another little container ghost-themer which might be useful for trying some out.
  • Adding Google Analytics
  • Adding comments
  • Adding other blogs/services; our modular Dockerised setup means we can setup other things behind our reverse proxy nice and simply. Of course you might need to upgrade to a more powerful Droplet :)

If you have any questions or suggestions about anything then feel free to leave a comment below :)