
Should I self-host Ghost(Pro)?
YESworth it
It's the same Ghost. $18/mo buys hosting, email infrastructure and zero ops — $216/yr buys a lot of newsletters, and the software is right there.
The math (nothing hidden, including your time)
| Ghost(Pro) (Starter) | $18.00/mo |
| VPS share + storage | −$1.50/mo |
| Your maintenance: 20 min/mo at $20/h | −$6.67/mo |
| Net saving | $9.83/mo |
| Setup: 50 min measured (one-time) | $16.67 |
| Break-even | 1.7 months |
| Markup Index (price ÷ real self-host cost) | 2.2× |
Price: source, checked 2026-08-05. Inputs are stored in git; every number above is derived, never hand-written.
What you lose
- Ghost(Pro)'s bundled newsletter deliverability — bring Mailgun for bulk sends
- Automatic upgrades from the people who wrote it (and fund it — Pro revenue pays for the software you're running)
What you're paying Ghost(Pro) for
- publishing/blog
- newsletter sending
- memberships/paid subs
- themes

Ranked alternatives
The same publishing platform Ghost(Pro) hosts, on your own box, with the editor, the themes and the members list intact.
Hardware
Runs comfortably on a real VPS (2 GB+).
The tested compose file (our evidence, CI-booted)
# Ghost — the same software Ghost(Pro) hosts for $18/mo (Starter)
services:
ghost:
image: ghost:5-alpine
container_name: ghost
restart: unless-stopped
depends_on:
ghost-db:
condition: service_healthy
environment:
url: http://localhost:2368
# Ghost exits with cryptic "Code: -97" (EAFNOSUPPORT) where IPv6 is disabled; bind IPv4.
server__host: 0.0.0.0
database__client: mysql
database__connection__host: ghost-db
database__connection__user: ghost
database__connection__password: ghost
database__connection__database: ghost
ports:
- "2368:2368"
volumes:
- ghost-content:/var/lib/ghost/content
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:2368 >/dev/null || exit 1"]
interval: 5s
timeout: 5s
retries: 36
start_period: 30s
ghost-db:
image: mysql:8
restart: unless-stopped
environment:
MYSQL_USER: ghost
MYSQL_PASSWORD: ghost
MYSQL_DATABASE: ghost
MYSQL_ROOT_PASSWORD: root-changeme # throwaway test value
volumes:
- ghost-db:/var/lib/mysql
healthcheck:
# Must probe over TCP: during first-boot initialization MySQL runs a temp server that
# answers the *socket* ping while port 3306 is still down — a socket-based check lets
# dependents start too early and crash-loop on ECONNREFUSED.
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "--protocol=tcp", "-ughost", "-pghost"]
interval: 5s
timeout: 5s
retries: 36
start_period: 30s
volumes:
ghost-content:
ghost-db:
Want a guided install instead? caniselfhostit.com/ghost-pro has AI-agent prompts that assume a bare machine — that's their half of the stool, and it's good.
Verification
Protocol v1 · verified by zernonia on 2026-08-10 · setup measured at 50 min · AI assistant: claude-code (allowed & stated)
What broke
- Where IPv6 is disabled Ghost dies with the deeply unhelpful 'Code: -97' — it's EAFNOSUPPORT; set server__host: 0.0.0.0
- MySQL 8's first-boot init answers socket pings while TCP is still down, so a socket-based healthcheck releases Ghost too early into an ECONNREFUSED crash-loop — probe with --protocol=tcp
- Ghost 5 requires MySQL 8 specifically — SQLite and MariaDB are not supported paths
Full timed log
# Timed setup log: Ghost(Pro) → ghost **Protocol:** v1 · **Verified by:** zernonia · **Date:** 2026-08-10 **Assistant:** claude-code · **Environment:** containerized runner, 2 vCPU class, Docker 29.3 / Compose v5.1 ## Timeline | Step | Time | |---|---| | Read upstream docs, draft compose with healthcheck | ~16 min | | **Boot: all services healthy in 22 s (measured, `compose up --wait`)** | — | | Endpoint-level workflow check (login/health/API serve) + re-run from clean volumes | ~16 min | | **Total: ~50 min** | | Boot and endpoint checks are machine-verified; `setup_min` is the wall-clock total for this session including authoring, diagnosis and re-runs. Endpoint-level ≠ full human UI workflow — dispute anything that doesn't reproduce (CONTRIBUTING.md). ## What broke - Where IPv6 is disabled Ghost dies with the deeply unhelpful 'Code: -97' — it's EAFNOSUPPORT; set server__host: 0.0.0.0 - MySQL 8's first-boot init answers socket pings while TCP is still down, so a socket-based healthcheck releases Ghost too early into an ECONNREFUSED crash-loop — probe with --protocol=tcp - Ghost 5 requires MySQL 8 specifically — SQLite and MariaDB are not supported paths ## Verdict-relevant notes - Memberships + Stripe work self-hosted; bulk newsletter needs a Mailgun key.
Verdict history
- 2026-08-10: unscored → YES — Initial verdict: It's the same Ghost. $18/mo buys hosting, email infrastructure and zero ops.