Hosting a Ghost blog with NGINX and Docker (Part 2)

Part 2: Ghost

After reading Jake Hamilton's interesting post on hosting a Ghost blog with Docker, I thought it would be good to share my own Ghost + Docker blog setup.

This will be a two-part post, with this second post focussing on integrating Ghost.

If you haven't read the previous post on Docker setup, check that out first!

Introduction

This post (like Part 1) will assume a basic understanding of Docker and how to work with Docker containers.

The setup outlined in this post will support running a Ghost blog at a subdomain of your own domain (in these examples blog.agchapman.com), but can also be run on it's own domain.

The examples here will assume you're using basically the same setup as outlined in Part 1, so some modification may be needed to suit your own environment.

Setting up your DB

Ghost, by default, ships using an SQLite database for data and configuration, but personally I prefer to use a "real" database, so I'm going to be using a separate MariaDB instance. Fortunately, Docker Compose makes this trivial:

version: '2'

services:  
  db:
    image: 'bitnami/mariadb:latest'

Running docker-compose up -d now will only create a new empty MariaDB container, so let's leave that for a moment.

Bitnami

Who is this bitnami/mariadb and why should you trust them? Bitnami are an awesome crowd who build ready-to-deploy, batteries-included packages for a wide range of applications across Linux, Windows and macOS. They use simple installers, come pre-configured (as much as possible), and are dead-simple to work with. Even better, they provide Docker images of many of their packages applications, making using them in complicated deployments a snap.

Adding Ghost to the mix

Now, it's time for the real app: Ghost. Installing Ghost from one of Bitnami's Docker images couldn't be easier. Just add a new service to your docker-compose.yml:

version: '2'

services:  
  db: ...
  blog:
    image: 'bitnami/ghost:latest'
    expose:
      - '2368'
    depends_on:
      - mariadb

There is also an official ghost image available, which uses the built-in SQLite DB.

Now, if we ran docker-compose up -d, you'd also get a Ghost blog, but it would be running on a custom port (2368), and only from within linked Docker containers. To expose our Ghost blog, we only need to add a few extra options:

version: '2'

services:  
  db: ...
  blog:
    image: 'bitnami/ghost:latest'
    expose:
      - '2368'
    depends_on:
      - mariadb
    environment:
      - VIRTUAL_HOST=blog.agchapman.com

That all-important VIRTUAL_HOST variable tells the nginx-proxy container we set up in Part 1 to create a new reverse proxy config in Nginx for the blog.agchapman.com domain name. By default, the config will point at the only port that the blog service exposes: 2368.

Finally, we can run docker-compose up and watch as Docker creates a new MariaDB container and a new Ghost container and links them together. In the background, the nginx-proxy container will create a new config entry for the new container.

You should now be able to browse to blog.agchapman.com in a browser and (assuming you have your DNS set correctly), it will bring you to your new Ghost blog.

SSL

As I outlined in Part 1, if you want SSL support (powered by the letsencrypt-nginx-proxy-companion container), we also need to add a few more variables to our configuration:

version: '2'

services:  
  db: ...
  blog:
    image: 'bitnami/ghost:latest'
    expose:
      - '2368'
    depends_on:
      - mariadb
    environment:
      - VIRTUAL_HOST=blog.agchapman.com
      - LETSENCRYPT_HOST=blog.agchapman.com
      - [email protected]

Those two new variables, LETSENCRYPT_HOST and LETSENCRYPT_EMAIL, are used by the ssl-companion container (attached to the nginx-proxy) to automatically generate a new SSL certificate from Let's Encrypt and inject the correct configuration into the reverse proxy. Note how we did not need to enable SSL or change any configuration in Ghost itself: Our reverse proxy will handle (and terminate) the SSL and communicate with the Ghost instance over plain old HTTP.

Now there's no excuse to run anything over HTTP!

Volumes

This setup will create un-named data volumes in the default location to hold blog and app data: functional, but not the easiest to manage. Alternatively, you can use the volumes node to add named volumes, or host directories, to mount at /bitnami/mariadb or /bitnami/ghost repsectively.

Personally, as a general rule, I use named volumes for DBs and host directories for app data

Next steps

As I mentioned in Part 1, you can apply the same VIRTUAL_HOST trick used here to any container, so you can also spin up web servers, CMSs, documentation sites, anything really! Simply include VIRTUAL_HOST (and LETSENCRYPT_HOST and LETSENCRYPT_EMAIL for SSL support), and everything else will take care of itself!

Example

Don't believe me? Let's add a website, just for shits and giggles (TM):

version: '2'

services:  
  web:
    image: 'bitnami/apache' #did I mention I love Bitnami!
    environment:
      - VIRTUAL_HOST=apache.agchapman.com
      - VIRTUAL_PORT=80 # since otherwise it will default to 443
  ... # snipped

That's it! Once you start the web server (with docker-compose up), hitting apache.agchapman.com (in this example) will get automatically proxied to our new Apache container on port 80!

Summary

While it seems a little complex at first glance, this is a dead-simple method of running Ghost with zero installation, no upgrade woes, isolated app data and no dependencies to worry about. When integrated with a larger Docker environment (such as the one I use now), you can run a multitude of services, applications and tools out of a single host with almost no configuration. The only thing that slows me down now is adding new DNS names!

As always, leave any feedback in the comments or hit me up on Twitter!

Useful links:

Comments

comments powered by Disqus