Configuring NGINX Proxy Manager for self-hosted Ghost CMS

Configuring NGINX Proxy Manager for self-hosted Ghost CMS
Photo by Philipp Katzenberger / Unsplash

Trying to get this site visible from the web, I made a mistake that cost me a bit of time trying to figure out. Maybe this will help you too.

I am running this site on Ghost 6.0, installed on a clean Ubuntu 24 LXC in Proxmox. I also have a reverse proxy on another LXC to handle inbound traffic for the various services and servers I have in my home lab. This was created using the excellent tteck installation script for NGINX Proxy Manager.

Ghost configures NGINX by default to accept incoming connections and forward them to the ghost server (running on port 2369). The Ghost setup also handles automatically obtaining an SSL certificate and configuring NGINX to handle that, but since I was terminating SSL at my primary reverse proxy, I didn't need opt for that. The setup was therefore done to accept http connections on port 80.

Without direct access to the configuration files on the NPM server (since NPM should be handling the configuration), I was adding a custom location for / and then adding the proxy_set_header entries into the advanced config field:

NGINX Proxy Manager UI showing custom configuration field with data.

This was resulting in all sorts of issue including HTTP 400 responses.

Digging into where NPM stores it's conf files, (/data/nginx/proxy_host/) I saw that this resulted in a conf that looked like this:

location / {
    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;

    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Scheme $scheme;
    proxy_set_header X-Forwarded-Proto  $scheme;
    proxy_set_header X-Forwarded-For    $remote_addr;
    proxy_set_header X-Real-IP          $remote_addr;

    proxy_pass       http://192.168.1.8:80;

    # Force SSL
    include /etc/nginx/conf.d/include/force-ssl.conf;
}

Snippet of my primary NGINX conf file showing double entries.

So these were entries were being doubled up and causing the issue, even though it passed the nginx testing.

It turns out that adding a location in NPM automatically adds the relevant proxy_set_header lines, and sets the proxy_pass line to the IP set in the UI. This means you should not put anything into the custom config box in the NPM UI.

If you are using the social features of Ghost (e.g. ActivityPub) there are some additional primary NGINX configurations for that, that you also need to make sure are forwarded to the Ghost NGINX config.

This are the examples that are working for me:

Note the custom config is blank.

Infinite Redirects

The next issue I had was a little more easily solved as there is documentation specifically for this: https://docs.ghost.org/faq/proxying-https-infinite-loops

When you configure ghost with the public url that includes https://, the NGINX config on the ghost server needs to have it's X-Forwarded-Proto header hard-coded to https otherwise it will see the insecure http connection from the primary NGINX and try to re-route it.

The location / section of your ghost server NGINX should look something like this:

location / {
    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 https;

    proxy_pass http://127.0.0.1:2368;
}