Ghost 6 and ActivityPub behind Traefik on Docker
How to set up ActivityPub when running Ghost 6 behind the Traefik reverse proxy 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.
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!
