<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>selfhosting &#8211; Treibsand</title>
	<atom:link href="https://blog.quicksands.de/tag/selfhosting/feed/" rel="self" type="application/rss+xml" />
	<link>https://blog.quicksands.de</link>
	<description>Informatik et al.</description>
	<lastBuildDate>Mon, 28 Apr 2025 18:23:19 +0000</lastBuildDate>
	<language>de</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.8.3</generator>

<image>
	<url>https://blog.quicksands.de/wp-content/uploads/2025/04/ross-bucher-QBQQCO8G8ho-unsplash-150x150.jpg</url>
	<title>selfhosting &#8211; Treibsand</title>
	<link>https://blog.quicksands.de</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Caching for Ghost, NGINX edition</title>
		<link>https://blog.quicksands.de/2025/04/28/caching-for-ghost-nginx-edition/</link>
		
		<dc:creator><![CDATA[csett86]]></dc:creator>
		<pubDate>Mon, 28 Apr 2025 18:23:19 +0000</pubDate>
				<category><![CDATA[Allgemein]]></category>
		<category><![CDATA[nginx]]></category>
		<category><![CDATA[selfhosting]]></category>
		<guid isPermaLink="false">https://blog.quicksands.de/?p=262</guid>

					<description><![CDATA[The (only) recommended stack to self-host Ghost via ghost-cli already has an nginx in it (for SSL termination), but its not using its caching feature. So to survive the &#8222;Mastodon hug of death&#8220;, as Elena put it, lets add a small cache to it. By default Ghost adds headers that effectively forbid caching, because the [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>The (only) <a href="https://ghost.org/docs/install/ubuntu/">recommended stack</a> to self-host Ghost via <code>ghost-cli</code> already has an nginx in it (for SSL termination), but its not using its caching feature. So to survive the &#8222;Mastodon hug of death&#8220;, as <a href="https://aseachange.com/@elena/statuses/01JQ435964HA7MV93ED21J1EWD">Elena put it</a>, lets add a small cache to it.</p>



<p>By default Ghost adds headers that effectively forbid caching, because the default cache-control header has a lifetime (<code>max-age</code>) set of 0. So lets change that first:</p>



<p>Add a caching config to the ghost <code>config.production.json</code> file (see <a href="https://ghost.org/docs/config/#caching">https://ghost.org/docs/config/#caching</a> for full details):</p>



<pre class="wp-block-code"><code>"caching": {
  "frontend": {
    "maxAge": 60
  },
  "contentAPI": {
    "maxAge": 60
  }
}</code></pre>



<p>Then go into the nginx config file for the site (the one that ghost-cli created in <code>/etc/nginx/sites-available/</code>) and add the bare minimum, because by nginx has quite <a href="https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_cache">good default values</a> for many cache options.</p>



<p>1. Give nginx a place to store the cached files, eg in <code>/tmp</code></p>



<pre class="wp-block-code"><code>proxy_cache_path /tmp/nginx_ghost levels=1:2 keys_zone=ghostcache:10m max_size=100m inactive=24h;</code></pre>



<p>2. Within the existing location block that does the reverse proxying, lets do these four things:</p>



<ul class="wp-block-list">
<li>enable the caching (<code>proxy_cache ghostcache;</code>)</li>



<li>protect the poor ghost instance that only one request for each cache key hit it (<code>proxy_cache_lock on;</code>)</li>



<li>allow it to update the cache during serving &#8222;outdated entries&#8220; (<code>proxy_cache_background_update on;</code>)</li>



<li>continue to serve cached content even if ghost should fall over (<code>proxy_cache_use_stale ...</code>)</li>
</ul>



<pre class="wp-block-code"><code>proxy_cache ghostcache;
proxy_cache_lock on;
proxy_cache_background_update on;
proxy_cache_use_stale updating error timeout http_500 http_502 http_429;</code></pre>



<p>3. Lastly, to see it in action, lets add a header that puts the cache status into it so that we can see it from the browser dev tools:</p>



<pre class="wp-block-code"><code>add_header X-Cache-Status $upstream_cache_status;</code></pre>



<p>And thats already it, restart ghost (to activate the config), reload nginx:</p>



<pre class="wp-block-code"><code>sudo systemctl restart ghost...
sudo systemctl reload nginx</code></pre>



<p>Just for completeness, my full nginx config then looks like this:</p>



<pre class="wp-block-code"><code>proxy_cache_path /tmp/nginx_ghost levels=1:2 keys_zone=ghostcache:10m max_size=100m inactive=24h;

map $status $header_content_type_options {
    204 "";
    default "nosniff";
}

server {
    listen 443 ssl http2;
    listen &#91;::]:443 ssl http2;

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

    ssl_certificate /etc/letsencrypt/blog.settgast.org/fullchain.cer;
    ssl_certificate_key /etc/letsencrypt/blog.settgast.org/blog.settgast.org.key;
    include /etc/nginx/snippets/ssl-params.conf;

    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;

	add_header X-Content-Type-Options $header_content_type_options;

	proxy_cache ghostcache;
	proxy_cache_lock on;
	proxy_cache_background_update on;
	proxy_cache_use_stale updating error timeout http_500 http_502 http_429;
	add_header X-Cache-Status $upstream_cache_status;
    }

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

    client_max_body_size 1g;
}</code></pre>



<h2 class="wp-block-heading">Benchmarking</h2>



<p>To benchmark it a bit, lets use the <a href="https://github.com/wg/wrk">wrk</a> tool and hit ghost with 500 parallel connections, once default without caching, once with caching enabled:</p>



<p>As a baseline I ran it without the caching, so in default mode:</p>



<pre class="wp-block-code"><code>wrk -c 500 -t 4 https://blog.settgast.org/caching-for-ghost-nginx-edition/
Running 10s test @ https://blog.settgast.org/caching-for-ghost-nginx-edition/
  4 threads and 500 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     1.13s   507.40ms   1.99s    58.33%
    Req/Sec    22.18     15.36    60.00     61.22%
  371 requests in 10.08s, 5.53MB read
  Socket errors: connect 0, read 0, write 0, timeout 311
Requests/sec:     36.80
Transfer/sec:    561.85KB</code></pre>



<p>36 requests per second, thats not much. Or to put it differently: From the 500 connections started, only 371 completed within 10 seconds, the others were still loading.</p>



<p>Now lets repeat this with NGINX caching enabled:</p>



<pre class="wp-block-code"><code>wrk -c 500 -t 4 https://blog.settgast.org/caching-for-ghost-nginx-edition/
Running 10s test @ https://blog.settgast.org/caching-for-ghost-nginx-edition/
  4 threads and 500 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    41.36ms   70.71ms   1.05s    93.24%
    Req/Sec     3.84k   804.38     5.36k    83.79%
  139524 requests in 10.06s, 1.95GB read
Requests/sec:  13865.81
Transfer/sec:    198.63MB</code></pre>



<p>Over <strong><strong>13 thousand requests per second</strong></strong> from a small VPS (2CPUs, 4GB RAM), or a factor of 380 times more requests!</p>



<h2 class="wp-block-heading"><strong><strong>References</strong></strong></h2>



<ul class="wp-block-list">
<li>Elenas great series on self-hosting Ghost and adding caching: <a href="https://aseachange.com/@elena">https://aseachange.com/@elena</a></li>



<li>This blog post on how nginx caching works: <a href="https://www.sheshbabu.com/posts/nginx-caching-proxy/">https://www.sheshbabu.com/posts/nginx-caching-proxy/</a></li>



<li>The official docs for nginx helped a lot: <a href="https://docs.nginx.com/nginx/admin-guide/content-cache/content-caching/">https://docs.nginx.com/nginx/admin-guide/content-cache/content-caching/</a></li>



<li>Scotts article was also an inspiration: <a href="https://scotthelme.co.uk/caching-ghost-with-nginx/">https://scotthelme.co.uk/caching-ghost-with-nginx/</a></li>
</ul>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
