How to Host Multiple Instances of Ghost with Docker and NGINX

How to Host Multiple Instances of Ghost with Docker and NGINX

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.