Ghost setup with separate, independent NGINX under SSL

I haven’t found documentation for setup in a case where Ghost runs on the Docker container independently and a separate NGINX container is its proxy and I’m running into problems. This is a standard use case for a larger micro-service based website. In my case, I want the website to use SSL. Example below:

https://www.example.com/ => nginx => ghost
https://www.example.com/api => nginx => api

From what I’ve found it seems that SSL and proxying is only supported by Ghost when using the internal strategies as explained here. This seems to couple the proxying layer with Ghost, which is not ideal in many cases (in this case).

I’m going through the steps of running Ghost locally with docker-compose to mimic my production functionality, so I can troubleshoot. My setup looks something like this:

version: '3'

services:
  ghost:
    image: ghost:latest
    environment:
      - url="https://local.example.com"
    ports:
      - "2368"
    volumes:
      - ../../../ghost-content:/var/lib/ghost/content
  nginx:
    build: ../../../../docker/my-nginx
    depends_on:
      - ghost
    ports:
      - "443:443"

Let’s just assume NGINX is correctly configured and looks something like this:

upstream ghost {
  server ghost:2368;
}

server {
  listen 443 ssl;

  server_name local.example.com;

  ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
  ssl_certificate /etc/nginx/ssl/localhost.crt;
  ssl_certificate_key /etc/nginx/ssl/localhost.key;

  location / {
    proxy_pass http://ghost;
  }
}

Issue #1: Ghost appears to redirect to the internal host name (the upstream host from NGINX perspective). Internally, within the container this host is simply “ghost” in my case, so it redirects to “http://ghost”. To reproduce, navigate to https://local.example.com or https://local.example.com/ and note the redirect to “http://ghost” (which is the internal hostname). I’m confused. I know Ghost redirects requests from the root domain to “/”, but what about when there is already a trailing slash in the URL? And it seems to redirect to the internal host name vs the URL specified in configuration (in my case the url environment variable).

I’m going to document my issues as I continue investigation, but would appreciate any feedback.

You’re missing proxy headers that configure the nginx proxy behaviour to do what you want, it doesn’t do it out of the box.

Some ref material includes the standard nginx template if you use Ghost CLI:

And our docs for if you were proxying to Ghost(Pro)

Which includes an nginx template:

There’s a bunch of different settings in there that are worth looking up, but specifically you’d want to be setting X-Forwarded-For, X-Forwarded-Host and probably X-Forwarded-Proto to pass the orignal settings through to Ghost.

2 Likes

Thanks @Hannah. That was quite promising and I appreciate the thoughtful response. The issue still exists for me. Here are a couple of examples.

  • When navigating to https://local.example.com the redirect goes to http://ghost
  • When navigating to https://local.example.com/ghost/ the page errors out with local.example.com redirected you too many times.

To reproduce I made the changes, removed everything in Docker and restarted on a clean slate.

This assumes Docker Compose networking. Kubernetes is similar in that it exposes a host name based on the container name.

Ghost seems to be redirecting to this internal host name and ignoring the env url variable.

This is my full config now, based on the feedback (and added what I have in full). Still reproducing issue #1 described in this post.

upstream ghost {
  server ghost:2368;
}

server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;

  server_name local.example.com;

  ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
  ssl_certificate /etc/nginx/ssl/localhost.crt;
  ssl_certificate_key /etc/nginx/ssl/localhost.key;

  access_log /var/log/nginx/example.com.log;

  location = /500.html {
    root /usr/share/nginx/html;
    internal;
  }

  location = /500.json {
    default_type application/json;
    root /usr/share/nginx/json;
    internal;
  }

  location / {
    proxy_set_header X-Forwarded-Host local.example.com;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Real-IP $remote_addr;
    error_page 500 502 503 504 /500.html;
    proxy_pass http://ghost;
  }
}

Okay, my troubleshooting steps were wrong. I wasn’t rebuilding my NGINX docker container :man_facepalming:

@Hannah had it right. This block did the trick:

proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

Next step for me will be to figure out how I can update my production site to use SSL. I’ll post my findings here. I’m thinking it will just entail updating my NGINX container and a find and replace in the DB… but we’ll see :fearful:

I was going to say the initial redirect problems sound like a Ghost that is configured to use https but not getting the right trust proxy setting, so it’ll just loop trying to redirect to https.

What it redirects to is different for admin requests and frontend requests. In admin land we strictly adhere to config.url - redirecting to the exact config setting, in frontend land it’s a bit more lenient and it’ll redirect to https://whatevertherequesthostwas.com.

Anyway glad to hear that with a bit of a restart/rebuild you got to the right thing.

I don’t know if resolver config (ref the Ghost(Pro) nginx template) is relevant in docker/kubernetes land, but for regular proxies if you want nginx to be tolerant of Ghost’s IP changing it is needed.

For production, the config changes shouldn’t be much different. Ghost does not store any absolute paths in the DB under normal circumstances so it should just be a case of getting the proxy configured and https in config.url.

1 Like

I have it working in production (Kubernetes) with just making the same updates and this deployment spec:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: example-blog
spec:
  replicas: 1
  selector:
    matchLabels:
      app: example-blog
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: example-blog
    spec:
      volumes:
        - name: example-claim
          persistentVolumeClaim:
            claimName: example-claim
      containers:
      - name: foo-blog
        image: ghost:latest
        imagePullPolicy: Always
        # livenessProbe:
        #   httpGet:
        #     path: /
        #     port: 2368
        #     scheme: HTTPS
        #   # give it 1 minute for the first check
        #   initialDelaySeconds: 60
        #   periodSeconds: 30
        #   timeoutSeconds: 5
        # readinessProbe:
        #   httpGet:
        #     path: /
        #     port: 2368
        #     scheme: HTTPS
        #   # give it 1 minute for the first check
        #   initialDelaySeconds: 60
        #   periodSeconds: 30
        #   timeoutSeconds: 5
          failureThreshold: 16
        volumeMounts:
          - mountPath: /var/lib/ghost/content
            name: example-claim
        env:
          - name: url
            value: "https://www.example.com"
        ports:
        - containerPort: 2368
      imagePullSecrets:
        - name: regcred
      restartPolicy: Always
status: {}

The only issue I’m having now is that the liveness and readiness probes (commented) out aren’t working. They were the same before for but without the HTTPS scheme.

Previously this worked.

readinessProbe:
  httpGet:
    path: /
    port: 2368

When I SSHd in I did a curl of http://localhost:2368/ and noticed the request redirected to https://localhost:2368/. I thought because of that the commented code would work… but it isn’t… not sure why.

The output from kubectl describe <pod> included the below:

Readiness probe failed: Get https://192.168.22.3:2368/: http: server gave HTTP response to HTTPS client

I got the probes to work by mimicing NGINX essentially:

readinessProbe:
  httpGet:
    path: /
    port: 2368
    httpHeaders:
      - name: Host
        value: "https://www.example.com"
      - name: X-Forwarded-Host
        value: "https://www.example.com"
      - name: X-Forwarded-Proto
        value: https

Thanks for your help everyone :heart: I hope this helps people Googling for answers.