After hosting my blog on WordPress for a number of years, I fancied a change, so I switched to Ghost.
Ghost is an open source blog platform built on Node.js. It's front end templating system is based on Handlebars.js - which is great because I don't have to refresh my stale PHP skills whenever I need to make a theme customisation.
I needed to set up a couple of instances - one for my blog, and one for my games website, so I configured a docker-compose file with Ghost, MySQL and NGINX to run them all from Docker containers. To deploy, I simply push any changes to Github and restart docker-compose.
Here's how I did it.
Configuring Docker Services
Starting with an empty compose file docker-compose.yml
, we'll add NGINX as our first service.
Setting up NGINX in Docker Compose
version: '3.1'
services:
nginx:
We'll be using the latest NGINX image; adding volumes to persist nginx.conf
and our SSL keys; and opening up ports 80 and 443 to http+/s traffic.
version: '3.1'
services:
nginx:
image: nginx
restart: always
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
- /etc/letsencrypt:/etc/letsencrypt
- /etc/ssl:/etc/ssl
ports:
- 80:80
- 443:443
Next, we'll configure the MySQL service to store our blog posts.
Setting up MySQL in Docker Compose
Add a new service called database, and tell it to use the MySQL v8 image from the Docker Hub.
version: '3.1'
services:
nginx:
image: nginx
restart: always
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
- /etc/letsencrypt:/etc/letsencrypt
- /etc/ssl:/etc/ssl
ports:
- 80:80
- 443:443
database:
image: mysql:8.0
restart: always
environment:
MYSQL_ROOT_PASSWORD: A_REALLY_LONG_PASSWORD
Here, we set up a root password for accessing the MySQL database. Next, we'll add a volume to persist the database on our server.
version: '3.1'
services:
nginx:
image: nginx
restart: always
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
- /etc/letsencrypt:/etc/letsencrypt
- /etc/ssl:/etc/ssl
ports:
- 80:80
- 443:443
database:
image: mysql:8.0
restart: always
environment:
MYSQL_ROOT_PASSWORD: A_REALLY_LONG_PASSWORD
volumes:
- ghost-database:/var/lib/mysql
volumes:
ghost-database:
Be sure to align volumes:
correctly, or docker-compose will think that you're configuring a service called 'volumes'.
Setting up Ghost in Docker Compose
We're going to set up two instances of Ghost as two separate services, and connect them to the same database service. Be sure to set up a volume to persist media and images - in Ghost, this is the content
folder.
version: '3.1'
services:
...
ghost1:
image: ghost:latest
restart: always
volumes:
- ./ghost1/content:/var/lib/ghost/content
ghost2:
image: ghost:latest
restart: always
volumes:
- ./ghost2/content:/var/lib/ghost/content
It's worth remembering that volumes are mapped host:container
, so the volumes above mount ./ghost1/content
and ./ghost2/content
on the host as /var/lib/ghost/content
inside each container.
The last step we need to take to configure the two Ghost instances is to add the database connection, NODE_ENV and Ghost URL to the containers' environment variables.
version: '3.1'
services:
...
ghost1:
image: ghost:latest
restart: always
volumes:
- ./ghost1/content:/var/lib/ghost/content
environment:
NODE_ENV: production
database__client: mysql
database__connection__host: database_host
database__connection__user: database_user
database__connection__password: A_REALLY_LONG_PASSWORD
database__connection__database: ghost1
url: https://yourfirstghostwebsite.com
ghost2:
image: ghost:latest
restart: always
volumes:
- ./ghost2/content:/var/lib/ghost/content
environment:
NODE_ENV: production
database__client: mysql
database__connection__host: database_host
database__connection__user: database_user
database__connection__password: A_REALLY_LONG_PASSWORD
database__connection__database: ghost2
url: https://yoursecondghostwebsite.com
That's everything for the docker-compose file. Put it all together and you get this:
version: '3.1'
services:
nginx:
image: nginx
restart: always
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
- /etc/letsencrypt:/etc/letsencrypt
- /etc/ssl:/etc/ssl
ports:
- 80:80
- 443:443
ghost1:
image: ghost:latest
restart: always
volumes:
- ./ghost1/content:/var/lib/ghost/content
environment:
NODE_ENV: production
database__client: mysql
database__connection__host: database_host
database__connection__user: database_user
database__connection__password: A_REALLY_LONG_PASSWORD
database__connection__database: ghost1
url: https://yourfirstghostwebsite.com
ghost2:
image: ghost:latest
restart: always
volumes:
- ./ghost2/content:/var/lib/ghost/content
environment:
NODE_ENV: production
database__client: mysql
database__connection__host: database_host
database__connection__user: database_user
database__connection__password: A_REALLY_LONG_PASSWORD
database__connection__database: ghost2
url: https://yoursecondghostwebsite.com
database:
image: mysql:8.0
restart: always
environment:
MYSQL_ROOT_PASSWORD: A_REALLY_LONG_PASSWORD
volumes:
- ghost-database:/var/lib/mysql
volumes:
ghost-database:
You can test if this works by running the command docker compose -f docker-compose.yml up -d
- this will run docker compose
using your compose file (`-f`), and will spawn it as a detached process. To stop all four containers, simply run docker compose down
.
In it's current state, your setup will pull NGINX, MySQL and Ghost images from the Docker Hub, and start the containers - but we still need to provide NGINX with a config file to serve the two instances of Ghost. Let's create that now.
Configuring NGINX as a Reverse Proxy
Open the NGINX configuration file, typically located at /etc/nginx/nginx.conf
, and make the necessary modifications. At a minimum, you’ll need to create a new server block for each instance of Ghost that you want to host. Each server block should specify the domain or subdomain to listen on and the corresponding port or IP address where the Ghost instance is running.
The really important part of this configuration is the proxy_pass
directive. Docker (really neatly) provides it's own DNS, so you can reference containers by their name and Docker will resolve the name to it's address on the Docker network. The proxy_pass
lines below connect to each Ghost instance's container at the default Ghost IP.
server {
listen 80;
server_name instance1.com;
location / {
proxy_pass http://ghost1:2368;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
server {
listen 80;
server_name instance2.com;
location / {
proxy_pass http://ghost2:2368;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Replace instance1.com
and instance2.com
with your domain names, and then configuration to ensure that everything is working correctly. Open a web browser and navigate to the domain or subdomain associated with each Ghost instance. If everything is set up properly (and you've forwarded your domain's IP to your web server) NGINX should forward the requests to the correct instance, and you should see the corresponding Ghost blogs.
There is a lot more to setting up NGINX (SSL, mime_types, caching etc) but that's beyond the scope of this article. You should now have everything you need to set up more than one instance of Ghost, using NGINX as a reverse proxy, with Docker.