Running Ghost on Digital Ocean with Docker
As stated in a previous post I changed a bunch of things to my blog.
It still uses Ghost as the blogging platform and docker to run all the necessary software.
It still runs on a Digital Ocean "droplet" (Virtual Private Server) preconfigured with Docker.
Some things have changed:
- I'm running ghost version 1.x now, instead of 0.11, this was a big rewrite the Ghost team did
- I no longer use MySQL for the blog's database but Sqlite, the default
- I used more automated tooling for nginx and Let's Encrypt
- I use docker-compose instead of my own scripts to start and stop the appropriate docker containers
The goal was to simplify things a little compared to how I was running the blog before. There's a lot of interesting stuff I learned while setting this up.
The first thing I want to talk about is docker compose and the various docker containers I'm running.
Docker-compose
In my previous setup I was using scripts to start the required docker containers. For example, to start the Ghost container, the script looked like this:
#!/bin/bash
../scripts/settings.sh
docker run --rm --name $container_name -v $PWD/data/$container_name/var/lib/ghost:/var/lib/ghost -v $PWD/config/config.js:/var/lib/ghost/config.js -e "NODE_ENV=production" --link $mysql_container_name:mysql -d ghost:0
That's a big command.
With docker-compose you define what containers you need and what options you want to pass in a configuration file called "docker-compose.yml".
You can then run sudo docker-compose up
to start all these containers as configured. And sudo docker-compose down
to stop and remove the images again.
My docker-compose.yml configuration currently looks like this:
version: '3.1'
services:
nginx-proxy:
image: jwilder/nginx-proxy
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro
- ./nginx/certs:/etc/nginx/certs:ro
- 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-companon:
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
- vhost.d:/etc/nginx/vhost.d
- nginx.html:/usr/share/nginx/html
environment:
- NGINX_PROXY_CONTAINER=nginx-proxy
ghost:
image: ghost:1-alpine
restart: always
depends_on:
- "nginx-proxy"
ports:
- 127.0.0.1:8080:2368
volumes:
- ./blog/data/ghost:/var/lib/ghost/content
environment:
- url=https://blog.aaronlenoir.com
- VIRTUAL_HOST=blog.aaronlenoir.com
- LETSENCRYPT_HOST=blog.aaronlenoir.com
- LETSENCRYPT_EMAIL=info@aaronlenoir.com
volumes:
vhost.d:
nginx.html:
Docker-compose let's you define "services" that need to run. And allows you to configure the options. In my case, I need to run 3 containers based on the following images:
- jwilder/nginx-proxy
- jrcs/letsencrypt-nginx-proxy-companion
- ghost:1-alpine
Docker Image: ghost:1-alpine
This is version 1 of Ghost. The alpine image is based on a lighter linux image than the usual images. But what I care about is Ghost of course.
The image, when started, offers you a blog where you can start with creating a user account and write posts.
I had to export / import my old posts though. Luckily this is something the old and new version of Ghost support. Except for images, which I had to copy over myself. Luckily, the expected folder structure stays the same so I could copy my image folder completely.
Docker Image: jwilder/nginx-proxy
This image does what I used to do manually: it runs an nginx instance in docker that acts as a proxy to the various services running in docker.
Nginx, inside the container, accepts connections on port 80 and 443 and forwards the requests to the appropriate docker container. It knows which container to forward requests to based on the hostname in the request.
In docker, my blog runs on a standard port 2368. I choose to expose that internal port on my system as port 8080:
ports:
- 127.0.0.1:8080:2368
Note that Ghost only listens on the local loopback device, because I don't need any external connections directly to my blog. Only nginx will be talking to it directly.
The nginx-proxy image will automatically proxy incoming connections on the public port 80 and 443 to this image, if I assign it the appropriate VIRTUAL_HOST environment variable.
- VIRTUAL_HOST=blog.aaronlenoir.com
If at some point I decide to run another web service on another port, the nginx-proxy would see this, and also forward this port according to another hostname.
I used to configure nginx myself. This image requires less work, but it does require me to trust that this image does the proxying in a correct way.
Docker Image: jrcs/letsencrypt-nginx-proxy-companion
This image extends the nginx-proxy with automated Let's Encrypt certificate installation and renewal.
It will automatically request certificates from Let's Encrypt for all containers that have these environment variables set:
- LETSENCRYPT_HOST=blog.aaronlenoir.com
- LETSENCRYPT_EMAIL=info@aaronlenoir.com
It checks every hour if the certificates exist and if they need renewal. If renewal is required, it'll automatically get them and load them in to the nginx server.
This is also simpler than in the past, where renewing the certificates required some manual work from me.
As soon as my DNS update for blog.aaronlenoir.com was propagated to the right servers, my new droplet received the certificates and the blog was live, with a good Certificate.
Docker Compose
This means I can now start my blog using this command:
sudo docker-compose up -d
Note: -d
is for "detached", so the containers run in the background
I can stop it with:
sudo docker-compose down
And I can update the containers (and thus my blog and webserver) using:
sudo docker-compose pull
Followed by a restart of the containers using docker-compose down
and docker-compose up
.
Conclusion
This is how the new blog runs. There are some details like the volume setup and the custom Ghost theme which I didn't go into.
You can check out the full configuration on github: github.com/AaronLenoir/aaronlenoir.com.
I'll try to write some more things about the specific problems I had and what the solutions were.