Ghost 6 and ActivityPub behind Traefik on Docker

How to set up ActivityPub when running Ghost 6 behind the Traefik reverse proxy on Docker.

Ghost 6 and ActivityPub behind Traefik on Docker

On August 4th, Ghost 6 launched - the latest major update to the popular publishing platform, bringing with it a feature I've been waiting for for a while - ActivityPub!

For the uninitiated, ActivityPub is a protocol and open standard for decentralized social networking, the technology powering the likes of Mastodon and Pixelfed - and to an extent Threads.

Now, I've been running Ghost for years and been patiently waiting for the release of Ghost 6 so that I could switch off my Mastodon instance and just use my main website. With the launch, Ghost now also has an official - even if it's still in preview for now - docker compose file which you can find in on Github.

GitHub - TryGhost/ghost-docker
Contribute to TryGhost/ghost-docker development by creating an account on GitHub.

The issue with the official compose.yml approach is that Ghost is using Caddy as their webserver. Now, as I run a few other services on the same machine and have been using Traefik as a reverse proxy for years, I ditched their Caddy configuration and added my Traefik configuration instead.

Global Traefik setup

If you already have Traefik running, you can easily skip this part. However, for the completeness of this guide, I'm just including my compose file for my Traefik instance below.

services:
  traefik:
    image: "traefik:latest"
    restart: unless-stopped
    command:
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--providers.docker"
      - "--providers.docker.exposedbydefault=false"
      # Generic Cert Resolver
      - "--certificatesresolvers.leresolver.acme.caserver=https://acme-v02.api.letsencrypt.org/directory"
      - "--certificatesresolvers.leresolver.acme.email=youremail@yourdomain.tld"
      - "--certificatesresolvers.leresolver.acme.storage=/acme.json"
      - "--certificatesresolvers.leresolver.acme.tlschallenge=true"

    ports:
      - "80:80"
      - "443:443"
    networks:
      - 'traefik_default'
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
      - "./acme.json:/acme.json"
    labels:
      - "traefik.enable=true"
      # global redirect to https
      - "traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`)"
      - "traefik.http.routers.http-catchall.entrypoints=web"
      - "traefik.http.routers.http-catchall.middlewares=redirect-to-https"

Some things to note:

  • I'm running traefik on a network called traefik_default.
  • My traefik entrypoint for HTTPS is websecure.

Setting up Ghost on Traefik

We'll be using the ghost-docker repository as a base and will be lightly modifying the .env and compose.yml file.

You can find the official setup guide here.

Start by copying the .env.example file to .env and go through the configuration, setting up your domain, database, email and the locations.

Next up, we'll get the basics of Ghost up and running. For this, you'll be able to comment out all services in the docker-compose except for ghost and db.

Now, let's start by adding some labels to our ghost service, by adding the following section to the ghost service in the compose.yml file.

    labels:
     - "traefik.enable=true"
     - "traefik.http.routers.ghost.rule=Host(`yourdomain.tld`) || Host(`www.yourdomain.tld`)"
     - "traefik.http.routers.ghost.entrypoints=websecure"
     - "traefik.http.routers.ghost.tls=true"
     - "traefik.http.routers.ghost.tls.certresolver=leresolver"
     - "traefik.http.middlewares.sslheader.headers.customrequestheaders.X-Forwarded-Proto = http"
     - "traefik.http.routers.ghost.service=ghost-server"
     - "traefik.http.services.ghost-server.loadbalancer.passhostheader=true"
     - "traefik.http.services.ghost-server.loadbalancer.server.port=2368"
     - "traefik.docker.network=traefik_default"

I also need to add the traefik_default network to my networks section but you may not need to (see section above). If you don't run Traefik on a separate network in Docker, you can also skip the line with the network label: "traefik.docker.network=traefik_default"

Fire up your ghost and database container by running docker compose up. You should be able to access and set up your Ghost instance after a few minutes. However, we haven't configured your ActivityPub endpoints yet.

Adding the router and service for ActivityPub

In order to get ActivityPub working you have 2 options - using Ghost's server or running your own.

Unless you're expecting to interact a lot, I'd recommend using Ghost's ActivityPub server. You can find their usage limits here.

Hosted ActivityPub server

In this case, all you need to do is add the following few lines to the labels section in your compose file - just add them below the labels we just added for Traefik already.

     # Activity Pub
     - "traefik.http.services.ghost-activitypub.loadbalancer.passhostheader=true"
     - "traefik.http.services.ghost-activitypub.loadbalancer.server.url=https://ap.ghost.org"
     - "traefik.http.routers.ghost-activitypub.entrypoints=websecure"
     - "traefik.http.routers.ghost-activitypub.service=ghost-activitypub"
     - "traefik.http.routers.ghost-activitypub.tls=true"
     - "traefik.http.routers.ghost-activitypub.tls.certresolver=leresolver"
     - "traefik.http.routers.ghost-activitypub.rule=Host(`yourdomain.tld`) && (PathPrefix(`/.ghost/activitypub/`) || Path(`/.well-known/webfinger`) || Path(`/.well-known/nodeinfo`))"

These few lines will add a new service and router for ActivityPub, redirecting the necessary paths to Ghost's ActivityPub servers.

Just redeploy your ghost container (docker compose down, docker compose up) and you should be ready to roll.

You can verify that it's working by going to the Network page in your Ghost Admin where you should see your ActivityPub address, along the lines of @index@yourdomain.tld.

Self-Hosted ActivityPub server

The other option is to self-host the ActivityPub server, for which you would need to add the following line to your .env file: COMPOSE_PROFILES=activitypub

Next we'll add the following labels to the activitypub service in the compose.yml file.

    labels:
     - "traefik.enable=true"
     - "traefik.http.services.ghost-activitypub.loadbalancer.passhostheader=true"
     - "traefik.http.services.ghost-activitypub.loadbalancer.server.port=8080"
     - "traefik.http.routers.ghost-activitypub.entrypoints=websecure"
     - "traefik.http.routers.ghost-activitypub.service=ghost-activitypub"
     - "traefik.http.routers.ghost-activitypub.tls=true"
     - "traefik.http.routers.ghost-activitypub.tls.certresolver=leresolver"
     - "traefik.http.routers.ghost-activitypub.rule=Host(`yourdomain.tld`) && (PathPrefix(`/.ghost/activitypub/`) || Path(`/.well-known/webfinger`) || Path(`/.well-known/nodeinfo`))"

Same as above, redeploy using docker compose and you should now have another container running for ActivityPub. Verify in the same way as above, by going to the Network page, that everything is working and you're good to go!

Welcome to the Fediverse!

That's all! You're ready to start exploring the fediverse! Now, feel free to reply to this post through ActivityPub - from your own Network page!