Ghost With nginx, Docker, and Lets Encrypt

Ghost

WordPress has long been the go to CMS for simple blogging, but recently while investigating self-hosting options (for other applications (Thanks https://apps.sandstorm.io/ !)), I stumbled across Ghost. Not just a generic CMS, but built specifically for blogging. Sounds like something I want to try out. So that's exactly what I'm doing.

update- I've since moved off of using nginx. I now use Kubernetes and Traefik (which has automated Lets Encrypt) for my reverse proxying. Maybe a blog post on that soon? But this is still a Ghost blog!

Step 1: Docker Image

To make this easier, I had Docker already installed, so this was just a matter of docker pull ghost to get the image. Ghost is an official repository on Docker Hub for people who like that sort of thing. There are also a few "pre-configured" images out there.
Then to get the image up and working docker run --name blog -v /server/ghostdata:/var/lib/ghost -p 2368:2368 -d ghost finishes it off. This creates a container named "blog" and maps /var/lib/ghost where the blog data is stored to /server/ghostdata and forwards container 2368 (which is used by default for the internal node server) to host 2368.
At this point, ghost is up and running, but it's only accessible in a limited way. Also, it is necessary to edit /server/ghostdata/config.js at this point. Since I didn't set a node environment, it defaults to using "development", so under the development section of the config file, I updated the url line to url: 'https://blog.nickbisby.net'. This makes links in the site use the appropriate URL instead of using localhost:2368 which outside visitors won't have access to.

Step 2: nginx config

The next step is to expose our ghost container using nginx. I have a standard format for nginx configs that I like to use.
The first step is to force all non SSL traffic to redirect to https (which won't work yet) and put a catch in place to handle the Lets Encrypt challenges.

  listen 80;
  server_name blog.nickbisby.net;
  location '/.well-known/acme-challenge' {
    default_type "text/plain";
    root /tmp/letsencrypt-auto;
  }
  location / { return 301 https://$server_name$request_uri; }
}

Step 3: Lets Encrypt Certificates

The most important part of enabling SSL is to get the SSL certs. For this I used a tool from Lets Encrypt.

With the Lets Encrypt git repo cloned, the webroot cert tool is used.
letsencrypt-auto certonly -a webroot --webroot-path=/tmp/letsencrypt-auto -d blog.nickbisby.net
This automatically handles everything by making a cert, signing something with it, placing that in webroot-path, and then using nginx config above, I tell it to look in webroot-path when going to .well-known/acme-challenge. This allows them to verify that I am in control of the cert and the domain, and they can validate it for me.

Step 4: Enable SSL on nginx

The second nginx step is to forward the SSL traffic to the docker container. We use our new Lets Encrypt certs and some proxy headers.

  listen 443 ssl;
  server_name blog.nickbisby.net;
  ssl_certificate /etc/letsencrypt/live/blog.nickbisby.net/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/blog.nickbisby.net/privkey.pem;
  location / {
    proxy_pass http://127.0.0.1:2368;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_set_header X-Forwarded-Proto $scheme;
  }
}

The proxy_set_headers are important, because when the config.js url is set to something starting with https, it causes a redirect if the request doesn't come through https. The proxy request is not https. Without the proxy headers to correct for this, an infinite redirect would occur.

That's all there is to it. Get a Docker image and set it up. Forward non-SSL traffic to SSL, and make a catch for Lets Encrypt challenges. Run Lets Encrypt. Forward SSL to Docker.

update - The Lets Encrypt client was intended to be a reference implementation of an ACME client. Many people believed it to be the only "official" client, which is not what Lets Encrypt wanted. As such, the client referenced above has been taken over by EFF and renamed Certbot (https://certbot.eff.org/). This allows separation of client and server identities. Some sources will have letsencrypt and others will have certbot (for instance, Debian does not change packages for anything other than security releases, so they still provide letsencrypt in their repositories).

If you cannot find letsencrypt, check for certbot. Or if you feel adventurous, check out some of the other great client implementations: https://github.com/certbot/certbot/wiki/Links