Self hosted Ghost 6 on Raspberry Pi 5 via Cloudflare Tunnel: ActivityPub routes (.well-known, /.ghost/activitypub) failing with 403


Hi everyone,

I’m looking for help troubleshooting ActivityPub on a self hosted Ghost site.

Site + environment

  • Domain: beitmenotyou.online

  • Hardware: Raspberry Pi 5 (64 bit, arm64)

  • OS: Raspberry Pi OS Lite (64 bit)

  • Deploy: Docker Compose

  • Edge: Cloudflare Tunnel (cloudflared running in a container)

  • Ghost: previously Ghost 5, now updated to Ghost 6.14.0

  • We have had issues updating to Ghost 6.15.0 (happy to share logs if needed)

What works

  • The main site loads fine through Cloudflare Tunnel (homepage and normal pages render)

  • Containers are healthy and the tunnel connects

The problem
ActivityPub (Network feature) is not functioning properly. Discovery endpoints and ActivityPub endpoints are failing, and we are seeing errors like SITE_MISSING and inconsistent HTTP responses (502, 503, 530, 403) while trying different proxy approaches.


Current architecture (Docker Compose)

We’re running:

  • ghost (ghost:6-alpine)

  • ghost-db (mysql:8.0)

  • cloudflared-ghost (cloudflare/cloudflared:latest)

  • ghost-proxy (nginx:alpine) to handle ActivityPub routing

docker compose ps shows:

NAME                IMAGE                           STATUS
cloudflared-ghost   cloudflare/cloudflared:latest   Up
ghost               ghost:6-alpine                  Up (ports: 127.0.0.1:2368->2368)
ghost-db            mysql:8.0                       Up (healthy)
ghost-proxy         nginx:alpine                    Up


What we tried (chronological)

1) Tried self hosting the ActivityPub containers

We initially tried adding the official ActivityPub containers alongside Ghost:

  • ghcr.io/tryghost/activitypub

  • ghcr.io/tryghost/activitypub-migrations

On the Pi (arm64), this failed due to image architecture mismatch. The migration container would not run and threw an exec format error.

Example of what we saw:

WARNING: The requested image platform (linux/amd64) does not match the detected host platform (linux/arm64/v8)
exec /usr/local/bin/up: exec format error

So we removed those services and moved on.


2) Tried proxying ActivityPub to hosted ap.ghost.org using nginx

We then switched to the “proxy ActivityPub to ap.ghost.org” approach.

We added this in the Ghost container environment:

ACTIVITYPUB_TARGET: https://ap.ghost.org
url: https://beitmenotyou.online

We created an nginx container (ghost-proxy) with a config intended to:

  • proxy /.ghost/activitypub/ to https://ap.ghost.org

  • proxy /.well-known/webfinger and /.well-known/nodeinfo to https://ap.ghost.org

  • proxy everything else to the local Ghost container (http://ghost:2368)

Current nginx config looks like this:

server {
  listen 80;

  location ^~ /.ghost/activitypub/ {
    proxy_pass https://ap.ghost.org;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto https;
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    proxy_ssl_server_name on;
    proxy_ssl_name ap.ghost.org;
  }

  location = /.well-known/webfinger {
    proxy_pass https://ap.ghost.org;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto https;
    proxy_set_header X-Forwarded-Host $host;
    proxy_ssl_server_name on;
    proxy_ssl_name ap.ghost.org;
  }

  location = /.well-known/nodeinfo {
    proxy_pass https://ap.ghost.org;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto https;
    proxy_set_header X-Forwarded-Host $host;
    proxy_ssl_server_name on;
    proxy_ssl_name ap.ghost.org;
  }

  location / {
    proxy_pass http://ghost:2368;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto https;
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  }
}

Cloudflared config validates:

cloudflared tunnel ingress validate
OK

Cloudflared connects successfully and shows the tunnel is live. We also see:

Updated to new configuration ... ingress ... hostname:"beitmenotyou.online"


Current symptoms and test results

Public tests (from the server)

Main site works:

curl -sS https://beitmenotyou.online/ | head

But ActivityPub / discovery endpoints fail.

Examples:

curl -sSIL "https://beitmenotyou.online/.well-known/webfinger/?resource=acct:admin@beitmenotyou.online" | sed -n '1,25p'
curl -sSIL "https://beitmenotyou.online/.well-known/nodeinfo/" | sed -n '1,25p'
curl -sSIL "https://beitmenotyou.online/.ghost/activitypub/v1/site/" | sed -n '1,25p'

We see a mix of:

  • HTTP/2 502 (earlier when routing changes were wrong)

  • HTTP/2 530 (during some proxy attempts)

  • HTTP/2 503 (during some proxy attempts)

  • and now consistently 403 with:

{"error":"Forbidden","code":"SITE_MISSING"}

We also sometimes see redirects like:

  • /.well-known/nodeinfo/.well-known/nodeinfo/

  • /.well-known/webfinger/.well-known/webfinger/

Internal tests (inside ghost-proxy)

From inside the nginx container we can hit the route and see nginx responding, but it still ends with a 403 coming back:

docker exec -it ghost-proxy sh -lc \
'curl -sSIL -H "Host: beitmenotyou.online" http://127.0.0.1/.well-known/nodeinfo/ | sed -n "1,25p"'

Result pattern:

  • 302 Found to the non trailing slash version

  • then 403 JSON


Extra notes

  • cloudflared logs always include:
Cannot determine default origin certificate path. No file cert.pem ...

Tunnel still connects and the site loads, so not sure if this matters for ActivityPub.

  • nginx logs show external ActivityPub style POST attempts hitting endpoints like /inbox and /wp-json/.../inbox returning 404, which suggests federation traffic is reaching the site but not being handled.

What I’m hoping the community can clarify

  1. Is self hosting ActivityPub officially supported on arm64 yet? The official ActivityPub images we tried appear to be amd64 only, so they fail on Raspberry Pi.

  2. For the ap.ghost.org reverse proxy approach, what does {"code":"SITE_MISSING"} actually mean in practice?

  • Is there a required registration or handshake step for self hosted sites?

  • Is this expected until some specific setting inside Ghost is enabled?

  1. Are there any known Ghost 6.14.0 issues (or 6.15.0 update issues) on arm64 that impact ActivityPub?

If anyone can point me at the correct “known good” setup for Cloudflare Tunnel + self hosted Ghost + ActivityPub, I’d really appreciate it. Happy to share full redacted configs and logs.

Thanks!


Optional context links (not required, but these match the errors we’re seeing):

Ghost forum thread about 403 SITE_MISSING on self host. (Ghost Forum)

https://forum.ghost.org/t/activitypub-error-forbidden-code-site-missing/57993

Ghost forum thread about self hosted ActivityPub 403 errors and proxy edge cases (JWKS endpoint mentioned). (Ghost Forum)

https://forum.ghost.org/t/fixing-ghost-activitypub-404-error-on-self-hosted-installation/60743

If you want, paste your current /opt/ghost/cloudflared/config.yml too (redact the token) and I’ll tweak the “Current architecture” section so it exactly matches what Cloudflare is routing to right now.

My approach is based on kubernetes, but maybe you could achieve it with Docker. I’ve managed using Traefik v3 as a “internal proxy”, cloudflare tunnel pointing to the internal traefik service, external dns to update the DNS records in cloudflare and kubernetes ingresses (with traefik as a controller) to manage the traffic for each domain name.

The trick for ActivityPub was a Traefik File Provider, which create a special kind of service and proxies the required requests to ap.ghost.org

If you want to try the Traefik method, I can give you more details or you can also refer to this topic ActivityPub at Ghost in Docker + Traefik and Cloudflare from @caeike who solved the problem.

thanks ever so much friend i’ll have a go at this and if I have any questions Il be back

I’m self hosting the activitypub server in a vm so my config might differ slightly from yours, but my nginx config looks like this, hope this helps

    location ~ /.ghost/activitypub/* {
        proxy_set_header        Host              $host;
        proxy_set_header        Authorization     $http_authorization;
        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_no_cache          1;
        proxy_cache_bypass      1;
        proxy_ssl_server_name on;
        set $upstream_app ghostactivitypub.rabbithole;
        set $upstream_port 80;
        set $upstream_proto http;
        proxy_pass $upstream_proto://$upstream_app:$upstream_port;
    }

    location ~ /.well-known/(webfinger|nodeinfo) {
        proxy_set_header        Host              $host;
        proxy_set_header        Authorization     $http_authorization;
        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_no_cache          1;
        proxy_cache_bypass      1;
        proxy_ssl_server_name on;
        set $upstream_app ghostactivitypub.rabbithole;
        set $upstream_port 80;
        set $upstream_proto http;
        proxy_pass $upstream_proto://$upstream_app:$upstream_port;
    }