Using Ghost blogging behind a Nginx reverse proxy with SSL

I've been thoroughly enjoying using Ghost as my website platform. This post outlines the challenge I faced when hosting multiple websites, including this Ghost blog, on a single home server with a Nginx reverse proxy to serve those websites, with SSL.

Note that this post is not a guide on how to setup Ghost, how to setup Nginx as a reverse proxy, or how to setup SSL.

My setup

Your setup likely differs, however the concepts apply to setups using containers and/or VMs to host multiple websites on a single server.

My setup:

  • A server with a Proxmox hypervisor
  • Multiple websites hosted by the server, with each website on their own separate LXC container (or VM). Each LXC is running a Linux distribution.
  • A container with Nginx installed to act as a reverse proxy
  • Access to all of the websites is via HTTPS only, thus will redirect any HTTP requests to HTTPS automatically
Figure 1. A Proxmox server hosting multiple websites via LXCs (Linux Containers).

Note the following details:

  • Each LXC has a unique IP
  • The Nginx reverse proxy listens on ports 80 (for HTTP requests) and 443 (for HTTPS requests)
  • Website 1 and 2 do not listen to port 80. When our server receives a HTTP request (which is by default on port 80), we can't have multiple LXCs trying to serve the same request.

The goal: Use Nginx as a reverse proxy to serve the Ghost blog with SSL

Our goal is to have multiple domain names point to the same server, and for the traffic to be routed to the appropriate container/VMs:

Figure 2. Traffic is received by our Nginx reverse proxy container/VM, then routed to the appropriate container/VMs
  1. Arriving traffic from ports 80 or 443 should be handled by Nginx. HTTP requests (port 80) will be redirected as HTTPS requests instead (port 443).
  2. Depending on the domain name received (e.g. myghostdomain.com), Nginx will route the request to the respective container/VM (via its IP address) that hosts the website for that domain name.

For example, if our Ghost blog is accessible by myghostdomain.com, we want it to route traffic to the LXC that is hosting Ghost (IP 192.168.0.20 in our example above).

The issue: It wasn't working...

My issue came from following the official 'How to install & setup Ghost on Ubuntu' guide, as well as my novice understanding of Nginx reverse proxies.

During the Ghost setup it offers to configure Nginx and SSL for you, with the annoying quote of "Setting up Nginx manually is possible, but why would you choose a hard life?".

Well, Ghost. Turns out I should manually setup Nginx when I am running multiple websites from a single server and require a reverse proxy..!

The reason being is that we want our Nginx reverse proxy to handle the SSL certificate (and connections via HTTPS) for us, and not the container running Ghost.

Nginx config for the container/VM hosting the Nginx reverse proxy

This post assumes that you already have a setup similar to Figure 1: a server with a container/VM that has Nginx installed to act as the reverse proxy, and other containers/VMs each hosting additional websites. If not, there is a large amount of better guides online to assist with that step.

Setup SSL certificate for your domain

For any of this post to be relatable, you must first have a registered domain name that is setup to point to your server's IP address

On your Nginx container/VM, I recommend using Certbot for setting up and managing your SSL certificates. If using Ubuntu, I found this excellent DigitalOcean guide. Ensure that you have your domain setup for SSL, and the certificates are stored in your Nginx container.

Nginx config

On your Nginx container/VM, browse to /etc/nginx/sites-available/ and create a .conf for your domain name, for example myghostdomain.com.conf.

Note: when setting up Nginx or Certbot, a .conf with your domain name may have already been created, so use that or start from fresh.

Below is the configurations we want for myghostdomain.com.conf:

server {
     # Respond to HTTP traffic (port 80) with the domain 
     # myghostdomain.com here
     listen 80;
     server_name myghostdomain.com www.myghostdomain.com;
     
     # Route HTTP traffic back to this server,
     # but now from port 443 (for HTTPS). This will be
     # responded to according to the code in the server
     # block below.
     return 301 https://myghostdomain.com$request_uri;
}


server {
	# Respond to HTTPS traffic (port 443) for the
    # domain myghostdomain.com
    listen 443 ssl http2;
    server_name myghostdomain.com www.myghostdomain.com;


	# Direct to where the SSL certficates are stored. Usually
    # they are stored in a folder with the same name as your domain name.
    
    # This will resolve HTTPS connections.
    ssl_certificate /etc/letsencrypt/live/myghostdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/myghostdomain.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;


    location / {
     
      client_max_body_size 100M;
   
      proxy_set_header        Host $host;
      proxy_set_header        X-Real-IP $remote_addr;
      proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header        X-Forwarded-Proto $scheme;


	  # The important piece goes here. Route requests to the appropriate 
      # container/VM where Ghost is being hosted.
      # In my example, we want to route to the LXC with IP 192.168.0.20.
      
      # NOTE: Also recongise that we are routing to
      # a port other than 80/443. Let's use port 8000 here.
      proxy_pass          http://192.168.0.20:8000/;
      proxy_read_timeout  90;
      proxy_redirect      http://192.168.0.20:8000 https://myghostdomain.com;

	  
      # Here are some recommended security configs for
      # hosting websites. I recommend you do further research
      # for how to best secure your websites.
      
      add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"; #HSTS
      add_header X-Frame-Options DENY; #Prevents clickjacking
      add_header X-Content-Type-Options nosniff; #Prevents mime sniffing
      add_header X-XSS-Protection "1; mode=block"; #Prevents cross-site scripting attacks
      add_header Referrer-Policy "origin";
    }
}

The above settings will receive any traffic from ports 80 and 443 for myghostdomain.com, and direct them to the container/VM hosting our Ghost instance, in this example to IP 192.168.0.20 on port 8000.

Remember to activate your .conf files by moving them to the sites-enabled directory (instead of our current sites-available directory). We should do so by creating a symlink:

ln -s /etc/nginx/sites-available/myghostdomain.com.conf /etc/nginx/sites-enabled/myghostdomain.com.conf

Remember to always reload Nginx when you make changes to your .conf files:

service nginx reload
For your other domains, for example 'mycoolblog.net', repeat the same steps as above by creating unique .conf files for each domain

Nginx config for the container/VM hosting Ghost

On your container/VM that is hosting Ghost, we need to receive traffic from port 8000 (routed from our Nginx reverse proxy), and redirect it to the Ghost instance. Again we will create a myghostdomain.com.conf file in /etc/nginx/sites-available/.

In myghostdomain.com.conf we want:

server {

	# Respond to traffic from port 8000 (as
    # per our Nginx reverse proxy config before),
    # for the domain myghostdomain.com
    listen 8000;
    server_name myghostdomain.com www.myghostdomain.com;
    
    access_log /var/log/nginx/ghost.log;
	
  	# The below are default settings from Ghost,
    # so there should be no need to change them.
    location / {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $http_host;
        proxy_set_header X-NginX-Proxy true;

        proxy_pass http://127.0.0.1:2368;
        proxy_redirect off;
    }
	
    # This specifies what size files we allow
    # to be uploaded to our Ghost blog.
    client_max_body_size 50M;
    
}

Again, remember to synlink the .conf file in /etc/nginx/sites-available/ so that it is also present in /etc/nginx/sites-enabled/, like so:

ln -s /etc/nginx/sites-available/myghostdomain.com.conf /etc/nginx/sites-enabled/myghostdomain.com.conf

Always reload for our setting to take affect:

service nginx reload

Summary

Browsing to myghostdomain.com, we should now be shown our Ghost blog as per Figure 2. Essentially:

  1. Internet traffic from port 80/443 is received by the Nginx container/VM
  2. Nginx will read the HTTP headers to extract the domain name (e.g. myghostdomain.com), and route to the container/VM that is hosting Ghost (IP address 192.168.0.20, port 8000 in our example).
  3. The Nginx instance on the container/VM hosting Ghost will receive the request (via port 8000), and route the request to the Ghost instance which is running locally (IP 127.0.0.1, port 2368)

I originally spent many hours trying to get the Nginx reverse proxy directing traffic to my instance of Ghost, after much Googling. I hope that by sharing my answers it may help you.

Potential troubleshooting solutions

Below are a few steps that helped me solve issues:

  • Ensure that your home router has ports 80 and 443 forwarded to the appropriate container/VM that is hosting your Nginx reverse proxy
  • Ensure that you setup the symlink correctly between your .conf files in /etc/nginx/sites-available and /etc/nginx/sites-enabled. If you just copied+pasted the .conf files across, they will be unlinked, and therefore any changes you make in one will not be reflected in the other.
  • You may have left over or default .conf files still present in your /etc/nginx/sites-enabled/ directories. Ensure that any unwanted ones have been removed!
  • I recommend opening a new browser Incognito window to ensure that old cached instances of your websites aren't being presented instead
  • You can try accessing your Ghost blog directly (assuming it is on your LAN) by typing the IP address of the container/VM that hosts Ghost, like so:
192.168.0.20:8000/ghost

Note that we specified the port 8000 (as per our .conf setup), and we must also specify the /ghost directory, else it won't find Ghost.

  • Ensure that myghostdomain.com is in fact pointing to your server's IP. A simple ping command ping myghostdomain.com in the terminal should tell you where your domain points to.