Fixing Ghost analytics when running behind nginx instead of Caddy
Ghost's Tinybird analytics silently break behind nginx because the full `/.ghost/analytics/` prefix gets forwarded instead of stripped. This post explains why the usual `proxy_pass` trailing slash trick fails with Docker DNS and shows the explicit rewrite fix.
I spent a couple of hours debugging Ghost's Tinybird analytics and wanted to share what I found. If you run Ghost behind nginx instead of the bundled Caddy reverse proxy, there is a path routing issue that will quietly break your analytics.
The symptom
Everything looks fine on the surface. The traffic-analytics container starts, the Ghost frontend injects the tracking script, and page hit requests fire on every visit. But in Ghost Admin, the Analytics section shows zero visitors. Nothing gets through to Tinybird.
What is actually happening
Ghost's tracking script sends POST requests to /.ghost/analytics/api/v1/page_hit. The bundled Caddy config in ghost-docker knows how to route these to the traffic-analytics service and strips the /.ghost/analytics prefix along the way. The service itself only handles routes under /api/v1/.
If you are using nginx, you have probably written something like this:
location /.ghost/analytics/ {
proxy_pass http://traffic-analytics:3000;
}This forwards requests to the right container, but it passes the full path through. The service receives /.ghost/analytics/api/v1/page_hit, does not recognize it, and returns a 404. The browser gets a 404 back, the event never reaches Tinybird, and your analytics dashboard stays empty.
Your first instinct might be to add a trailing slash to proxy_pass to trigger nginx's built-in URI stripping:
location /.ghost/analytics/ {
proxy_pass http://traffic-analytics:3000/;
}This works with literal hostnames, but if you are using nginx variables for DNS resolution (which you probably are, since Docker's internal DNS does not play well with nginx's static resolver), it will not work. Nginx skips URI rewriting when the upstream is a variable.
The fix
Use an explicit rewrite rule:
resolver 127.0.0.11 valid=30s;
set $analytics traffic-analytics:3000;
location /.ghost/analytics/ {
rewrite ^/.ghost/analytics/(.*)$ /$1 break;
proxy_pass http://$analytics;
proxy_set_header Host $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_buffering off;
}The rewrite directive captures everything after /.ghost/analytics/ and forwards just the remainder. So /.ghost/analytics/api/v1/page_hit?name=analytics_events becomes /api/v1/page_hit?name=analytics_events. The break flag tells nginx to stop processing rewrite rules and use the modified URI for the proxy pass.
How to tell if you are affected
Check the traffic-analytics logs:
docker compose logs traffic-analyticsIf you see requests to /.ghost/analytics/api/v1/page_hit coming back with status: 404, this is your problem. After applying the rewrite, you should see requests to /api/v1/page_hit returning status: 202.