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

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 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.