{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreiac77gv3hwtifb3odk64anbtpcvjdjxh76timhactolgxuqibyzoy",
"uri": "at://did:plc:t4geo2rtk4ooytkblfxbmwwk/app.bsky.feed.post/3mnrbkxld27t2"
},
"coverImage": {
"$type": "blob",
"ref": {
"$link": "bafkreiframjm6cyznqhdhlfqybtoxymqvk7y52wsafnpsxa2ei4znqg5py"
},
"mimeType": "image/png",
"size": 3396245
},
"description": "Why and How I Moved My Analytics Backend to a Local Deployment",
"path": "/digital-sovereign-ghost-local-tinybird/",
"publishedAt": "2026-06-08T08:30:57.000Z",
"site": "https://blog.gelbphoenix.de",
"tags": [
"Ghost",
"Tinybird",
"@gelbphoenix@social.gelbphoenix.de",
"@gelbphoenix@gram.social",
"becoming a member",
"donating",
"Mastodon",
"Bluesky",
"@tinybird_admin"
],
"textContent": "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.\n\nWhen 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.\n\nExactly 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.\n\n## __The Experiment__\n\nMy try to move to local Tinybird\n\nTinybird 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.\n\nBut 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.\n\n**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.\n\n## __The Architecture__\n\nWhat is running under the hood\n\nWe 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.\n\nWhen 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.\n\nHere is how the data flows in this self-hosted ecosystem:\n\n 1. **The Reader:** Someone visits the blog. Ghost's native tracking script sends an event.\n 2. **Caddy:** Intercepts the analytics traffic and securely routes it to the local infrastructure.\n 3. **Tinybird Local:** Ingests the raw event stream, validates it, and efficiently stores and aggregates the analytics data.\n 4. **Ghost Dashboard:** Queries the local Tinybird API to display your real-time visitor stats.\n\n\n\n_****If you like this blog post you can support my work by either becoming a paid member or leaving a tip.****_\n\nDonate\n\n## __The Setup__\n\nMaking it Work\n\n⚠️\n\nThe following is not officially supported by either team of Ghost or Tinybird. Proceed with your own risk.\n\nTo 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 and to have at least 8GB of RAM in your server):\n\n**Docker Compose**\nWe can simply add the following configuration to the compose.yml with which we run Ghost:\n\n\n tinybird-local:\n image: tinybirdco/tinybird-local:latest\n container_name: tinybird-local\n restart: unless-stopped\n ports:\n - \"127.0.0.1:7181:7181\"\n volumes:\n - ./data/tb_local/clickhouse:/var/lib/clickhouse\n - ./data/tb_local/redis:/redis-data\n profiles: [analytics]\n networks:\n - proxy\n deploy:\n resources:\n limits:\n cpus: '6.0' # Set this to your needs\n memory: 4096M\n reservations:\n memory: 2048M\n healthcheck:\n test: [\"CMD\", \"curl\", \"-fsS\", \"http://localhost:7181\"]\n interval: 5s\n timeout: 5s\n retries: 30\n\n tinybird-sync:\n # Do not alter this without updating the Ghost container as well\n image: ghost:${GHOST_VERSION}\n container_name: tinybird-sync\n command: >\n sh -c \"\n if [ -d /var/lib/ghost/current/core/server/data/tinybird ]; then\n rm -rf /data/tinybird/*;\n cp -rf /var/lib/ghost/current/core/server/data/tinybird/* /data/tinybird/;\n echo 'Tinybird files synced into shared volume.';\n else\n echo 'Tinybird source directory not found.';\n fi\n \"\n volumes:\n - ./data/tinybird/files:/data/tinybird\n depends_on:\n tinybird-local: # Add this\n condition: service_healthy # Add this\n tinybird-login:\n condition: service_completed_successfully\n networks:\n - proxy\n profiles: [analytics]\n restart: no\n\n tinybird-deploy:\n build:\n context: ./tinybird\n dockerfile: Dockerfile\n container_name: tinybird-deploy\n working_dir: /data/tinybird\n command: >\n sh -c '\n for i in $(seq 1 20); do\n if tb-wrapper --cloud deploy -v; then\n exit 0\n fi\n echo \"Tinybird deploy not ready yet, retrying in 5s...\"\n sleep 5\n done\n exit 1\n ' # Change this\n volumes:\n - ./data/tinybird/home:/home/tinybird\n - ./data/tinybird/files:/data/tinybird\n depends_on:\n tinybird-sync:\n condition: service_completed_successfully\n profiles: [analytics]\n networks:\n - proxy\n tty: true\n\n**Caddy**\nAllowing Ghost to talk with the Tinybird Local container requires that we add the following to the Caddyfile. Replace `DOMAIN` with your own domain.\n\n\n # Tinybird for Ghost\n tinybird.DOMAIN {\n @tinybird_admin {\n path /tokens*\n not remote_ip 127.0.0.1 ::1 100.64.0.0/10\n }\n respond @tinybird_admin \"Forbidden\" 403\n\n reverse_proxy 127.0.0.1:7181\n }\n\n**Environment file**\nAfter 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.\n\n## __Closing thoughts__\n\nSecuring 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.\n\nBy 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.\n\n**_Would you – if you have a Ghost based blog – switch your analytics backend or stay with whatever you have? Let's discuss!_** 📝\n\nIf 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.\n\nLiked this post? Please share it with others via: Mastodon, Bluesky or anywhere else by copying the link.",
"title": "Digital Sovereign Ghost",
"updatedAt": "2026-06-21T09:50:29.540Z"
}