Running Ghost CMS with Docker, Tinybird Analytics, ActivityPub and a Clean nginx + Caddy Split
Ghost’s new Docker-based installation approach significantly modernizes how a production Ghost instance can be deployed. It introduces first-class web analytics via Tinybird, native ActivityPub support for the social web, and a clean, composable service architecture that works very well with container orchestration.
This site you are looking at is such a Ghost deployment.
In this post, I’ll walk through:
- Installing Ghost using the new Docker setup
- Understanding the new Tinybird Analytics and ActivityPub capabilities
- Running nginx as the public HTTP + SSL reverse proxy
- Using Caddy only inside the Docker network to route traffic to Ghost, ActivityPub, and Analytics
- Backing up Ghost cleanly using restic
This setup is not fully documented in the Ghost docs, but works extremely well in practice and is exactly how this blog is running.
Why the New Docker-Based Ghost Installation?
The official Docker install described here
👉 https://docs.ghost.org/install/docker
is a big step forward compared to the classic “single Ghost container + MySQL” or full local install approach.
Key architectural changes
- Ghost is no longer just a CMS container
- Multiple services run side-by-side:
- Ghost (content)
- Tinybird (analytics ingestion)
- ActivityPub (social federation)
- Caddy (internal smart router)
This design makes Ghost:
- More observable
- More federated
- More production-ready
Installing Ghost with Docker (New Approach)
At a high level, the installation looks like this:
nginx (host, SSL termination)
↓
Caddy (docker network, HTTP only)
├─ Ghost
├─ ActivityPub
└─ Tinybird Analytics
Prerequisites
- Linux host
- Docker + Docker Compose
- DNS pointing your domain to the host
- nginx + Certbot (or acme.sh) on the host
Clone or create the Ghost Docker setup following the official docs, then adjust it as described below.
New Ghost Capabilities Explained
1. Built-in Web Analytics (Tinybird)
Ghost now integrates privacy-friendly, first-party analytics via Tinybird.
What you get
- No cookies
- No external trackers
- No GDPR banner required (in most jurisdictions)
- Page views, referrers, devices
- Fast, real-time dashboards
How it works
- Traffic flows through Caddy
- Requests are mirrored to the Tinybird ingestion endpoint
- Tinybird processes events in real time
This replaces Google Analytics entirely while keeping data under your control.
2. Native ActivityPub (Social Web / Fediverse)
Ghost now speaks ActivityPub, the protocol behind Mastodon and the Fediverse.
Capabilities
- Your blog becomes a federated actor
- Readers can follow your blog from Mastodon
- Posts appear as social posts
- Replies and likes federate back
What Ghost handles
- Actor discovery
- WebFinger
- Inbox / Outbox
- Signing and verification
All of this runs as a dedicated service behind Caddy.
nginx as the Public Reverse Proxy (SSL + HTTP)
The Ghost docs assume Caddy handles HTTPS directly.
In many production environments, that’s not ideal.
Instead, this setup uses:
- nginx on the host for:
- SSL termination
- Certbot / Let’s Encrypt
- Security headers
- Access logging
- Caddy inside Docker for:
- Routing between Ghost services
- Analytics ingestion
- ActivityPub handling
nginx configuration (host)
# Main CORTI.COM site
server {
server_name corti.com;
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
root /var/www/corti.com/html/system/nginx-root;
ssl_certificate /etc/letsencrypt/live/corti.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/corti.com/privkey.pem;
include /etc/nginx/snippets/ssl-params.conf;
# Security headers
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
# Proxy everything to Caddy
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $server_name;
# WebSockets (required by Ghost)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_buffering off;
}
access_log /var/log/nginx/corti.com_access.log;
error_log /var/log/nginx/corti.com_error.log;
}
server {
server_name corti.com;
listen 80;
listen [::]:80;
return 301 https://$host$request_uri;
}
Why this works so well
- nginx remains the single TLS entry point
- Certificates never touch Docker
- Caddy stays simple and internal
- Ghost still sees correct headers and scheme
- nginx can handle other, virtual servers or services
Caddy as an Internal Router (Docker Network Only)
Caddy runs HTTP only, listening inside Docker.
Key principles
- No TLS
- No public exposure
- Pure request routing
Caddyfile
{
auto_https off
}
:80 {
import snippets/Logging
# Traffic Analytics
import snippets/TrafficAnalytics
# ActivityPub
import snippets/ActivityPub
# Default: Ghost
handle {
reverse_proxy ghost:2368 {
header_up Host {host}
header_up X-Real-IP {header.X-Real-IP}
header_up X-Forwarded-For {header.X-Forwarded-For}
header_up X-Forwarded-Proto {header.X-Forwarded-Proto}
header_up X-Forwarded-Host {header.X-Forwarded-Host}
}
}
encode gzip
import snippets/SecurityHeaders
}
Why keep Caddy at all?
- Ghost’s Docker stack expects it
- Routing logic is clean and modular
- Analytics + ActivityPub hooks live here
- Easier upgrades as Ghost evolves
- No need to expose all the ports needed by the setup from Docker to the host system
Backing Up Ghost with restic (Best Practice)
A Ghost install has three things that matter:
- Content database
- Images and uploads
- Configuration secrets
What to back up
| Component | Location |
|---|---|
| Ghost content | content/ |
| Database | MySQL/Postgres volume |
| Environment | .env |
| Caddy config | Caddyfile + snippets |
Example restic setup
Initialize repository:
restic init -r s3:s3.amazonaws.com/ghost-backups
Backup script:
d#!/bin/bash
export RESTIC_REPOSITORY=s3:s3.amazonaws.com/ghost-backups
export RESTIC_PASSWORD=your-secret-password
export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_ACCESS_KEY=...
restic backup \
/opt/ghost/data \
/opt/ghost/.env \
/opt/ghost/caddy/CaddyfileSchedule via cron or systemd timer.
Why restic?
- Encrypted by default
- Deduplicated
- Snapshot-based
- Cloud-agnostic
- Fast restores
Perfect match for Ghost.
Final Thoughts
This setup combines:
- Ghost’s modern Docker architecture
- First-party analytics without tracking
- Native Fediverse support
- Battle-tested nginx TLS handling
- Clean internal routing with Caddy
- Reliable encrypted backups
It’s production-grade, observable, and future-proof — and it works extremely well.
If you’re serious about running Ghost as a modern publishing platform rather than “just a blog”, this approach is absolutely worth it.