Skip to content
8 min read

Trying to get ActivityPub running as well

Trying to get ActivityPub running as well

I just read Cathy's blog post about getting ActivityPub started:

Getting ActivityPub running
... well, not yet.

Since I want to get ActivityPub running on Magic Pages asap, this has been on my list forever – but well...other things always seem to pop up.

Cathy left her blog post with the thought that:

Maybe, just maybe, I made this harder than it needed to be by running in WSL and with a local install of Ghost. I may need to take another run at it with a 'real' Linux server, because it feels like I got almost there....

Well, let's put that to a test. I am running a non-virtualised Ubuntu 24.04 environment. So...maybe things will run better? Follow along:

(I am typing this post as I run the commands on my machine – I am curious how far we get.)

First up, install a fresh copy of Ghost in a new folder:

ghost install --local

Cool, that worked.

Meanwhile, install Tailscale. For some reason, the command Cathy ran (curl -fsSL https://tailscale.com/install.sh | sh and sudo tailscaled did not work for me):

curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/oracular.noarmor.gpg | sudo tee /usr/share/keyrings/tailscale-archive-keyring.gpg >/dev/null
curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/oracular.tailscale-keyring.list | sudo tee /etc/apt/sources.list.d/tailscale.list

sudo apt-get update
sudo apt-get install tailscale

sudo tailscale up

Then, make sure that Ghost is listening to the Tailscale URL:

ghost config url https://YOURINFO.tailYOURINFOHERE.ts.net

Ghost reminded me that I should restart Ghost for the config to work, so I did just that:

ghost restart

Next step, get the ActivityPub server running.

git clone https://github.com/TryGhost/ActivityPub.git
cd ActivityPub
yarn dev

Docker images are being downloaded – and then started as containers.

But...I run into the same issue as Cathy. The Tailscale URL is not connecting to anything. Looking at the docker-compose.yml in the ActivityPub repository, I see that NGINX should be started though.

A docker ps shows no NGINX container running though. Hm...

So, I run a docker logs activitypub-server-nginx-1:

/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf
10-listen-on-ipv6-by-default.sh: info: /etc/nginx/conf.d/default.conf differs from the packaged version
/docker-entrypoint.sh: Sourcing /docker-entrypoint.d/15-local-resolvers.envsh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
/docker-entrypoint.sh: Configuration complete; ready for start up
2025/03/02 19:46:38 [emerg] 1#1: host not found in upstream "host.docker.internal" in /etc/nginx/conf.d/default.conf:15
nginx: [emerg] host not found in upstream "host.docker.internal" in /etc/nginx/conf.d/default.conf:15

Okay, this is interesting. The NGINX container couldn't resolve the hostname host.docker.internal, which is set in the nginx/server.conf. This is makes sense. host.docker.internal is a Mac and Windows specific way of telling Docker to access the host. For Linux you'd need to pass this explicitly:

What is the Linux equivalent of “host.docker.internal”?
On Mac and Windows, it is possible to use host.docker.internal (Docker 18.03+) inside a container. Is there one for Linux that will work out of the box without passing environment variables or extr…

I have changed the nginx service in the docker-compose.yml to this:

  nginx:
    build: nginx
    ports:
      - "80:80"
    depends_on:
      - activitypub
    extra_hosts:
      - "host.docker.internal:host-gateway"

And tadaaaa...NGINX is running:

/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf
10-listen-on-ipv6-by-default.sh: info: /etc/nginx/conf.d/default.conf differs from the packaged version
/docker-entrypoint.sh: Sourcing /docker-entrypoint.d/15-local-resolvers.envsh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
/docker-entrypoint.sh: Configuration complete; ready for start up

Accessing my Tailscale URL I get a 502 error, which is well...better? I mean, before I just didn't get a connection at all. Now, NGINX is telling me that there is no connection to Ghost. So...yay? 😂

I checked with ghost ls to make sure Ghost is running. And yes, it is. But...it is listening to the Tailscale URL. That sparks a thought. What if I set the Ghost configuration to have Ghost listen to 0.0.0.0 rather than just 127.0.0.1?

So, changed the configuration, restarted Ghost, opened the Tailscale URL again and...

Woohoo 🎉

Now, next step: activate the ActivityPub alpha. For that, developer experiments need to be activated in Ghost's configuration:

  "enableDeveloperExperiments": {
    "enableDeveloperExperiments": true
  }

Then, another ghost restart – and the option appears in Ghost's Labs settings:

And...you guessed it. Another ghost restart. According to the README.md of the ActivityPub repository, this should initialise a handshake between Ghost and ActivityPub. Well, let's see what happens.

[2025-03-02 21:16:22] INFO Url configured as: http://jannis-laptop.tail11f393.ts.net/
[2025-03-02 21:16:22] INFO Ctrl+C to shut down
[2025-03-02 21:16:22] INFO Ghost server started in 0.242s
[2025-03-02 21:16:23] INFO Database is in a ready state.
[2025-03-02 21:16:23] INFO Ghost database ready in 0.409s
[2025-03-02 21:16:23] INFO Invalidating assets for regeneration
[2025-03-02 21:16:23] INFO Adding offloaded job to the inline job queue
[2025-03-02 21:16:23] INFO Scheduling job mentions-email-report at 53 37 * * * *. Next run on: Sun Mar 02 2025 21:37:53 GMT+0100 (Central European Standard Time)
[2025-03-02 21:16:24] INFO Adding offloaded job to the inline job queue
[2025-03-02 21:16:24] INFO Scheduling job clean-expired-comped at 54 45 4 * * *. Next run on: Mon Mar 03 2025 04:45:54 GMT+0100 (Central European Standard Time)
[2025-03-02 21:16:24] INFO Adding offloaded job to the inline job queue
[2025-03-02 21:16:24] INFO Scheduling job clean-tokens at 14 43 17 * * *. Next run on: Mon Mar 03 2025 17:43:14 GMT+0100 (Central European Standard Time)
[2025-03-02 21:16:24] INFO Ghost booted in 1.512s
[2025-03-02 21:16:24] INFO URL Service ready in 1021ms
[2025-03-02 21:16:24] INFO "GET /ghost/.well-known/jwks.json" 200 10ms
[2025-03-02 21:16:25] ERROR Could not get webhook secret for ActivityPub FetchError: invalid json response body at http://jannis-laptop.tail11f393.ts.net/.ghost/activitypub/site reason: Unexpected token 'I', "Internal S"... is not valid JSON
[2025-03-02 21:16:25] ERROR No webhook secret found - cannot initialise
[2025-03-02 21:16:25] INFO Adding offloaded job to the inline job queue
[2025-03-02 21:16:25] INFO Scheduling job update-check at 22 58 1 * * *. Next run on: Mon Mar 03 2025 01:58:22 GMT+0100 (Central European Standard Time)
[2025-03-02 21:16:25] INFO Running milestone emails job on Sun Mar 02 2025 21:16:30 GMT+0100 (Central European Standard Time)

Hm. No webhook secret found? Well, let's check the logs of the ActivityPub stack in Docker:

TypeError: fetch failed
Error: connect ECONNREFUSED 100.96.111.34:443

Hm. Similar issue to what Cathy saw. SSL isn't set up. Quick search on how to do that with Tailscale (yeah, never used that before). Ran sudo tailscale cert mytailscaledomain.com. And...

500 Internal Server Error: your Tailscale account does not support getting TLS certs

🙃

You need to explicitly enable that here: https://login.tailscale.com/admin/dns

Bit weird, but hey.

Ran the command again, and got the certificates. Yay! I reconfigured Ghost to listen to the new URL with https by running ghost config url https://myfancynewtailscaleurlwithhttps.com and restarted it.

...and I got a ERR_CONNECTION_REFUSED.

Okay, let's take a step back. Have another look at the README.md. Oh. It says tailscale funnel 80 in there. I just did tailscale up. Whoopsie. The funnel command makes a lot more sense. It just exposes a single port. Oh well.

So, I ran tailscale funnel 80 🤷

Ghost can now initiate the handshake with the ActivityPub server, it seems:

[2025-03-02 22:15:32] INFO Checking ActivityPub Webhook state
[2025-03-02 22:15:32] INFO ActivityPub webhooks in correct state

Alright. Time to check the new ActivityPub dashboard we have all seen in Ghost's ActivityPub newsletter.

Wooohooo! 🎉

And as it turns out, I can even find this new ActivityPub instance through Mastodon:

Now, I can't seem to get see my posts or notes on Mastodon. But uhhh...we have ActivityPub running.

Next step: how the hell will I make this effort scalable for Magic Pages? 😂


And while this post shows that it's possible to self-host Ghost's ActivityPub server, I want to stress what the Ghost team published in today's newsletter:

At the moment we're moving quickly and making many breaking changes each week which aren't backwards compatible (like switching to a new DB) – so the app isn't stable. Even if we did document everything, if you self-hosted then it would just break constantly — so it doesn't make much sense for us to try to document and promote self-hosting, because it won't be a good experience for anyone.

So, do try this at home, I guess. But don't expect it to work in production.