Skip to main content

Self-Host Ghost: Blog CMS and Newsletter Platform 2026

·OSSAlt Team
ghostcmsnewsletterblogsubstackwordpressself-hostingdocker2026

TL;DR

Ghost (MIT, ~46K GitHub stars, Node.js) is a modern publishing platform combining a headless CMS, newsletter, and membership/payment system. It's the fastest path to a professional publication. Substack takes 10% of revenue. Ghost self-hosted takes 0% — you bring your own email (Mailgun/Postmark/SES) and payment processor (Stripe). Ghost(Pro) starts at $9/month; self-hosted is free.

Key Takeaways

  • Ghost: MIT, ~46K stars, Node.js — blog CMS + newsletter + memberships in one
  • Built-in newsletter: Send via SMTP — Mailgun, Postmark, or SES
  • Members and paid subscriptions: Stripe integration for paid newsletters/memberships
  • Headless CMS: REST API + GraphQL for decoupled frontends
  • Performance: ~100ms page loads with built-in image optimization and lazy loading
  • Themes: 100+ free themes, or build your own with Handlebars

Ghost vs WordPress vs Substack

FeatureGhost (self-hosted)WordPressSubstack
LicenseMITGPL 2.0Proprietary
CostFree (hosting + SMTP)Free (hosting)Free (10% cut on paid)
Newsletter built-inYesPlugin neededYes
MembershipsYes (Stripe)Plugin neededYes (10% cut)
Revenue cut0%0%10%
CMS editorModern (Koenig)GutenbergBasic
API-firstYesYesNo
Setup complexityMediumMediumZero
GitHub Stars~46K~20K

Part 1: Docker Setup

# docker-compose.yml
services:
  ghost:
    image: ghost:latest
    container_name: ghost
    restart: unless-stopped
    ports:
      - "2368:2368"
    volumes:
      - ghost_content:/var/lib/ghost/content
    environment:
      url: "https://blog.yourdomain.com"
      database__client: mysql
      database__connection__host: mysql
      database__connection__user: ghost
      database__connection__password: "${MYSQL_PASSWORD}"
      database__connection__database: ghost
      mail__transport: SMTP
      mail__options__host: "smtp.mailgun.org"
      mail__options__port: "587"
      mail__options__auth__user: "postmaster@mg.yourdomain.com"
      mail__options__auth__pass: "${MAILGUN_PASSWORD}"
      mail__from: "noreply@yourdomain.com"
    depends_on:
      - mysql

  mysql:
    image: mysql:8.0
    container_name: ghost-mysql
    restart: unless-stopped
    volumes:
      - mysql_data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD}"
      MYSQL_DATABASE: ghost
      MYSQL_USER: ghost
      MYSQL_PASSWORD: "${MYSQL_PASSWORD}"

volumes:
  ghost_content:
  mysql_data:
docker compose up -d

Visit https://blog.yourdomain.com/ghost → setup wizard.


Part 2: HTTPS with Caddy

blog.yourdomain.com {
    reverse_proxy localhost:2368
}

Part 3: Initial Setup

  1. Visit https://blog.yourdomain.com/ghost
  2. Create admin account (email + password)
  3. Set up your publication:
    • Name: "My Blog"
    • Description: "Writing about tech and startups"
    • Logo and cover image

Part 4: Write and Publish

New Post → Koenig Editor:

Title → Click to write
Subtitle (optional)
Body → Start writing...

Drag-and-drop images
/gallery — image gallery
/code — code block with syntax highlighting
/html — raw HTML embed
/video — video embed
/bookmark — URL card preview
/callout — highlighted callout box

Post Settings (sidebar):

  • URL slug
  • Publish date (schedule for later)
  • Author
  • Tags
  • SEO: custom meta title/description
  • Feature image

Part 5: Newsletter Setup

Ghost sends newsletters to your member list via SMTP:

Settings → Email newsletter:

  • Confirm SMTP settings (configured in docker-compose.yml)
  • From name: "Your Name"
  • Reply-to: your email

Send newsletter:

  1. Write a post
  2. Toggle Email newsletter in the sidebar
  3. Select which members receive it: Free, Paid, All
  4. Publish sends the post + email to subscribers simultaneously

Part 6: Members and Paid Subscriptions

Set up free and paid memberships:

Settings → Members → Enable Members

Free tier: Anyone can sign up for email updates

Paid tier (Stripe):

  1. Settings → Stripe → Connect Stripe account
  2. Settings → Tiers → Create paid tier
    • Name: "Supporter" or "Premium"
    • Price: $5/month or $50/year
  3. Subscribers at paid tier get access to premium content

Gate content: In the post editor → Visibility:

  • Public: Everyone (logged in or not)
  • Members only: Free subscribers
  • Paid members only: Paying subscribers

Part 7: Themes

Change your blog's appearance:

Settings → Design → Change Theme → Upload ZIP

Popular free themes:

  • Casper: Default, clean and minimal
  • Edition: Newsletter-focused
  • Source: Developer-friendly, fast
  • Download from Ghost Marketplace

Customize Without Code

Settings → Design → Site-wide:

  • Navigation menu
  • Colors and fonts (theme-specific)
  • Homepage layout
  • Sidebar widgets

Part 8: Headless CMS (API Usage)

Use Ghost as a headless CMS with your own frontend:

# Content API — public, no auth required:
curl "https://blog.yourdomain.com/ghost/api/content/posts/?key=YOUR_CONTENT_API_KEY"

# Get posts:
curl "https://blog.yourdomain.com/ghost/api/content/posts/?key=KEY&include=tags,authors&limit=10"

# Get single post:
curl "https://blog.yourdomain.com/ghost/api/content/posts/slug/my-post-slug/?key=KEY"
// Next.js example:
import GhostContentAPI from '@tryghost/content-api'

const api = new GhostContentAPI({
  url: 'https://blog.yourdomain.com',
  key: process.env.GHOST_CONTENT_API_KEY,
  version: "v5.0"
})

export async function getPosts() {
  return await api.posts.browse({
    include: ['tags', 'authors'],
    limit: 'all'
  })
}

Part 9: Custom Domain Email

Use your own domain for newsletter sending:

Mailgun setup:

  1. Sign up at mailgun.com → Add sending domain mg.yourdomain.com
  2. Add DNS records: MX, TXT (SPF), CNAME (DKIM)
  3. In Ghost config: MAILGUN_API_KEY + MAILGUN_DOMAIN

Or use Postmark for better deliverability:

environment:
  mail__options__host: "smtp.postmarkapp.com"
  mail__options__port: "587"
  mail__options__auth__user: "${POSTMARK_API_TOKEN}"
  mail__options__auth__pass: "${POSTMARK_API_TOKEN}"

Maintenance

# Update Ghost:
docker compose pull
docker compose up -d

# Backup:
# Ghost content (images, themes):
tar -czf ghost-content-$(date +%Y%m%d).tar.gz \
  $(docker volume inspect ghost_ghost_content --format '{{.Mountpoint}}')

# MySQL database:
docker exec ghost-mysql mysqldump -u ghost -p${MYSQL_PASSWORD} ghost | gzip \
  > ghost-db-$(date +%Y%m%d).sql.gz

# Logs:
docker compose logs -f ghost

# Ghost CLI (if needed):
docker exec ghost ghost doctor
docker exec ghost ghost update

See all open source CMS and publishing tools at OSSAlt.com/alternatives/wordpress.

Comments