> ## Content Index
> Fetch the complete content index at: https://corti.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Running Ghost CMS with Docker, Tinybird Analytics, ActivityPub and a Clean nginx + Caddy Split
- URL: https://corti.com/running-ghost-cms-with-docker-tinybird-analytics-activitypub-and-a-clean-nginx-caddy-split/
- Published: 2025-12-30T07:19:16.000Z
- Updated: 2025-12-30T07:19:16.000Z
- Author: Sascha Corti

[Ghost](https://ghost.org/?ref=corti.com)’s new [Docker-based installation](https://docs.ghost.org/install/docker?ref=corti.com) 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](https://docs.ghost.org/install/docker?ref=corti.com)  
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:

```text
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)

```nginx
# 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

```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**:

1. Content database
2. Images and uploads
3. 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:

```bash
restic init -r s3:s3.amazonaws.com/ghost-backups

```

Backup script:

```bash
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/Caddyfile
```

Schedule 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.