This post summarizes the steps needed to setup a blog like this, using Ghost on DigitalOcean with CoreOs, Docker, Nginx and Let's Encrypt.

Setup the tools

During this tutorial we are using a (recent) Ubuntu Linux environment, able to run docker. We also assume that we control the DNS of our domain.

We need initially to setup docker :


sudo apt-get install apt-transport-https ca-certificates curl software-properties-common

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
   $(lsb_release -cs) stable"

sudo apt-get update

sudo apt-get install docker-ce

Then we need also docker-compose and docker machine:


sudo curl -L \
  https://github.com/docker/compose/releases/download/1.22.0/docker-compose-$(uname -s)-$(uname -m) \
  -o /usr/local/bin/docker-compose
  
sudo chmod +x /usr/local/bin/docker-compose

curl -L https://github.com/docker/machine/releases/download/v0.14.0/docker-machine-$(uname -s)-$(uname -m) \ 
  > /tmp/docker-machine
  
sudo install /tmp/docker-machine /usr/local/bin/docker-machine

Create the host

Now we can create the droplet to host our docker environments on DigitalOcean. We opt for a CoreOs droplet, hosted in a small 1Gb virtual machine.  

Luckily most of the setup is handled by the docker machine with:


docker-machine create --driver=digitalocean \
  --digitalocean-access-token=${DIGITAL_OCEAN_ACCESS_TOKEN} \
  --digitalocean-image=coreos-stable --digitalocean-region=ams3 \
  --digitalocean-size=s-1vcpu-1gb --digitalocean-ssh-user=core \
  coreos-base

Once the CoreOs droplet is set up, we can edit our DNS to point our blog host name to the droplet IP address, verifiable using docker-machine ls, which hopefully will write something like:

NAME          ACTIVE   DRIVER         STATE     URL                        SWARM   DOCKER        ERRORS
coreos-base   *        digitalocean   Running   tcp://xx.xx.xx.xx:2376           v18.03.1-ce  

Setup the blog

It's now time to start our blog, initially we will setup a simple http, using the official ghost docker image. We start by writing a docker-compose.yaml file:

version: '3.1'
services:
  ghost:
    image: ghost:2-alpine
    restart: always
    ports:
      - "80:2368"
    volumes:
      - ghost.data:/var/lib/ghost/content
    environment:
      - url=http://www.ourdomain.com 
volumes:
  ghost.data:

We use the alpine version for the image to spare some disk space, and a named volume ghost.data to hold our blog data even when the images are not running.

To launch the service inside the machine we already created  we need to point our docker commands to it, using docker-machine env, then start our compose file:


eval $(docker-machine env coreos-base)

docker-compose up -d

We may browse http://www.ourdomain.com/ghost and set up the admin user.

Add SSL

Once the blog is up and running, we can add https, using the free Let's Encrypt certificates.  To do that we need a Nginx proxy (jwilder/nginx-proxy) as SSL terminator to hide the ghost image behind it. The Nginx certificate update will be handled by an automatic updater: jrcs/letsencrypt-nginx-proxy-companion.

version: '3.1'
services:
  nginx-proxy:
    image: jwilder/nginx-proxy:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/tmp/docker.sock:ro
      - nginx.certs:/etc/nginx/certs:ro
      - nginx.vhost.d:/etc/nginx/vhost.d
      - nginx.html:/usr/share/nginx/html
    labels:
      com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy: "true"
  nginx-proxy-companion:
    image: jrcs/letsencrypt-nginx-proxy-companion
    depends_on:
      - "nginx-proxy"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - nginx.certs:/etc/nginx/certs:rw
      - nginx.vhost.d:/etc/nginx/vhost.d
      - nginx.html:/usr/share/nginx/html
    environment:
      - NGINX_PROXY_CONTAINER=nginx-proxy
  ghost:
    image: ghost:2-alpine
    restart: always
    depends_on:
      - "nginx-proxy"
    expose:
      - "2368"
    volumes:
      - ghost.data:/var/lib/ghost/content
    environment:
      - url=https://www.ourdomain.com 
      - NODE_ENV=production
      - VIRTUAL_HOST=www.ourdomain.com,ourdomain.com
      - LETSENCRYPT_HOST=www.ourdomain.com,ourdomain.com
      - LETSENCRYPT_EMAIL=info@ourdomain.com
volumes:
  nginx.vhost.d:
  nginx.html:
  nginx.certs:
  ghost.data:

Again we prefer the alpine version of the images.  The  nginx-proxy service needs read access to the docker socket to inspect the exposed ports and the VIRTUAL_HOST environment variables of the services it proxies, an so does the  nginx-proxy-companion service, which performs the Let's Encrypt dance.


FOLLOW-UP

Exposing the docker socket  to a container is not a security best practice, read my follow-up article Improving the blog security to see how the same results can be achieved without exposing the docker socket.