While ghost was great in that it provided a very convenient editor via the browser, I decided to switch to using Hugo hosting on GitLab. The maintenance of the Linux server hosting Ghost on Azure became too much and upgrading was always painful for some reason. However, I may still go back to Ghost and will keep this post here for future reference. Anywys, this post is mostly about Docker and less about Ghost.
This site is My previous site at www.metamost.com was made possible by the very popular combination of docker containers by Jason Wilder and Yves Blusseau. That is, an nginx server, running in a Docker container is forwarding all traffic to the container running the Ghost instance. This forwarding is configured as soon as the Ghost container comes up by docker-gen, another running docker container. Finally, the letsencrypt companion registers the url (www.metamost.com) with the fantastic Let’s Encrypt certificate authority, and thus you might see a green lock icon next to the url in your browser.
Ghost is no longer the new kid on the block, but it seems that WordPress is still dominant in terms of usage. I had started down the road of using WordPress, but for various reasons, it did not play well in the docker-gen/nginx-proxy framework I was used to. It was on a whim I tried Ghost and it just worked out of the box, SSL and everything. And while Ghost might do well to develop or support several more free themes, the core features are very impressive and easy to use.
All of this is controlled by a single docker-compose.yaml
file, along with the nginx template file which is necessary because the docker-gen image is not nginx-specific and the template file tells docker-gen what to do when a new docker container is brought up. So let’s take a look at the compose file:
version: '3'
volumes:
nginx-conf:
nginx-vhost:
nginx-html:
nginx-certs:
nginx-htpasswd:
ghost-db:
ghost-content:
networks:
proxy:
ghost:
services:
nginx-proxy:
container_name: nginx-proxy
image: nginx
restart: always
networks:
- proxy
ports:
- "80:80"
- "443:443"
security_opt:
- label:type:docker_t
volumes:
- ./nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl:ro,z
- nginx-conf:/etc/nginx/conf.d
- nginx-vhost:/etc/nginx/vhost.d
- nginx-html:/usr/share/nginx/html
- nginx-certs:/etc/nginx/certs:ro
- nginx-htpasswd:/etc/nginx/htpasswd:ro
docker-gen:
container_name: docker-gen
image: jwilder/docker-gen
restart: always
networks:
- proxy
security_opt:
- label:type:docker_t
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro,z
- ./nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl:ro,z
- nginx-conf:/etc/nginx/conf.d
- nginx-vhost:/etc/nginx/vhost.d
- nginx-html:/usr/share/nginx/html
- nginx-certs:/etc/nginx/certs:ro
- nginx-htpasswd:/etc/nginx/htpasswd:ro
command: >-
-notify-sighup nginx-proxy -watch -wait 5s:30s
/etc/docker-gen/templates/nginx.tmpl
/etc/nginx/conf.d/default.conf
nginx-ssl:
container_name: nginx-ssl
image: jrcs/letsencrypt-nginx-proxy-companion
restart: always
networks:
- proxy
security_opt:
- label:type:docker_t
volumes:
- nginx-vhost:/etc/nginx/vhost.d
- nginx-html:/usr/share/nginx/html
- nginx-certs:/etc/nginx/certs
- /var/run/docker.sock:/var/run/docker.sock:ro,z
environment:
NGINX_PROXY_CONTAINER: nginx-proxy
NGINX_DOCKER_GEN_CONTAINER: docker-gen
ghost-db:
container_name: ghost-db
image: mysql:5.7
restart: always
networks:
- ghost
security_opt:
- label:type:docker_t
volumes:
- ghost-db:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: <password>
ghost:
container_name: ghost
image: ghost:2-alpine
restart: always
networks:
- proxy
- ghost
security_opt:
- label:type:docker_t
volumes:
- ghost-content:/var/lib/ghost/content
environment:
VIRTUAL_HOST: sub.domain.tld
LETSENCRYPT_HOST: sub.domain.tld
LETSENCRYPT_EMAIL: admin@domain.tld
url: https://sub.domain.tld
admin_url: https://sub.domain.tld
database__client: mysql
database__connection__host: ghost-db
database__connection__user: root
database__connection__password: <password>
database__connection__database: ghost
Of course, for this website, “sub.domain.tld” is really “www.metamost.com” and the database password is some ridiculously long random string. I’ve always liked Alpine Linux as a secure, no-nonsense and well-supported distribution for servers and docker containers and so was very happy to see Ghost had an Alpine image I could use directly.
Detail
First, you’ll notice I used docker volumes (almost) exclusively - with the exception of the nginx template file. Also, I created one network on which all web traffic will flow and another which will be used by Ghost to communicate with the database - MySQL in this case though I might try mariadb in the future.
version: '3'
volumes:
nginx-conf:
nginx-vhost:
nginx-html:
nginx-certs:
nginx-htpasswd:
ghost-db:
ghost-content:
networks:
proxy:
ghost:
Not all of these volumes need to be backed up. Indeed ghost-db
and ghost-content
are the only critical volumes that need to be saved off. The other volumes are there just so they can be shared between the “services” as defined in the compose file.
At first I was mounting host directories into the containers, but I found that docker makes using these volumes quite easy since it handles permissions, security contexts, and migration should I want it.
Here is the now classic web-proxy combination of Jason Wilder’s docker-gen
and nginx
:
nginx-proxy:
container_name: nginx-proxy
image: nginx
restart: always
networks:
- proxy
ports:
- "80:80"
- "443:443"
security_opt:
- label:type:docker_t
volumes:
- ./nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl:ro,z
- nginx-conf:/etc/nginx/conf.d
- nginx-vhost:/etc/nginx/vhost.d
- nginx-html:/usr/share/nginx/html
- nginx-certs:/etc/nginx/certs:ro
- nginx-htpasswd:/etc/nginx/htpasswd:ro
docker-gen:
container_name: docker-gen
image: jwilder/docker-gen
restart: always
networks:
- proxy
security_opt:
- label:type:docker_t
volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro,z
- ./nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl:ro,z
- nginx-conf:/etc/nginx/conf.d
- nginx-vhost:/etc/nginx/vhost.d
- nginx-html:/usr/share/nginx/html
- nginx-certs:/etc/nginx/certs:ro
- nginx-htpasswd:/etc/nginx/htpasswd:ro
command: >-
-notify-sighup nginx-proxy -watch -wait 5s:30s
/etc/docker-gen/templates/nginx.tmpl
/etc/nginx/conf.d/default.conf
The nginx.tmpl
file is locally attached to the two volumes and most of the nginx directories are shared. Only the docker-gen
container gets access to the docker socket as we don’t necessarily want to expose that in the same container that is receiving web requests.
The security_opt
section added to each service:
security_opt:
- label:type:docker_t
is there to make selinux happy and allow proper read and write permissions in the docker_t
“role.”
The Let’s Encrypt companion container mounts most of the nginx volumes (it’s the only one that mounts certs as read-write). It also needs access, though read-only, to the docker socket:
nginx-ssl:
container_name: nginx-ssl
image: jrcs/letsencrypt-nginx-proxy-companion
restart: always
networks:
- proxy
security_opt:
- label:type:docker_t
volumes:
- nginx-vhost:/etc/nginx/vhost.d
- nginx-html:/usr/share/nginx/html
- nginx-certs:/etc/nginx/certs
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
NGINX_PROXY_CONTAINER: nginx-proxy
NGINX_DOCKER_GEN_CONTAINER: docker-gen
Finally, the database and website host itself. Here, I went with MySQL and Ghost’s “1-alpine” image since I had difficulty getting their “ghost:alpine” image to work, though I’m not entirely sure why - maybe it’s a development tag?
ghost-db:
container_name: ghost-db
image: mysql:5.7
restart: always
networks:
- ghost
security_opt:
- label:type:docker_t
volumes:
- ghost-db:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: <password>
ghost:
container_name: ghost
image: ghost:1-alpine
restart: always
networks:
- proxy
- ghost
security_opt:
- label:type:docker_t
volumes:
- ghost-content:/var/lib/ghost/content
environment:
VIRTUAL_HOST: sub.domain.tld
LETSENCRYPT_HOST: sub.domain.tld
LETSENCRYPT_EMAIL: admin@domain.tld
url: https://sub.domain.tld
admin_url: https://sub.domain.tld
database__client: mysql
database__connection__host: ghost-db
database__connection__user: root
database__connection__password: <password>
database__connection__database: ghost
So the content lives in the ghost-db
and ghost-content
docker volumes. This includes themes which are found in <ghost-content>/themes
.