Digital Sovereign Ghost

Why and How I Moved My Analytics Backend to a Local Deployment

Digital Sovereign Ghost
Picture: Phoenix Paulina Schmid / Canva

Ghost is a wonderful blogging and publishing platform and running an own modern blog is more than just putting a Content Management System on a VPS or use a dedicated provider. As it is a constant commitment to digital sovereignity, data ownership and minimizing unnecessary dependencies on foreign cloud providers.

When Ghost published its 6.0 version, it introduced a native analytics feature that relies upon the New York, US-based Tinybird for cookie-free, real-time data processing. And by default does the documentation steer the admin of self-hosted Ghost instances (servers) to signup for an free Tinybird account for the cloud based offering. The free offering is good for small and medium blogs but does run on the cloud infrastructure of Google and(/or) Amazon.

Exactly this point feels for me like an compromise of the very principles that led for me to do self-hosting in the first place and therefore led to me to look for the possibility to pull that dependency into my own control.

The Experiment

My try to move to local Tinybird

Tinybird offers two ways to self-host its infrastructure. Tinybird Self-Managed Regions and Tinybird Local. The former one is aimed at enterprise-scale deployments with the related resources, whereas the latter is designed for development.

But with the right configuration and a robust reverse proxy can the Tinybird Local container function as a lightweight, permanent backend for Ghost Analytics on a self-hosted Ghost instance.

I tested the possibility of that in the night from the 6th to the 7th of June 2026. And as you see this version of the blog post I can say that this was successful – the analytics of this blog use the Tinybird Local container instead of the cloud offering.

The Architecture

What is running under the hood

We need to look at what the Tinybird Local is to understand how this works. This isn't a monolithic black box but a developer-centric streaming data platform build on top of ClickHouse – an database designed for analytics data.

When you spin up the tinybird-local container you are essentially starting a lightweight project model similar to like in the cloud offering of Tinybird but contained in a single container.

Here is how the data flows in this self-hosted ecosystem:

  1. The Reader: Someone visits the blog. Ghost's native tracking script sends an event.
  2. Caddy: Intercepts the analytics traffic and securely routes it to the local infrastructure.
  3. Tinybird Local: Ingests the raw event stream, validates it, and efficiently stores and aggregates the analytics data.
  4. Ghost Dashboard: Queries the local Tinybird API to display your real-time visitor stats.

If you like this blog post you can support my work by either becoming a paid member or leaving a tip.

Donate

The Setup

Making it Work

⚠️
The following is not officially supported by either team of Ghost or Tinybird. Proceed with your own risk.

To transition your analytics backend from the cloud offering to an deployed instance of Tinybird Local we have to do the following (Requirement is to have Ghost already deployed via Docker):

Docker Compose
We can simply add the following configuration to the compose.yml with which we run Ghost:

  tinybird-local:
    image: tinybirdco/tinybird-local:latest
    container_name: tinybird-local
    restart: unless-stopped
    ports:
      - "127.0.0.1:7181:7181"
    volumes:
      - ./data/tb_local/clickhouse:/var/lib/clickhouse
      - ./data/tb_local/redis:/redis-data
    profiles: [analytics]
    networks:
      - proxy
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:7181"]
      interval: 5s
      timeout: 5s
      retries: 30

  tinybird-sync:
    # Do not alter this without updating the Ghost container as well
    image: ghost:${GHOST_VERSION}
    container_name: tinybird-sync
    command: >
      sh -c "
        if [ -d /var/lib/ghost/current/core/server/data/tinybird ]; then
          rm -rf /data/tinybird/*;
          cp -rf /var/lib/ghost/current/core/server/data/tinybird/* /data/tinybird/;
          echo 'Tinybird files synced into shared volume.';
        else
          echo 'Tinybird source directory not found.';
        fi
      "
    volumes:
      - ./data/tinybird/files:/data/tinybird
    depends_on:
      tinybird-local: # Add this
        condition: service_healthy # Add this
      tinybird-login:
        condition: service_completed_successfully
    networks:
      - proxy
    profiles: [analytics]
    restart: no

  tinybird-deploy:
    build:
      context: ./tinybird
      dockerfile: Dockerfile
    container_name: tinybird-deploy
    working_dir: /data/tinybird
    command: > 
      sh -c '
        for i in $(seq 1 20); do
          if tb-wrapper --cloud deploy -v; then
            exit 0
          fi
          echo "Tinybird deploy not ready yet, retrying in 5s..."
          sleep 5
        done
        exit 1
      ' # Change this
    volumes:
      - ./data/tinybird/home:/home/tinybird
      - ./data/tinybird/files:/data/tinybird
    depends_on:
      tinybird-sync:
        condition: service_completed_successfully
    profiles: [analytics]
    networks:
      - proxy
    tty: true

Caddy
Allowing Ghost to talk with the Tinybird Local container requires that we add the following to the Caddyfile. Replace DOMAIN with your own domain.

# Tinybird for Ghost
tinybird.DOMAIN {
    @tinybird_admin {
        path /tokens*
        not remote_ip 127.0.0.1 ::1 100.64.0.0/10
    }
    respond @tinybird_admin "Forbidden" 403

    reverse_proxy 127.0.0.1:7181
}

Environment file
After starting the tinybird-local container for the first time can we look up the tokens via the http://localhost:7181/tokens URL and changing or adding the respective tokens to the .env file. Don't forget to also change the .tinyb file in the subdirectories of data/tinybird.

Closing thoughts

Securing your own digital sovereignty isn't always easy. It sometimes requires peeking behind or around official documentations and occasionally repurposing tools to serve production needs.

By pulling the Ghost Analytics of self-hosted instances out of the cloud do we eliminate a major point of external surveillance and take responsibility for the data privacy of the readers of our blog and members of our communities. It's just pure and high-power independent publishing.

Would you – if you have a Ghost based blog – switch your analytics backend or stay with whatever you have? Let's discuss! 📝

If you want to hear more from me you can find me in the Fediverse at @gelbphoenix@social.gelbphoenix.de (Mastodon) or @gelbphoenix@gram.social (Pixelfed). For more posts like this subscribe to my newsletter or support me by becoming a member or donating.