Upgrading to Ghost 6.0: ActivityPub, Analytics, and Wrestling with Cloudflare Connectors

Self-hosting Ghost 6.0 with Docker, Cloudflare Connector, and the new ActivityPub and Tinybird analytics features. Here's what worked and what didn't.

Upgrading to Ghost 6.0: ActivityPub, Analytics, and Wrestling with Cloudflare Connectors
Photo by JESHOOTS.COM / Unsplash

The Challenge

My home server was running Ghost 5.0 in Docker, humming along behind a Cloudflare Zero Trust Connector. Ghost 6.0 dropped with two features I'd been waiting for: ActivityPub integration for the social web and built-in traffic analytics powered by Tinybird's API. Time to upgrade.

The catch? Ghost's official Docker Compose setup assumes you're running a standard reverse proxy. When you're routing through Cloudflare's connector, that assumption breaks. Let's modify our way through it.

What You'll Need

Hardware & Setup:

  • A home server running Docker and Docker Compose
  • Cloudflare Zero Trust Connector already configured
  • Your existing Ghost 5.0 installation
  • SSH access to your server

Materials:

  • Ghost 6.0 Docker image
  • Access to your Caddy configuration files
  • A text editor you trust (I use nano, but vim works just as well)

The Upgrade Path

Step 1: Backup Everything

Before we touch anything, let's make sure we can roll back if things go sideways.

  1. Copy your current docker-compose.yml somewhere safe

Export your database:

docker exec ghost-mysql mysqldump -u root -p ghost_prod > ghost-db-backup-$(date +%Y%m%d).sql

Backup your Ghost content directory:

tar -czf ghost-content-backup-$(date +%Y%m%d).tar.gz /path/to/ghost/content

The weight of that backup file is reassuring. We're ready to break things.

Step 2: Update the Docker Compose File

Ghost's official Docker setup is more sophisticated than a basic single-container deployment. Open your compose.yml - you'll see it orchestrates multiple services: Caddy for reverse proxy, Ghost itself, MySQL for the database, plus optional traffic-analytics and activitypub services.

The key change is in the Ghost service image version. Look for this line:

ghost:
  image: ghost:${GHOST_VERSION:-6-alpine}

Update your .env file to set the Ghost version:

GHOST_VERSION=6-alpine

The compose file uses environment variables extensively. Make sure your .env has these critical variables set:

DOMAIN=yourdomain.com
DATABASE_PASSWORD=your_secure_password
DATABASE_ROOT_PASSWORD=your_secure_root_password
TINYBIRD_ADMIN_TOKEN=your_token  # For analytics
TINYBIRD_WORKSPACE_ID=your_workspace_id

Notice the compose file includes profiles for analytics and activitypub. We'll enable those in a moment. But here's where it gets interesting.

Step 3: Modify Caddy for Cloudflare Connector

This is the tricky part. Ghost's official compose setup uses Caddy as the reverse proxy with automatic HTTPS. But with Cloudflare Zero Trust Connector, SSL termination happens at Cloudflare's edge. Your connector talks to Caddy over plain HTTP internally.

The Problem: The default Caddy setup expects to handle HTTPS directly. When traffic comes through the Cloudflare Connector, it arrives as HTTP, but Ghost needs to know the original request was HTTPS.

The Solution: We need to custom-fit the Caddy configuration to trust forwarded headers from Cloudflare.

  1. Locate your Caddyfile. The compose file mounts it from ./caddy:/etc/caddy. Create a Caddyfile in your ./caddy directory if you haven't already.

Create a Cloudflare-compatible Caddyfile:

{
    auto_https off
    admin off
}

{$DOMAIN}:80 {
    import snippets/Logging
    import snippets/TrafficAnalytics
    import snippets/ActivityPub

    # Forward requests to Ghost and ensure Ghost sees HTTPS and the real client IP
    handle {
        reverse_proxy ghost-hslabs:2368 {
            header_up Host {host}
            header_up X-Forwarded-Proto https
        }
    }

    # gzip, security headers, etc.
    encode gzip
    import snippets/SecurityHeaders
}


Modify ActivityPub file in snippets folder:

# ActivityPub
# Proxy activitypub requests /.ghost/activitypub/
handle /.ghost/activitypub/*  {
	reverse_proxy {$ACTIVITYPUB_TARGET} {
		header_up Host {host}
		header_up X-Real-IP {remote_host}
		header_up X-Forwarded-Proto https
		header_up X-Forwarded-Host {host}
	}
}

handle /.well-known/webfinger {
	reverse_proxy {$ACTIVITYPUB_TARGET} {
		header_up Host {host}
		header_up X-Real-IP {remote_host}
		header_up X-Forwarded-Proto https
		header_up X-Forwarded-Host {host}
	}
} 

handle /.well-known/nodeinfo {
	reverse_proxy {$ACTIVITYPUB_TARGET} {
		header_up Host {host}
		header_up X-Real-IP {remote_host}
		header_up X-Forwarded-Proto https
		header_up X-Forwarded-Host {host}
	}
}

The critical pieces here:

  • auto_https off - We're not handling SSL; Cloudflare is
  • X-Forwarded-Proto https - This tells Ghost the original request was HTTPS, even though Caddy received HTTP
  • Path-based routing - Different paths go to different services (Ghost, ActivityPub, analytics)

Verify your Ghost environment variables in .env:

url=https://yourdomain.com

Keep the URL as https:// even though Caddy listens on HTTP internally. Ghost needs to know its public-facing URL for generating links correctly.

The texture of this configuration is important - you're threading the needle between Cloudflare's edge, your connector, and Ghost's expectations about HTTPS.

Step 4: Deploy with Profiles

Time to assemble the new setup. Ghost's compose file uses profiles to enable optional services. We want both ActivityPub and analytics.

    • ghost - The main application
    • caddy - Reverse proxy with your custom config
    • db - MySQL database
    • activitypub - ActivityPub federation service
    • traffic-analytics - Tinybird analytics proxy
    • Plus supporting migration and sync containers

Edit the .env file

Add activitypub to the COMPOSE_PROFILES variable at the top. This automatically includes the activitypub profile when running docker compose commands:

COMPOSE_PROFILES=analytics,activitypub

# Alternatively if you only want activitypub
COMPOSE_PROFILES=activitypub

Next, uncomment the ACTIVITYPUB_TARGET line to point to your local install. It should look like this:

ACTIVITYPUB_TARGET=activitypub:8080

Install and run Ghost with ActivityPub

Use docker compose to install new images and run Ghost with ActivityPub:

docker compose pull
docker compose up -d

You'll see Ghost run its migration scripts. This is the moment of truth. The logs scroll by, database tables get modified, and then... it settles. Your site is live on Ghost 6.0.

Check service logs:

docker compose logs -f activitypub
docker compose logs -f traffic-analytics

Each service should start cleanly and report its readiness.

Ghost will generate a new ActivityPub actor for your site and connect to the activitypub service container. The setup mentions integration with the Fediverse - platforms like Mastodon should theoretically be able to follow your blog.

What it does: Your blog posts can now be discovered and followed by people on federated social platforms. It's an experiment in open, decentralized social networking.

My experience so far: I've enabled it and I'm watching how it federates. The Ghost ActivityPub GitHub project mentions Fediverse compatibility, but I haven't seen concrete Mastodon integration yet. More exploration needed - this is bleeding edge territory.

Step 5: Enabling Web Analytics

This section assumes you’ve migrated an existing site to Docker.

Get Tinybird setup

Make sure you have a Tinybird account with a workspace. You can choose any cloud and region. Then, making sure you’re still inside of /opt/ghost, run the following 4 commands:

docker compose run --rm tinybird-login

Follow the steps to login to your Tinybird account.

docker compose run --rm tinybird-sync

Copy the Tinybird schema files from the Ghost container into a shared volume. The service should log “Tinybird files synced into shared volume.”, then exit.

docker compose run --rm tinybird-deploy

Deploy your tinybird configuration. This will create your Tinybird datasources, pipes and API endpoints. It may take a minute or two to complete the first time. You should see “Deployment #1 is live!” in your terminal before the service exits.

docker compose run --rm tinybird-login get-tokens

Get your tinybird tokens and configuration.

Add them to your .env file:

TINYBIRD_ADMIN_TOKEN=your_admin_token
TINYBIRD_WORKSPACE_ID=your_workspace_id
TINYBIRD_TRACKER_TOKEN=your_tracker_token

Enable in Ghost:

  1. In Ghost Admin, go to Settings → Analytics
  2. Toggle on Traffic Analytics
  3. Ghost will start collecting data automatically

The analytics interface is clean and fast. You get:

  • Page views and visitor counts
  • Referral sources
  • Top content
  • Real-time traffic

It's not as comprehensive as Google Analytics, but it's tactile and integrated. You can see trends without leaving your Ghost dashboard. The data flows through your own infrastructure - Ghost → traffic-analytics container → Tinybird API → back to Ghost's admin interface.

(I'm planning a future post comparing Ghost's built-in analytics vs GA - there's material here for a deeper dive.)

The Result

The upgrade took about two hours - most of it spent understanding how Cloudflare Connector, Caddy, and Ghost's multi-service architecture needed to work together. Once I custom-fit the Caddyfile to handle forwarded headers and enabled the compose profiles correctly, everything clicked into place.

What worked:

  • Docker profiles made enabling ActivityPub and analytics clean - just add --profile flags
  • The multi-container setup (Ghost + Caddy + ActivityPub + traffic-analytics) orchestrates smoothly
  • Migration scripts ran without issues - database updates were seamless
  • ActivityPub integration was literally one toggle after the service was running
  • Tinybird analytics started collecting data immediately after credential setup

What was messy:

  • The Caddy configuration isn't documented for Cloudflare Connector setups - I had to piece it together
  • Path-based routing to different services (Ghost, ActivityPub, analytics) required careful attention to X-Forwarded-* headers
  • Tinybird credential setup and the deploy process took some trial and error
  • ActivityPub is still new - the Fediverse integration is promising but I haven't seen concrete federation yet

What's next:

  • Monitoring how ActivityPub actually federates with Mastodon and other platforms - this is uncharted territory
  • Diving deeper into Tinybird's analytics data pipeline and what metrics matter most
  • Comparing Ghost's integrated analytics vs Google Analytics in a future build log
  • Exploring the activitypub service's API and how it handles federation requests

The tactile satisfaction of a self-hosted Ghost 6.0 instance running the latest features through your own hardware stack? Worth wrestling with Caddy configs and Docker profiles. The materials are there - you just need to assemble them right.


Running into issues with Cloudflare Connector and Ghost? Drop a comment - let's troubleshoot together.