Run a Self Updating Ghost Blog with Docker

Want to run your own Ghost blog in Docker? How about a little sprinkle of continuous integration on top to make it self-updating? This guide is for you.

Run a Self Updating Ghost Blog with Docker

[Update 02-20-20] Updated the docker-compose.yml and update scripts to use Ghost v3 instead of Ghost v2.

Ghost is an awesome open-source blogging platform. Docker is an awesome container and orchestration platform. Why not marry the two and make your sysadmin life (which we all do out of necessity) super easy?

I should mention that this blog itself is running Ghost within Docker. Basically everything that you're reading here is a living example of following the instructions below. #StreetCred

Running Ghost yourself can be a little tough especially if you want to run multiple applications on a single server (like me). Docker solves a lot of these pain points and gives you a clean and portable container you can run anywhere. It allows you to not have to worry about the runtime environment. You don't have to worry about what version of node you're running, CLI commands, system compatibility, etc. It also makes your life super easy when you want to migrate your ghost files to another host or cloud platform.

Given the frequency of Ghost updates, it can also be cumbersome to have to continually update your Ghost instance. That can be solved with just using their SaaS service, but what can you do if you're running your own instance? So the goal of today's post is to outline how you can get Ghost up and running in a Docker / Nginx environment, and how to set it up to be self-updating.

If you haven't seen the Ghost Docker or Github pages I encourage you to check them out.

Overview

Here's the scenario we are creating at a high level.
Basic Traffic Flowchart

Prerequisites

If you already meet these then just skip to the guide section and find what's relevant for you. I'm covering a little more scope to help folks out who are less familiar.

  1. Your own domain / URL that you want to deploy your Ghost blog on.

You can use NameCheap, GoDaddy, 1and1, etc. Point your desired DNS name to your host's public IP.

  1. A host with Docker running somewhere (in the cloud, on your own server, doesn't matter)

If you are already set up: How to install Docker on Ubuntu 16.04

If you aren't set up DigitalOcean gives you an excellent 1 click option

DigitalOcean Droplets

My Digital Ocean Referral link - if you want to get $10 free credit

  1. Nginx installed on your host

Install Nginx on Ubuntu 16.04

I am only posting DigitalOcean links because I use them myself and have found their tutorials immensely useful. You can feel free to use them if you'd like, or use your own. If you have your domain and DNS set up, and your host ready to go with Docker and Nginx installed, you are ready to proceed.

Guide

Part 1: Get your Ghost blog container running
Part 2: Configure Nginx to route traffic to Ghost
Part 3: Setup Continuous Integration so Ghost will update automatically

Part 1 - Set Up the Container

If you are familiar with using Docker you may think we can actually just run the container with a single line run command. You are absolutely able to pull and run a Ghost container that way. However, I have elected to create and set up a docker-compose file for your ghost container to make things consistent and repeatable.

If you aren't familiar with using Docker the concept is this: We are creating a config file with your specific settings with which to run the application (container). We can then add and remove containers as needed and get consistent results because we are using the exact same configuration every time.

Docker Compose Setup

Docker Compose doesn't come installed with Docker by default. The easiest way to install it is to use pip.

sudo pip install docker-compose

You can verify that it's installed properly like this:

whereis docker-compose
docker-compose: /usr/local/bin/docker-compose
docker-compose --version
docker-compose version 1.21.2, build a133471

Docker-compose.yml Reference

First we're going to create a directory for all things ghost related, then create the config file in that directory. In the YAML file that we create we will specify all of our runtime settings specific to your blog.

mkdir ghost
cd ghost
touch docker-compose.yml
nano docker-compose.yml

This will open up the editor for this config file. Whenever you see curly braces {} below, it means you need to put in your own variables. You can use the following as a reference. The main couple things if you are going to be deploying this in a production-level setting is to specify your url under environment as well as the mail settings. Lastly, you will want to adjust the username under volumes to the account that you will be using. For this guide we are using the default sqlite3 database. If you want to run MySQL as your database engine you will need to set the database environment variables. If you are interested in running MySQL for your Ghost blog please check out this docker compose file.

Please refer to the official documentation on the specific configuration variables here. The mail specific settings are here. If some of this is foreign to you I would suggest spending a little time familiarizing yourself with Ghost first before proceeding.

# docker-compose.yml

version: '3.2'

services:

  ghost:
    # We are specifying the Ghost Image to run
    image: ghost:3-alpine
    
    # We want the container to restart in case the host gets restarted
    restart: always
    
    # Map host port 3050 to container port 2368
    ports:
      - 3050:2368
      
    # Mount host directory to container directory
    volumes:
      - /{username}/ghost/data:/var/lib/ghost/content
      
    # Specify your Ghost blog URL and mail server settings
    environment:
      url: {Your Blog URL}
      mail__transport: SMTP
      mail__options__service: {Your Mail Service}
      mail__options__auth__user: {Your User Name}
      mail__options__auth__pass: {Your Password}

Save this file and quit.

Running the Ghost Container

We can then run start the container with this command:

docker-compose up

You will see some messages about pulling and running the docker container. You should now be able to check that the container is running with the following:

docker ps -a
CONTAINER ID        IMAGE                                    COMMAND                  CREATED             STATUS                  PORTS                    NAMES
8daf35773818        ghost:1-alpine                           "docker-entrypoint..."   47 hours ago        Up 47 hours             0.0.0.0:3050->2368/tcp   ghost_ghost_1
docker logs ghost_ghost_1
[2018-06-25 23:34:08] INFO Finished database migration! 
[2018-06-25 23:34:10] WARN Theme's file locales/en.json not found. 
[2018-06-25 23:34:10] INFO Ghost is running in production... 
[2018-06-25 23:34:10] INFO Your blog is now available on yourblog.com 
[2018-06-25 23:34:10] INFO Ctrl+C to shut down 
[2018-06-25 23:34:10] INFO Ghost boot 2.422s 

The logs should tell you that Ghost is up and running successfully! We are now well on our way. Next we'll take a look at setting up Nginx to proxy web traffic to your Ghost container.

Part 2 - Nginx Configuration

Nginx is going to do a few of things for us:

  1. HTTPS -> HTTP Reverse Proxy
  2. Allow us to use as many containers as we want behind the proxy
  3. Static File Hosting for your blog (Tutorial coming soon)

We're going to go through some basic Nginx configuration. An in-depth guide is available here. I'm covering the basics to get you up and running.

Nginx Config File

First we need to create our config file.

sudo touch /etc/nginx/sites-available/blog
sudo nano /etc/nginx/sites-available/blog

This will open our nano editor with a blank file. You will want to fill in your blog URL into this configuration. Replace yourblog.com with your actual DNS name.


# Sample Nginx HTTP Configuration
server {
        # Your server URL here
        server_name yourblog.com;
        listen [::]:80;
        listen 80 ;
    # disable any limits to avoid HTTP 413 for large image uploads
    client_max_body_size 0;
    
    # required to avoid HTTP 411: see Issue #1486 (https://github.com/docker/docker/issues/1486)
    chunked_transfer_encoding on;
    
    location / {
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-NginX-Proxy true;
            proxy_pass http://0.0.0.0:3050/;
            proxy_ssl_session_reuse off;
            proxy_set_header Host $http_host;
            proxy_cache_bypass $http_upgrade;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_read_timeout 900;
            proxy_redirect off;
    }

}

Starting Nginx

Once you've copied and pasted your information into this file, we are ready to test and activate. Don't worry we will be setting up SSL later. Save and quit, you should be back at your command line.

sudo ln -s /etc/nginx/sites-available/blog /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx

Our server should now be running on port 80, listening for HTTP traffic. By default the Ghost app runs on port 2368, but through our Docker-compose file we are mapping host port 3050 -> 2368. Thus our Nginx configuration is routing traffic to port 3050. If you want to choose a different port you are welcome to do that as well, just make sure you update the docker-compose.yml file to refelct it.

Verifying Ubuntu Firewall Rules

As a final check please make sure your firewall is allowing traffic to Nginx. You can check to verify with this:

ufw status
Status: active

To Action From


Nginx Full ALLOW Anywhere
Nginx Full (v6) ALLOW Anywhere (v6)

If you don't see Nginx listed on here, please add it using this command:

sudo ufw allow 'Nginx Full'

Run the status command again to verify that Nginx is listed.

Adding SSL

We are now going to use certbot to automatically generate some SSL certificates for us. Note this is only going to work if you've set up your DNS properly. Make sure you have gone to your DNS service and pointed yourblog.com to the public IP of your host.

Certbot uses LetsEncrypt to automatically assign you a free SSL certificate. For more information you can refer to this.

First we are going to download and install certbot to assist us.

sudo add-apt-repository ppa:certbot/certbot
sudo apt-get update
sudo apt-get install python-certbot-nginx

Next we are going to use certbot to automagically set up SSL for us! Replace yourblog.com with the actual DNS name of your blog.

sudo certbot --nginx -d yourblog.com

You will be prompted with a challenge to verify that you're the actual domain owner. Once you have verified the challenge it's going to automatically set up the relevant SSL configuration inside your nginx config. It will also ask you whether or not to redirect all HTTP traffic to HTTPS - I highly recommend you use the automatic redirect (select option 2).

If you are having issues with this piece I suggest taking a look at the official documentation for Certbot.

If you've set everything up correctly up until this point, you should now be able to verify and test that your blog works by going to https://yourblogurl.com/ghost/!!

Part 3 - Continuous Integration Setup (Scheduled Auto Updating)

This is a little bit of a hack, but I think it works pretty well. We are going to create a shell script that pulls a new container image from the Docker Hub, shuts down the old image, and replaces it with the newer one.

touch update.sh
nano update.sh

This will open up our shell script in the nano editor. You can copy and paste the following script into your editor. Basically we are pulling a new build, stopping the old one, removing the old container, and starting a new instance. Since we have set our environment configuration properly the new container will spin up with all of the same settings and your data will be preserved. This downtime will last seconds.

#! /bin/sh
echo Pulling New Build
docker pull ghost:3-alpine
echo Stopping Old Build
docker-compose down
echo Starting New Build
docker-compose up -d ghost

Next we need to make our script executable.

chmod +x update.sh

Lastly we are going to set up cron to run our script on a weekly basis. This will mean that your Ghost blog will auto update every week. If you want to do it every month, day, hour, etc. you can as well - just set the proper interval in the standard cron format. Running this command will open up your crontab editor:

crontab -e

We are just concerned with the bottom (last) line here. You will want to copy and paste it - but replace the {user} with the account that you are running the containers under.

# Edit this file to introduce tasks to be run by cron.
#
# Each task to run has to be defined through a single line
# indicating with different fields when the task will be run
# and what command to run for the task
#
# To define the time you can provide concrete values for
# minute (m), hour (h), day of month (dom), month (mon),
# and day of week (dow) or use '*' in these fields (for 'any').#
# Notice that tasks will be started based on the cron's system
# daemon's notion of time and timezones.
#
# Output of the crontab jobs (including errors) is sent through
# email to the user the crontab file belongs to (unless redirected).
#
# For example, you can run a backup of all your user accounts
# at 5 a.m every week with:
# 0 5 * * 1 tar -zcf /var/backups/home.tgz /home/
#
# For more information see the manual pages of crontab(5) and cron(8)
#
# m h  dom mon dow   command
0 0 * * 0 cd /{user}/ghost && ./update.sh

Save and quit the editor to commit the changes. Your cron job is now set up.

EDIT 07/01/18: Had to fix the cron command to change into the right directory first. Please note the change.

Conclusion

If you made it this far - congrats! Thanks for sticking with me. Hopefully you're up and running at this point. Let me know what you think. If you ran into any issues feel free to post in the comments section and I'll see if I can answer your question.

Take it a step further and add PrismJS support to your blog.