Skip to content

Instantly share code, notes, and snippets.

@pnommensen
Last active September 14, 2024 01:23
Show Gist options
  • Save pnommensen/707b5519766ba45366dd to your computer and use it in GitHub Desktop.
Save pnommensen/707b5519766ba45366dd to your computer and use it in GitHub Desktop.
Ghost CMS with NGINX for Maximum Performance

Full blog post can be found here: http://pnommensen.com/2014/09/07/high-performance-ghost-configuration-with-nginx/

Ghost is an open source platform for blogging founded by John O'Nolan and Hannah Wolfe. It's a node.js application and therefore works great in conjunction with nginx. This guide will will help you create a high performance nginx virtual host configuration for Ghost.

"Don't use #nodejs for static content" - @trevnorris. If #nginx isn't sitting in front of your node server, you're probably doing it wrong.

— Bryan Hughes (@nebrius) August 30, 2014
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

The node.js application runs on a port on your server. We can configure nginx to proxy to this port and also cache so that we don't need to rely on express, the default node web application framework.

To start we need to tell nginx what port Ghost is running on. Define an upstream in your domains virtual host configuration file.

upstream ghost_upstream {
    server 127.0.0.1:2368;
    keepalive 64;
}

This tells nginx that Ghost is running on 127.0.0.1:2358 and sets the connection to last 64 secconds to avoid having to reconnect for every request.

Proxy Cache

We want to cache responses from Ghost so we can avoid having to proxy to the application for every request. The first step to do this is set a proxy_cache_path. In your configuration define the cache. The configuration below creates a zone that is 75 megabytes, and removes files after 24 hours if they haven't been requested.

proxy_cache_path /var/run/cache levels=1:2 keys_zone=STATIC:75m inactive=24h max_size=512m;

Server Block

Now we can start the configuration for the domain that will be serving your Ghost blog. Note, if your using SSL/TLS for your blog you will want to use the configuration towards the end of this guide.

####1) Location block for blog page requests:

This configuration will cache valid 200 responses for 30 minutes and 404 responses for 1 minute from the previously defined upstream into the STATIC proxy_cache. We also want to ignore and or hide several headers that Ghost creates since we will be using our own. In addition to the nginx cache we will also be caching the pages in the browser for 10 minutes, expires 10m;.

location / {
        proxy_cache STATIC;
        proxy_cache_valid 200 30m;
        proxy_cache_valid 404 1m;
        proxy_pass http://ghost_upstream;
        proxy_ignore_headers X-Accel-Expires Expires Cache-Control;
        proxy_ignore_headers Set-Cookie;
        proxy_hide_header Set-Cookie;
        proxy_hide_header X-powered-by;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        expires 10m;
    }

It's also helpful to add a header to your page requests that tells if the request hit the nginx cache. This can be done easily with add_header X-Cache $upstream_cache_status;.

####2) Location block(s) for static file requests like css, js and images:

We are going to tell nginx where to find static files like css, js and images since the node.js powered Ghost application is on the same server as nginx. To do thise we need four location blocks that point to the location of images folder, assets folder, public folder, and scripts folder. We will cache these static fiiles with expires max; so they remain cached forever in the users browser. This is safe to do since ghost appends a version query string that updates when node.js is reloaded/restarted.

Note: When changing your Ghost theme you will need to change the alias path in the location /assets nginx block.

location /content/images {
        alias /path/to/ghost/content/images;
        access_log off;
        expires max;
    }
    location /assets {
        alias /path/to/ghost/content/themes/(theme-name)/assets;
        access_log off;
        expires max;
    }
    location /public {
        alias /path/to/ghost/core/built/public;
        access_log off;
        expires max;
    }
    location /ghost/scripts {
        alias /path/to/ghost/core/built/scripts;
        access_log off;
        expires max;
    }

#####3) nginx Location Block for Ghost Admin Interface

The administrative interface should definitely not be cached. The location block below applies to the backend and signout page. It defines the establed ghost_upstream backend and sets cache headers to ensure nothing is cached. Most importantly, note that we are not defining any proxy_cache settings.

location ~ ^/(?:ghost|signout) { 
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $http_host;
        proxy_pass http://ghost_upstream;
        add_header Cache-Control "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0";
    }

Full HTTP NGINX Configuration for Ghost.

If you've followed along you will now end up with a working nginx configuration that looks like this:

server {
   server_name domain.com;
   add_header X-Cache $upstream_cache_status;
   location / {
        proxy_cache STATIC;
        proxy_cache_valid 200 30m;
        proxy_cache_valid 404 1m;
        proxy_pass http://ghost_upstream;
        proxy_ignore_headers X-Accel-Expires Expires Cache-Control;
        proxy_ignore_headers Set-Cookie;
        proxy_hide_header Set-Cookie;
        proxy_hide_header X-powered-by;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        expires 10m;
    }
    location /content/images {
        alias /path/to/ghost/content/images;
        access_log off;
        expires max;
    }
    location /assets {
        alias /path/to/ghost/content/themes/uno-master/assets;
        access_log off;
        expires max;
    }
    location /public {
        alias /path/to/ghost/core/built/public;
        access_log off;
        expires max;
    }
    location /ghost/scripts {
        alias /path/to/ghost/core/built/scripts;
        access_log off;
        expires max;
    }
    location ~ ^/(?:ghost|signout) { 
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $http_host;
        proxy_pass http://ghost_upstream;
        add_header Cache-Control "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0";
    }
    
}

SSL/TLS Configuration for Ghost Blog

You may want to server your blog over HTTPS with SSL/TLS. First thing you should do is update the URL in the Ghost config.js file.

The nginx setup for usin SSL/TLS for Ghost requires several additional configurations. A full sample configuration is below. I will highlight the important differences.

The first several lines of the nginx configuration below establish and optimize HTTPS connections. You can use SPDY and additional settings like spdy_headers_comp, keepalive_timeout , ssl_session_cache, and OCSP stapling. Here I'm going to assume you know what those are since the purpose of this guide is to talk about Ghost.

In the location / block it's very important that you include proxy_set_header X-Forwarded-Proto https; or else when you go to load your Ghost blog you will receive a redirect loop. You'll need the same thing in the location ~ ^/(?:ghost|signout) { block.

server {
   server_name domain.com;
   listen 443 ssl spdy;
   spdy_headers_comp 6;
   spdy_keepalive_timeout 300;
   keepalive_timeout 300;
   ssl_certificate_key /etc/nginx/ssl/domain.key;
   ssl_certificate /etc/nginx/ssl/domain.crt;
   ssl_session_cache shared:SSL:10m;  
   ssl_session_timeout 24h;           
   ssl_buffer_size 1400;              
   ssl_stapling on;
   ssl_stapling_verify on;
   ssl_trusted_certificate /etc/nginx/ssl/trust.crt;
   resolver 8.8.8.8 8.8.4.4 valid=300s;
   add_header Strict-Transport-Security 'max-age=31536000; includeSubDomains';
   add_header X-Cache $upstream_cache_status;
   location / {
        proxy_cache STATIC;
        proxy_cache_valid 200 30m;
        proxy_cache_valid 404 1m;
        proxy_pass http://ghost_upstream;
        proxy_ignore_headers X-Accel-Expires Expires Cache-Control;
        proxy_ignore_headers Set-Cookie;
        proxy_hide_header Set-Cookie;
        proxy_hide_header X-powered-by;
        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_set_header Host $http_host;
        expires 10m;
    }
    location /content/images {
        alias /path/to/ghost/content/images;
        access_log off;
        expires max;
    }
    location /assets {
        alias /path/to/ghost/themes/uno-master/assets;
        access_log off;
        expires max;
    }
    location /public {
        alias /path/to/ghost/built/public;
        access_log off;
        expires max;
    }
    location /ghost/scripts {
        alias /path/to/ghost/core/built/scripts;
        access_log off;
        expires max;
    }
    location ~ ^/(?:ghost|signout) { 
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $http_host;
        proxy_pass http://ghost_upstream;
        add_header Cache-Control "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0";
        proxy_set_header X-Forwarded-Proto https;
    }
}

Questions or comments? Post below!

@martinseener
Copy link

Nice post! I’ve also tuned my ghost blog using nginx almost identically like you did but i dont use the proxy (yet!). But also without the proxy cache i get full page load times (pingdom tools) of about 400-500ms!

I’ve one addition to your setup to, again, increase the performance.

You should use a unix domain socket instead of an IP connection for the nginx upstream. i use this:

upstream ghost_upstream {
server unix:/tmp/ghost.sock;
}

and in the config.js of ghost:

socket: ‘/tmp/ghost-sysorchestra.sock’

instead of the Host/Port thing in server:….with this you can get rid of the TCP/IP overhead and further increase the performance!

@astralis
Copy link

If Ghost needs a plugin to perform effectively, then I think the problem is that Ghost is not ready for prime time.

@bharley
Copy link

bharley commented May 1, 2015

@astralis What plugin are you talking about?

@gferrin
Copy link

gferrin commented Oct 18, 2015

Your Awesome! Thanks for this. I've been having a redirect loop once I put ghost behind a CDN and really needed this line: proxy_set_header X-Forwarded-Proto https;

@HaeckDesign
Copy link

Trying to juggle Nginx, EC2, S3, Cloudfront, SSL, etc. was racking my brain. This config was crazy helpful - Thank you!!

@pascalandy
Copy link

You rock !

@Chouhada
Copy link

Using Gzip offers a nice improvement. In my server block, I use:

gzip on;
        gzip_http_version 1.1;
        gzip_min_length  256;
        gzip_comp_level 5;
        gzip_proxied any;
        gzip_buffers  4 32k;
        gzip_vary on;
        gzip_types
        application/atom+xml
        application/javascript
        application/json
        application/rss+xml
        application/vnd.ms-fontobject
        application/x-font-ttf
        application/x-web-app-manifest+json
        application/xhtml+xml
        application/xml
        font/opentype

See here for useful comments: https://mattstauffer.co/blog/enabling-gzip-on-nginx-servers-including-laravel-forge

@manhhailua
Copy link

I'm using Ghost 0.8.0 and I cound not find any folder like: /built/public and /core/built/scripts in Ghost directory. So I removed these two blocks:

location /public {
    alias /path/to/ghost/built/public;
    access_log off;
    expires max;
}
location /ghost/scripts {
    alias /path/to/ghost/core/built/scripts;
    access_log off;
    expires max;
}

My site seems to work fine... What is your opinion?

@akatasonov
Copy link

My experience it the same - there are no /built/public and /core/built/scripts in Ghost directory

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment