Skip to content

Single-container Ghost 6 blogging platform deployed on Cloudflare Workers with Containers. Worker forwards requests to port 2368. Uses external MySQL database (RDS with TLS) and Cloudflare R2 for media storage via S3 adapter. Includes optional ActivityPub support and SMTP configuration for notifications.

Notifications You must be signed in to change notification settings

adam0white/ghost-cf

Repository files navigation

Ghost on Cloudflare Containers

Run Ghost 6 on Cloudflare Workers with Cloudflare Containers. This project builds a Ghost image with an S3-compatible storage adapter for R2, and a minimal Worker that proxies all HTTP traffic to the container.

Example production domains used here:

Features

  • Single-container Ghost deployment managed by Cloudflare Containers
  • Ephemeral disk by design; persistent data lives in external services
    • MySQL (e.g., RDS) for the database
    • Cloudflare R2 for media via an S3 adapter
  • Minimal Worker in TypeScript that forwards requests and sets the correct proxy headers
  • Sensible defaults for cold starts (sleepAfter = 5m) and logs

Architecture

  • Worker (Durable Object) → forwards requests to the Ghost container on port 2368
  • Ghost Docker image → based on ghost:6-alpine, with the ghos3 S3 adapter baked into content/adapters/storage/s3
  • MySQL (RDS) → external DB; Ghost connects over TLS
  • R2 → S3-compatible storage for media

Prerequisites

  • Cloudflare account and Wrangler CLI (logged in)
  • Docker (for local builds)
  • Publicly reachable MySQL 8 instance (RDS is fine)
  • Cloudflare R2 bucket + S3 API credentials
  • SMTP provider (Mailgun/Postmark/SES/etc.)

Setup

  1. Install dependencies
npm install
  1. Configure domains in wrangler.jsonc
  • workers_dev is disabled and routes are set to your domains:
    • zero.adamwhite.work/*
  • vars includes URL and (optionally) ADMIN_URL.
  1. Provide secrets (safest for credentials)
# Database
wrangler secret put DB_HOST
wrangler secret put DB_PORT
wrangler secret put DB_USER
wrangler secret put DB_PASSWORD
wrangler secret put DB_NAME
wrangler secret put DB_SSL_REJECT_UNAUTHORIZED  # "false" for many hosted DBs
# Optional: provide a CA bundle if your provider requires it
# wrangler secret put DB_SSL_CA

# R2 / S3
wrangler secret put R2_ENDPOINT           # e.g. https://<accountid>.r2.cloudflarestorage.com
wrangler secret put R2_BUCKET             # e.g. ghost-bucket
wrangler secret put R2_ACCESS_KEY_ID      # e.g. obtain through Cloudflare R2 Console
wrangler secret put R2_SECRET_ACCESS_KEY  # e.g. obtain through Cloudflare R2 Console
# Optional
# wrangler secret put R2_REGION           # default: auto
# wrangler secret put R2_FORCE_PATH_STYLE # default: true
# wrangler secret put R2_ASSET_HOST       # optional CDN, e.g. https://r2.yourdomain.com
# wrangler secret put R2_PATH_PREFIX      # optional bucket subdir

# SMTP
wrangler secret put SMTP_HOST
wrangler secret put SMTP_PORT             # 465 or 587
wrangler secret put SMTP_SECURE           # "true" or "false"
wrangler secret put SMTP_USER
wrangler secret put SMTP_PASS
# Optional from address
# wrangler secret put SMTP_FROM
  1. Deploy
npm run deploy

First deployment may take several minutes while the image builds and the container boots.

Configuration Reference

Ghost configuration is injected via environment variables in src/index.ts using Ghost’s double-underscore syntax (e.g., server__port). Key items:

  • URL

    • URL (required): canonical site URL
    • ADMIN_URL (optional): separate admin domain; admin UI is at /ghost
  • Server

    • server__host = 0.0.0.0
    • server__port = 2368
    • PORT = 2368 is also set to neutralize platforms that inject a different PORT
  • Database (MySQL)

    • database__client = mysql
    • database__connection__host = DB_HOST
    • database__connection__port = DB_PORT
    • database__connection__user = DB_USER
    • database__connection__password = DB_PASSWORD
    • database__connection__database = DB_NAME
    • TLS:
      • database__connection__ssl__rejectUnauthorized = DB_SSL_REJECT_UNAUTHORIZED
      • optionally database__connection__ssl__ca = DB_SSL_CA
  • Storage (R2 via S3 adapter)

    • GHOST_STORAGE_ADAPTER_S3_ENDPOINT = R2_ENDPOINT
    • GHOST_STORAGE_ADAPTER_S3_PATH_BUCKET = R2_BUCKET
    • AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
    • AWS_DEFAULT_REGION = R2_REGION (default auto)
    • GHOST_STORAGE_ADAPTER_S3_FORCE_PATH_STYLE = R2_FORCE_PATH_STYLE (default true)
    • Optional: GHOST_STORAGE_ADAPTER_S3_ASSET_HOST, GHOST_STORAGE_ADAPTER_S3_PATH_PREFIX, GHOST_STORAGE_ADAPTER_S3_ACL
  • SMTP

    • mail__transport = SMTP
    • mail__options__host = SMTP_HOST
    • mail__options__port = SMTP_PORT
    • mail__options__secure = SMTP_SECURE
    • mail__options__auth__user = SMTP_USER
    • mail__options__auth__pass = SMTP_PASS
    • Optional: mail__from = SMTP_FROM

Local Development

npm run dev
  • The Worker runs locally and forwards to a Cloudflare-managed container instance.
  • Default Ghost port is 2368; the Worker proxies all HTTP traffic.
  • Admin is available at /ghost on your domain.
  • Updates: Update the compatibility_date in wrangler.jsonc to the current date. Check the latest version of package.json for updates. Run npm outdated to check for updates. Run npm install and/or npm update, then wrangler types to update the types. Also update the Dockerfile if there's a newer Ghost version. Minor updates will be auto applied on wrangler deploy.

Operations

  • Logs

    • wrangler tail --format=pretty
  • Scaling and cold starts

    • sleepAfter is set to 5 minutes in src/index.ts. Increase if you want fewer cold starts.
    • Consider a periodic ping (Cron Trigger) if you want to keep the instance warm.
  • MySQL reverse DNS warnings

    • RDS may log warnings like "IP address 'x.x.x.x' could not be resolved". Attach a parameter group with skip_name_resolve = 1 and reboot to suppress these.

ActivityPub (optional)

Ghost 6 includes ActivityPub functionality behind a feature flag. No extra container is required.

  • Prereqs:
    • Public domain, valid HTTPS, correct canonical URL configured
    • Ghost admin → Settings (or Labs) → enable ActivityPub
    • Ensure /.well-known/webfinger and related endpoints are reachable
  • Verification:
    • Test WebFinger and ActivityPub endpoints from an external checker

Repository Layout

  • src/index.ts — Worker that proxies to the container and injects Ghost config via env vars
  • wrangler.jsonc — Worker, routes, Durable Object, Containers config
  • Dockerfile — Extends ghost:6-alpine, installs and activates the S3 storage adapter
  • config.production.json — Minimal base config (local dev defaults); env overrides at runtime

Notes

  • The project uses a single container instance (max_instances: 1) to avoid duplicate scheduled jobs.
  • Disk is ephemeral; media must go to R2 (already configured by this repo).

Known issues

  • On container restart, the visitors see the "We’ll be right back." page. It does not auto redirect to the site. Regardless of your 'sleep after' setting, the container will eventually restart. Need to further test if caching helps.
  • Despite disabling caching on the admin site, the post editor has problems saving the post.
  • ActivityPub is showing "Loading interrupted" page.
  • Analytics isn't enabled.
  • SMTP configuration needs to be tested.

About

Single-container Ghost 6 blogging platform deployed on Cloudflare Workers with Containers. Worker forwards requests to port 2368. Uses external MySQL database (RDS with TLS) and Cloudflare R2 for media storage via S3 adapter. Includes optional ActivityPub support and SMTP configuration for notifications.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published