Optimise Ghost and nginx

This is a step by step guide on how to optimise Ghost and nginx. This guide is part of the Deploy Ghost series and is split into three parts - Deploying, Securing, and Optimising. This is part three and once we’re done, we will have nginx and our Ghost blog optimised to serve content quickly and reduce the server load in the process.

Before you start typing into your console, please take the time to research and understand what it is that you’re actually executing on your server. This process worked for me, and might change as the applications have new patches and versions rolled out.

Series Links


Update

04/01/2017: The nginx configuration for the location / {} block has been updated to include all of the headers. The original configuration was sending the root document without the correct security headers. The issue was caused by an added header preventing the location / {} block from inheriting the server {} headers.


Our Ghost blog is well and truly up and running. We could put the keyboard away now if we wanted to, but we really should look at fine tuning everything. This should help visitors get served faster, and reduce our server load.

You can get pretty crazy with optimising, so this post definitely doesn’t cover everything. You might consider trying to optimise your fonts, HTML, CSS, even your favicon. Users don’t tend to like waiting, speeding up your sites responsiveness even slightly could be the difference between a purchase or a negative review.

Utilise nginx

So it turns out that nginx is actually really good at serving static content and caching. When a request comes in, we can get nginx to respond instead of sending the request over to Ghost. This is great because we remove the burden on Ghost to generate everything each time a request comes in. This will free up server resources and also reduce the amount of time a client waits before getting a response.

Serve Static Content

Getting nginx to serve static content is as simple as telling it what to serve. To let nginx know what to serve, we want to edit our ghost.conf.

1
2
cd /etc/nginx/sites-available
nano ghost.conf

We’ll ask nginx to serve up our casper theme, and our images. If you ever change your theme, you will need to update nginx.

1
2
3
4
5
6
location ^~ /assets/ {
    root /var/www/ghost/content/themes/casper;
}
location ^~ /content/images/ {
    root /var/www/ghost;
}

Caching Content

Getting nginx to cache content is a little more in depth as we need to create the area to hold the cache, configure the area to hold the cache, and then we can tell nginx what we want to be cached. First we need to create an area to hold the cache, we’ll call it ghostcache.

1
2
cd /var/cache/nginx
sudo mkdir ghostcache

Then we want to edit nginx.conf to configure our cache.

1
2
cd /etc/nginx
sudo nano nginx.conf

We’ll want to add in the below.

1
2
proxy_cache_path /var/cache/nginx/ghostcache levels=1:2 keys_zone=ghostcache:60m max_size=500m inactive=24h;
proxy_cache_key "$scheme$request_method$host$request_uri";

Now that we’ve made our cache and configured it, we can tell nginx what to cache. We’ll do this in ghost.conf.

1
2
cd /etc/nginx/sites-available
nano ghost.conf

This is what our root location will look like with our new cache directives added in. There’s some extra stuff in there too such as specifying the cache validity, ignoring some headers that Ghost uses, configuring nginx to serve from cache if there’s an issue, and adding in a header that shows if the cache was used. We’ll need to add in our other headers again, as add_header X-Cache-Status $upstream_cache_status; will prevent the location / {} block from inheriting the server {} headers. This could be done more cleanly by adding the headers into their own configuration file and including them where they’re needed, but this is easier to show.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
location / {
    proxy_cache ghostcache;
    proxy_cache_valid 60m;
    proxy_cache_valid 404 1m;
    proxy_ignore_headers Set-Cookie Cache-Control;
    proxy_hide_header Set-Cookie;
    proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504;
    add_header X-Cache-Status $upstream_cache_status;
    add_header Strict-Transport-Security max-age=15768000; includeSubDomains;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Xss-Protection "1";
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' *.google-analytics.com";
    proxy_set_header Host $http_host;
    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 $scheme;
    proxy_pass http://127.0.0.1:2368;
}

We definitely don’t want to cache our Ghost administrator panel, so we will need to add in an exception for that. If the administrator panel is hit, just pass through to Ghost.

1
2
3
4
5
6
7
location ^~ /ghost/ {
        proxy_set_header Host $http_host;
        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 $scheme;
        proxy_pass http://127.0.0.1:2368;
}

Final Configuration

At last, this is what our complete ghost.conf file looks like.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
server {
    listen 80 default_server;
    server_name example.com www.example.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name example.com www.example.com;
    server_tokens off;
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_dhparam /etc/ssl/certs/dhparam.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 8.8.8.8 8.8.4.4;
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:50m;
    ssl_session_tickets off;
    ssl_protocols TLSv1.2;
    ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
    ssl_prefer_server_ciphers on;
    add_header Strict-Transport-Security max-age=15768000; includeSubDomains;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Xss-Protection "1";
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' *.google-analytics.com";

    location '/.well-known/acme-challenge' {
        root /var/www/ghost/;
    }

    location ^~ /assets/ {
        root /var/www/ghost/content/themes/casper;
    }

    location ^~ /content/images/ {
        root /var/www/ghost;
    }

    location ^~ /ghost/ {
        proxy_set_header Host $http_host;
        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 $scheme;
        proxy_pass http://127.0.0.1:2368;
    }

    location / {
        proxy_cache ghostcache;
        proxy_cache_valid 60m;
        proxy_cache_valid 404 1m;
        proxy_ignore_headers Set-Cookie Cache-Control;
        proxy_hide_header Set-Cookie;
        proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504;
        add_header X-Cache-Status $upstream_cache_status;
        add_header Strict-Transport-Security max-age=15768000; includeSubDomains;
        add_header X-Frame-Options "SAMEORIGIN" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header X-Xss-Protection "1";
        add_header Content-Security-Policy "default-src 'self'; script-src 'self' *.google-analytics.com";
        proxy_set_header Host $http_host;
        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 $scheme;
        proxy_pass http://127.0.0.1:2368;
    }
}

Ghost is Optimised

We’ve now got a functioning, secure, optimised blog. There’s always going to be more we can do to keep getting enhanced security and further optimisation, but this is a very solid start.

Series Links

References