Is there a way to "force" use of relative URLs for images?

Hi everyone. I am trying to get Ghost working in my self-hosted environment. However, all the images (uploaded via the Ghost Editor) do not show. Everything else, eg the text, shows correctly.

I have found that the images are not loading due to them having “img src” of (1) absolute URL, (2) using http transport.

My current setup looks like this:

[Internet] — [Reverse Proxy] — [Ghost]

Ghost is set up to serve via plain http, i.e. no SSL.

Reverse Proxy serves via https, i.e. it adds https with an SSL certificate for incoming Internet requests, terminates/proxies requests to non-encrypted http Ghost at the backend.

So, while all the usual HTML text are rendered correctly, all “img src” generated by Ghost to use “http://FQDN/content/images/…” do not. Modern browsers detect these as “mixed content, i.e. http within https), and block them, and render them as “broken-link images”.

Perhaps one solution is for Ghost to not generate using absolute URLs, and use relative URLs like “/content/images/…”.

Another way is to set up Ghost backend to also serve https, but I am trying to avoid unnecessary encryption/decryption logic at backend, and just stick with plain http.

Does anyone have any good suggestions to help the images show properly in my setup?

It your website is served over ssl, you need to configure your Ghost URL to https://[...]. With th cli you can do this as ghost config set url https://... :slight_smile:

3 Likes

Hi @vikaspotluri123,

Actually, I have tried that before posting this help. It didn’t work for me. Ghost seemed to just send 301 Redirects. I am unsure why.

You need to pass X-Forwarded-Proto: https header from your reverse proxy to Ghost. Because otherwise Ghost will accept to serve itself with a proper https connection, since the url set as https://

2 Likes

Hi @muratcorlu,

I checked that the moment I got the 301. X-Forwarded-Proto was already set to https. In fact, I double confirmed the strange behaviour I’m witnessing by telnet-ing directly into Ghost at port 80, and supplying the headers directly. Something like:

GET / HTTP/1.1
Host: [FQDN, same as configured in Ghost]
X-Forwarded-Proto: https

And I get back:

HTTP/1.1 301 Moved Permanently
Server: nginx/1.24.0 (Ubuntu)
Date: SOME-DATETIME-STAMP
Content-Type: text/plain; charset=utf-8
Content-Length: 61
Connection: keep-alive
X-Powered-By: Express
Cache-Control: public, max-age=31536000
Location: https://FQDN
Vary: Accept, Accept-Encoding
X-Content-Type-Options: nosniff

Anyone knows if there is a way to use “relative URLs” for images instead? There may be a reason why Ghost is using “full URLs”. But maybe there is some undocumented way to disable this?

Does Ghost think it is https://fqdn, or something else, based on the config file?

I’m not aware of a way to force using images with relative URLs. (Excluding hacky ways, like post-processing HTML responses on reverse-proxy or CDN)

We’ve had several people run into this issue before, and the only correct fix is to get the URL updated - the URL is used in many other places, so relative urls for this broken case is a band-aid.

1 Like

Good point, relative urls will broke more places than it fixes (mails, activitypub reader, rss…)

I am not sure if my architectural use case where the [Reverse Proxy], which overlays with HTTPS, then proxies to [Ghost] backend, which serves HTTP, is rare…

Any ideas?

Seems like there is currently no “clean way” to fix the issue.

To close this “ticket”, I have decided to implement a post-processing hack to replace the “http:// FQDN” with “https:// FQDN”.

Thanks, all, for the discussion!

IMO the correct solution is to figure out what you’ve done wrong in your webserver configuration. It’s quite difficult to help without seeing it, so try comparing it with the template used by the CLI :slight_smile:

2 Likes

Hi @vikaspotluri123,

Thanks for helping to take at look at this. These are the relevant files:

  • This is a from-scratch Ghost site. I’ve used http:// blog. site002. com for initial install.
  • Then config url to https. Same FQDN.
  • Relevant nginx conf is not modified. Here’s what it looks like:
map $status $header_content_type_options {
    204 "";
    default "nosniff";
}

server {
    listen 80;
    listen [::]:80;

    server_name blog.site002.com;
    root /var/www/site002/system/nginx-root; # Used for acme.sh SSL verification (https://acme.sh)

    location ~ /.ghost/activitypub/* {
        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;
        proxy_set_header Host $http_host;
        add_header X-Content-Type-Options $header_content_type_options;
        proxy_ssl_server_name on;
        proxy_pass https://ap.ghost.org;
    }

    location ~ /.well-known/(webfinger|nodeinfo) {
        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;
        proxy_set_header Host $http_host;
        add_header X-Content-Type-Options $header_content_type_options;
        proxy_ssl_server_name on;
        proxy_pass https://ap.ghost.org;
    }

    location / {
        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;
        proxy_set_header Host $http_host;
        proxy_pass http://127.0.0.1:2369;

        add_header X-Content-Type-Options $header_content_type_options;
    }

    location ~ ^/.well-known/acme-challenge {
        allow all;
    }

    client_max_body_size 50m;
}
  • From an external host, telnet to Ghost port 80. Then issue:
GET / HTTP/1.1
Host: blog.site002.com
X-Forwarded-Proto: https
  • Response is 301:
HTTP/1.1 301 Moved Permanently
Server: nginx/1.24.0 (Ubuntu)
Date: SOME DATETIME
Content-Type: text/plain; charset=utf-8
Content-Length: 59
Connection: keep-alive
X-Powered-By: Express
Cache-Control: public, max-age=31536000
Location: https://blog.site002.com/
Vary: Accept, Accept-Encoding
X-Content-Type-Options: nosniff

Any suggestions what else I can do? Do you need other settings or conf details?

If you want your site to work over ssl, you should be connecting over 443, and nginx should be configured to use your SSL certificate. I don’t see it here which might be why it’s not working

1 Like

Hi @vikaspotluri123,

Remember the setup I have? As follows:
[Internet] — [Reverse Proxy] — [Ghost]

The Reverse Proxy proxies to Ghost over port 80. That is what I’m simulating. By connecting to Ghost over port 80, but with the correct proxy headers. If the solution is to put SSL certificate in Ghost, then it does not help me in my desired setup where Ghost does not need to perform encryption/decryption, as Reverse Proxy is already doing it.

I think you misunderstood.

Nginx still needs to listen to port 443, unless you have another proxy in front of nginx that does TLS termination?

The listen 80 means nginx is listening on port 80 on the outside. Secured https connections come in on port 443 though (again, assuming you don’t have another proxy in front of nginx).

Edit: re-reading the entire thread, I do think you might have a second reverse proxy there. For the full picture, it will be helpful to also have the config of that. The issue might not be in nginx > Ghost but in [reverse proxy] > nginx?

1 Like

Hi @jannis,

My original design is to have the following:

[Internet] — (443)[Reverse Proxy] — (80)[Ghost]

  • Reverse Proxy listens at 443, and serves the outside Internet
  • Reverse Proxy connects/proxies (unencrypted) to Ghost. Thus, Ghost listens at 80.
  • Ghost has an internal nginx, which performs this job to response at 80. It then redirects as appropriate to the relevant localhost web servers on other ports.

The problem was that with the above setup, all [img] returned by Ghost are tagged absolute URIs beginning with “http”. As described earlier, modern browsers will then block all these as the page is supposed to be over “https” and contains “http” assets.

A reader suggested using “ghost config url https:// FQDN” as a possible solution. That is what I tried. But it gave 301. I thought it may be some config issue. The last few posts are around troubleshooting for this 301 issue.

Ultimately, I am just looking for a solution for my original setup. So far, the solution I have temporarily adopted is to implement a post-processing hack to replace all “http” with “https”. That way, the modern browsers do not block the images which are, for some reason, served using absolute URIs beginning with http. I am still unsure why Ghost does not serve using relative URIs.

But is there a reason for the double proxy?

What’s the argument against connecting your reverse proxy (the one listening to 443, not nginx listening to port 80) directly to Ghost on port 2369 (as per your nginx config)?

I have read through the thread, but I truly don’t understand the reason for the double proxy. From what I see, that might be a very likely source of the issues. You’re not just going reverse proxy > Ghost, but reverse proxy > nginx > Ghost.