Ghost Behind a Reverse Proxy: Loops, Images, Logins
Ghost expects a proxy in front of it. Two settings decide whether it works: the site URL and the forwarded-protocol header. What breaks when either is wrong.
Quick answer: Ghost is built to sit behind a reverse proxy. It listens on a local port over plain http and expects something in front of it to handle the certificate. Two things then decide whether it works. Ghost’s site URL must be the public https address, and the proxy must tell Ghost the original request was secure through the forwarded-protocol header. Get the first wrong and images break or the admin refuses to log in. Get the second wrong and every request becomes a redirect to itself.
You put a proxy in front of Ghost. Perhaps a control panel did it for you, perhaps it is a NAS, a Docker setup with Traefik or Nginx Proxy Manager, or Cloudflare in front of everything. The site half works. It redirects forever, or the pages load without images, or the admin greets you with an error about origins. All three have the same two causes, and this guide explains them in the order you can fix them.
We run our own sites this way, Ghost in containers behind a proxy that terminates the certificate. We tested how Ghost answers the same request with and without the header on one of them. Ghost’s own proxy documentation covers the header; this guide adds the rest.
How Ghost sees a request through a proxy
Ghost does not speak https itself. It listens on a local port, by default 2368 on the machine’s own address, and answers plain http. The proxy in front of it holds the certificate, takes the visitor’s https request, and passes it to Ghost as http. From Ghost’s point of view every request now looks insecure and arrives from the proxy rather than from the visitor.
To behave correctly, Ghost needs two facts it cannot see for itself:
- Its own public address. Ghost builds every link, image address, canonical tag and admin redirect from the site URL in its configuration. On a Ghost-CLI install it is the
urlvalue in the config file; on Ghost’s Docker Compose setup it comes from the domain in the.envfile; on other Docker images it is usually an environment variable calledurl. - Whether the visitor’s request was secure. The proxy passes this along in a header named
X-Forwarded-Proto, with the valuehttps. Ghost trusts that header from the proxy and treats the request as secure.
Everything below is one of those two facts being wrong.
The redirect loop: the header is missing
When Ghost’s site URL starts with https but a request arrives looking like http, Ghost does the sensible thing and redirects the visitor to the https address. If the proxy did not forward the protocol, every request looks like http, so every request gets that redirect. The proxy passes it to the browser, the browser follows it, the proxy strips the protocol again, and Ghost redirects again. The browser gives up with “too many redirects”.
We sent the same request to Ghost directly on one of our own sites, changing only that header:
no forwarded header -> 301 https://example.com/X-Forwarded-Proto: http -> 301 https://example.com/X-Forwarded-Proto: https -> 200 (the page)That is the entire mechanism. Ghost’s documentation says the same in one sentence. Set the header incorrectly and Ghost “will think the requests are insecure, attempt to redirect to the https version of a URL and cause an infinite redirect loop”.
The fix is to make the proxy forward the header, and each proxy has its own way:
| Proxy | What to do |
|---|---|
| nginx | Add the four header lines Ghost-CLI writes itself, shown below. |
| Caddy | When Caddy terminates the certificate it forwards the header on its own. When another proxy sits in front of Caddy, Caddy must be told not to redirect: turn its automatic https off, listen on port 80, and pass X-Forwarded-Proto https upstream. |
| Traefik | Forwards the header when it terminates the certificate. Loops usually come from a second proxy behind it, such as the Caddy inside Ghost’s Docker setup, still trying to redirect. |
| Nginx Proxy Manager | Forwards the header by default. If a loop appears, check that the scheme in the proxy host entry is http and that Ghost’s URL is https. |
| Cloudflare | Use the Full (strict) encryption mode. Flexible sends plain http to your server, and Ghost redirects it back to https forever. |
For the technical reader, this is the block Ghost-CLI writes for nginx, and it is what every other proxy has to imitate:
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:2368;}Missing images and wrong links: the site URL is wrong
The second symptom looks different. The page loads, the text is there, and the images are blank, or links point at http:// or at localhost. Nothing is wrong with the proxy. Ghost is building addresses from a site URL that does not match how the site is reached.
The common versions are two. A URL that starts with http while the site is served over https, and a URL still set to the default local address because the installer or the Docker image never asked. In the first case the browser is on an https page and refuses to load http images, which it calls mixed content. In the second, every link on the page points at a machine the visitor cannot reach.
The fix is the setting itself. Ghost’s configuration documentation puts it plainly: if you use SSL, always enter the URL with https://. On a Ghost-CLI install:
ghost config url https://example.comghost restartOn Ghost’s Docker Compose setup, change the domain in .env and recreate the containers. On another Docker image, change the url environment variable and restart the container. There is no need to re-upload images: Ghost stores uploaded image addresses relative to the site URL, so correcting the setting corrects every image at once.
The admin will not log in: the origin does not match
The third symptom is the most alarming, because the site itself works. You open the admin, enter your details, and get “Request made from incorrect origin”, followed by two addresses: the one Ghost expected and the one the browser sent. Ghost checks that admin requests come from the address it is configured with, and refuses when they do not. The message tells you exactly which setting is wrong, because the expected address is Ghost’s site URL, or its separate admin URL if one is set.
The cause is the same site URL as above, seen from the admin side. A default of http://localhost:2368 is the classic case: the site is reached at your real domain, Ghost expects localhost, and every admin request is rejected. Fix the URL as in the previous section and the admin logs in.
Two related messages point the same way. “Access denied from url” means the site URL in the configuration does not match the address being used, and Ghost’s documentation says so directly. And if you run the admin on its own hostname, such as admin.example.com, Ghost needs that told separately. The admin URL option in the config, or the admin domain in the Docker setup, does that; without it the origin check fails for the admin alone.
Uploads that fail part way
One more thing a proxy adds, unrelated to the two settings. Proxies limit the size of a request body, and the default is often small enough to reject a theme zip or a large image. Ghost-CLI’s own nginx configuration raises the limit to 50 MB with a single line, and Ghost’s proxy documentation reminds you to do the same on any other proxy. If uploads of small images work and larger ones fail with a generic error, this is the first place to look.
For the technical reader: on nginx the line is client_max_body_size 50m; inside the server block. Other proxies have an equivalent setting under a similar name.
Using your own proxy with Ghost’s Docker Compose setup
Ghost’s Docker Compose setup ships with its own Caddy, which obtains certificates and forwards the right headers. If a proxy already sits in front of the server, running two of them is where loops come from. The arrangement that self-hosters have settled on has three parts. Remove the Caddy service from the compose file, publish the Ghost container’s port to the proxy, and have the proxy forward the standard headers to it.
Two things carry over from Caddy’s job. The site URL still has to be the public https address, which the .env domain provides. And if you use the social web features, the three ActivityPub paths that Caddy routed to the ActivityPub service now need routing at your proxy instead. Our ActivityPub guide lists them.
When something still doesn’t work
| What you see | The setting to check |
|---|---|
| Endless redirect, “too many redirects” | The proxy is not forwarding X-Forwarded-Proto: https, or Cloudflare is in Flexible mode |
| Page loads, images blank, browser console mentions mixed content | Site URL starts with http |
| Links go to localhost or a wrong hostname | Site URL is still the default |
| ”Request made from incorrect origin” in admin | Site URL, or admin URL, does not match the address in the browser |
| ”Access denied from url” | Site URL does not match the address in use |
| Theme or image uploads fail above a certain size | Proxy body size limit |
| Site works on http but https loops, only with Cloudflare | Encryption mode is Flexible; use Full (strict) |
A useful habit: after any change to the URL or the proxy, request the home page and the admin page from outside the server and look at the raw responses. One redirect from http to https is correct. A redirect from https to https is the loop. Images pointing at a different scheme or host than the page are the URL.
Where this leaves you
Ghost behind a proxy comes down to a URL that says https and a header that says https. With both in place the site, the images and the admin agree on one address, and everything else in Ghost works as it would without the proxy. Our Ghost themes are developed and demoed on sites that run exactly this way. If you are still choosing how to run Ghost, the self-hosting guide compares the setups. If the domain or certificate is the part still open, the custom domain guide picks up from here.
Frequently Asked Questions
Why does my Ghost site redirect forever behind a proxy?
Why are images not loading on my self-hosted Ghost site?
What does 'Request made from incorrect origin' mean in Ghost?
Do I need Ghost's Caddy if I already run a reverse proxy?
Keep reading
How to Install Ghost: Each Method Step by Step
Install Ghost on Ghost(Pro), with Ghost's Docker Compose setup, with Ghost-CLI on Ubuntu, or on your own computer: what each needs and the steps to follow.
Ghost Self-Hosting Guide: Server, Install, Upkeep
What self-hosting Ghost involves: the server it needs, Ghost's Docker Compose setup versus Ghost-CLI, email, backups, safe updates and moving to a new server.
Ghost Custom Domain Setup: DNS, SSL and Email
How to point a domain at Ghost(Pro) or a self-hosted site: the DNS records, automatic SSL, the www redirect Ghost does not do for you, and a sending domain.
Ghost Newsletters Without Mailgun: Real Options
Self-hosted Ghost only sends newsletters through Mailgun. What SMTP can and can't do, how Amazon SES and Postmark fit, what the proxies cost, and what to pick.
Comparing options? Browse all premium Ghost themes side by side.
Explore all premium Ghost themes
The complete Luxe Themes bundle: every theme, one purchase.