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.