How-to: Offloading Ghost image delivery to Bunny CDN

I found a simple solution for offloading images from my self-hosted Ghost and serve them over a CDN for… no money, really. It’s a little bit of a hack, rewriting the image URL’s with nginx, but it works like a dream. We will have three URL’s to keep track of when done…

If anyone has suggestions on how to make the set-up even better pls share. Using Ubuntu 22.04, nginx 1.18.0 & Bunny CDN. I am not affiliated with Bunny

Update 25/5: Ghost does not send referrer header when previewing a blog post, if you have Hotlinking protection enabled on Bunny CDN images will not display in Preview.

I also added how to restrict access to our Origin URL to Bunny in the end of this post.

Also published this as a (CDN-accelerated) blog post with additional info re caching

1. Create a CDN origin URL

That’s your Ghost blog without CDN acceleration. The CDN will not be able to fetch images from your regular blog URL when enabled - the image links are pointing back to the CDN and Bunny will be unable to pull and cache the images. Angry Bunny in a loop, not good.

2. Set up an account with Bunny CDN at and create a pull zone.

3. Configure nginx

  • Edit the ‘live’ configuration file for - not the origin -

  • I use nginx sub_filter to rewrite image-requests to our CDN. It’s basically search-and-replace
    sub_filter 'replace this' 'with this';

  • Add this in the nginx proxy_pass block - change to your domain and to the CNAME subdomain

    # Enable multiple replacements in one request
    sub_filter_once off;
    # sub_filter does not work with gzip compression, so disable. 
    proxy_set_header Accept-Encoding "";
    # Replace all image links to /content/images with CDN-url
    sub_filter '' '';
    # The same for poster images loaded as background
    sub_filter 'background-image:url(/content' 'background-image:url(';
    # Optional: javascript libraries
    sub_filter '/assets/js/' '';

Done. Restart nginx. Verify in Developer Console / Network with a shift-reload in browser.

Restricting access to origin

We only want Bunny to be able to access pull assets from, so we send a header X-Pull in our requests, and have nginx check for this header.

1. Create new Edge rule

Create a new Edge rule in BunnyCDN control panel

  • Action: Set Request Header
  • Header: Name: X-Pull
  • Header Value: random12345 - use any passphrase here
  • Conditions: Request URL matching Any for*

2. nginx config for origin URL/host

Add this to the proxy_pass block in nginx. If the header X-Pull in the request does not contain the key we assigned in the Edge rule we return 403 Forbidden status code.

     if ($http_x_pull != "random12345") {
         return 403;

This solution was suggested by KeyCDN and they have a HTTP Header Checker to verify that requests with X-Pull are accepted.

PageSpeed with Bunny CDN enabled is 97, pretty good…



Thanks for sharing! I tried something before but I get stucked on Nginx step.

Could be good to exclude /ghost?

I was confident this hack would wreck the editor and admin interface behind /ghost - surprisingly it was not affected at all. Turns out all requests are made to the content-API and never triggers /content/images etc sub_filters.

Updating Ghost on origin URL is an option, to make sure we’re looking at the non-CDN un-cached origin. Edit: This is not a good idea - better to lock down this URL so that only the CDN can access and pull assets, a subdomain can be sniffed and gets indexed. Original post updated.

1 Like

Thanks, it’s working!

But it seems that original link persists when using lightbox and clicking on blog post images, have you figured it out? :slight_smile:

Lightbox… hmmm… are you thinking Gallery cards or …
Can you send a link to a page?

That’s Lightbox (!)

So, it seems that images on galleries are not changed by the Nginx filter.

Can you please, try to reproduce? I got it working with other images.

I’ve checked a lightbox/gallery card and it uses the full URL for images…

so… this filter should trigger…

sub_filter '' '';

Check the html source for the lightbox/gallery image links… what does it say?

1 Like

Hey! That seems to do the trick (because I’m using lazy and automatic resize using CSS selectors):

sub_filter '/content/images/size/' 'https://cdn.mydo.main/content/images/size/';

Thanks again :love_you_gesture:

1 Like

I already do this with Cloudflare, it’s free and I just need to change the nameserver (5 minutes work). It also does hotlinking prevention, bots and tons of other more features. Website is fast AF. What are the Bunny advantages over Cloudflare?

“If it’s free you’re the product” - here’s section 10 of Cloudflare’s user agreement, they can terminate you for no reason.

Additionally, the purpose of Cloudflare’s Service is to proxy web content, not store data. Using an account primarily as an online storage space, including the storage or caching of a disproportionate percentage of pictures, movies, audio files, or other non-HTML content, is prohibited. You further agree that if, at Cloudflare’s sole discretion, you are deemed to have violated this section, or if Cloudflare, in its sole discretion, deems it necessary due to excessive burden or potential adverse impact on Cloudflare’s systems, potential adverse impact on other users, server processing power, server memory, abuse controls, or other reasons, Cloudflare may suspend or terminate your account without notice to or liability to you.

1 Like

You didn’t answer my question. Also the user agreement part is taken out of context, it’s about using Cloudflare as a cloud data storage, and not for delivering content over a website.

Sorry, I thought I did, thing is if you’re not paying you’re not a customer, they don’t have any obligations to deliver a service. Facebook, Snapchat, Instagram or any ‘free’ service.

Cloudflare uses the “free tier” to beta test new features before they make them available to paying customers with an SLA.

I don’t know what’s important for you but if you want a uptime guarantee that’s the Cloudflare Business Plan which is $200/month.

Bunny offers 4-nine SLA (99,99% uptime) included in minimum monthly fee of $1 + traffic costs.
With Bunny you’re a customer, and for me that’s important. Their support is 5-star and superfast.

They also transcode video and host it with DRM protection, provide DNS servers, file storage with access tokens… all in a friendly user interface.

Actually this is not true. The free tier has everything the paid tiers already have, there are no “beta services”. The paid tiers simply have more services than the free plan.

Cloudflare is putting out of business many companies because they sell at real cost of the service.

For example, a domain on Namecheap costs 50% more than on Cloudflare.

The article you posted is from your website, which I guess is with Bunny. The pagespeed score (run twice):

With Cloudflare (free) on my website (origin server is with the cheapest Digital Ocean droplet) I get 99 score on Performance and 100 on all the other metrics…

Here is what a Cloudflare engineer writes on Stack Exchange, that was my source…
How can CloudFlare offer a free CDN with unlimited bandwidth? - Webmasters Stack Exchange

Also, for the speedtest, that’s for the Mobile test, check the Desktop tab.

The mobile score isn’t really great since I load a carousel, and it wants all images loaded, however they are off-screen so doesn’t really matter. The Time To Interactive on mobile is 0.57 seconds, which I calculate dynamically in the third paragraph in the blog post …
Ghost speed with Bunny CDN (

1 Like

So, everyone can chose what - companies - support.

1 Like

Conspiracy theories about their DNS service which is a complete different business than the CDN.

Exactly, offer. Doesn’t mean new features are going to be activated by default. This happened with Zaraz (like Google Tag Manager but much better, server side). It was for free until september, until it became “freemium” with a certain limit.