Ghost is an open-source publishing platform and Substack alternative with native newsletters, membership tiers, and full control over your MySQL data.
Ghost is an open-source publishing platform designed for blogs, newsletters, and paid memberships. Its core architecture bundles member authentication, subscription paywalls, and email delivery directly into the application rather than relying on external plugins. Homelab operators deploy it as a Node.js daemon paired with MySQL, keeping complete custody of subscriber lists and content archives.
Substack manages your publication and subscriber billing behind a hosted dashboard, collecting a ten percent cut on subscription revenue. Ghost replaces that pipeline with a self-hosted application connecting directly to Stripe with zero platform transaction fees. It matches Substack's primary publishing features—gated posts, member tiers, and broadcast email delivery—while supporting custom Handlebars themes and headless frontends. However, Substack provides an internal recommendation network that drives audience discovery, whereas Ghost includes no shared reader feed, leaving audience acquisition to your own channels.
The operational shift is substantial. Substack requires zero infrastructure maintenance, while self-hosting Ghost entails running a Node.js container, operating a dedicated MySQL 8 database, and managing automated TLS behind a reverse proxy. Furthermore, sending newsletters at scale requires Mailgun API credentials because standard SMTP cannot handle bulk delivery reliably. Migrating subscribers and archives from Substack is straightforward via CSV and JSON imports, but ongoing server maintenance, database backups, and software updates become your responsibility.
Can I self-host Ghost for free? Yes. The software is free to self-host, with operating expenses limited to your server infrastructure, domain registration, and Mailgun email delivery fees.
Ghost vs Substack — which is better for independent writers? Substack suits writers wanting zero maintenance and built-in audience discovery, while Ghost is better for publishers seeking custom branding, full data ownership, and zero platform cuts on subscriptions.
Does Ghost support PostgreSQL or SQLite in production? No. Ghost officially requires MySQL 8 for production deployments; SQLite is supported only for local testing, and PostgreSQL is not officially supported.
Ghost is the right choice for administrators who can manage a Node.js and MySQL stack to gain full data ownership and avoid subscription revenue cuts; it is ill-suited for writers who need automated platform discovery without managing servers. Actively maintained with frequent updates and an MIT license that permits full customization or embedded commercial deployments, it provides a dependable foundation for independent publishing.
We started this exact setup from scratch in a clean Linux machine on Nov 1, 2026 and checked it through Ghost's own API: every check passed. Recipes are re-checked regularly and removed if they stop working.
mkdir -p ghost && cd ghost
curl -fsSL https://open-source.store/tools/ghost/compose.yml -o compose.ymlcat > .env <<EOF
GHOST_URL=http://localhost:2368
DATABASE_ROOT_PASSWORD=$(openssl rand -hex 12)
DATABASE_PASSWORD=$(openssl rand -hex 12)
STAFF_DEVICE_VERIFICATION=false
ADMIN_PASSWORD=$(openssl rand -hex 12)
EOFGHOST_URL — Public URL where Ghost is accessed (e.g. https://<your-server> or http://<your-server>:2368); change this to your public domain so links, emails, and assets resolve correctly.DATABASE_ROOT_PASSWORD — Root password of the bundled MySQL database. Generated above; keep it secret.DATABASE_PASSWORD — Password Ghost uses for its MySQL database. Generated above; keep it secret.STAFF_DEVICE_VERIFICATION — Email verification codes for staff logins from new devices; set to true once SMTP (mail__* variables) is configured.ADMIN_PASSWORD — Password used only by verification smoke steps to complete the initial setup wizard; it is not read by Ghost or compose.yml and is not the login credentials, which the operator chooses directly in the web setup wizard. Generated above; keep it secret.docker compose up -dservices:
ghost:
image: ghost:6.73.0-alpine
restart: always
env_file:
- .env
ports:
- "2368:2368"
environment:
NODE_ENV: production
url: ${GHOST_URL}
database__client: mysql
database__connection__host: db
database__connection__user: ghost
database__connection__password: ${DATABASE_PASSWORD}
database__connection__database: ghost
security__staffDeviceVerification: ${STAFF_DEVICE_VERIFICATION}
volumes:
- ./data/ghost:/var/lib/ghost/content
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "wget -q -O /dev/null http://localhost:2368/ghost/api/admin/site/ || exit 1"]
interval: 10s
timeout: 5s
retries: 5
start_period: 60s
networks:
- ghost_network
db:
image: mysql:8.0.44
restart: always
expose:
- "3306"
environment:
MYSQL_ROOT_PASSWORD: ${DATABASE_ROOT_PASSWORD}
MYSQL_USER: ghost
MYSQL_PASSWORD: ${DATABASE_PASSWORD}
MYSQL_DATABASE: ghost
volumes:
- ./data/mysql:/var/lib/mysql
healthcheck:
test: mysqladmin ping -p$$MYSQL_ROOT_PASSWORD -h 127.0.0.1
interval: 1s
start_period: 30s
start_interval: 10s
retries: 120
networks:
- ghost_network
networks:
ghost_network:
Free IDE for Java, Kotlin, Go, TypeScript and Python. Built on IntelliJ Community, fully open source, 300+ plugins.
Show that Ghost is listed as an open-source alternative. Paste into your README:
Ubuntu 24.04 · Apple container, Docker 29.1.3, Compose 2.40.3+ds1-0ubuntu1~24.04.1. Images pulled in 96 s: mysql:8.0.44, ghost:6.73.0-alpine.
Based on the upstream setup. Official ghost-docker compose reduced to Ghost and MySQL: the Caddy TLS proxy (needs its own Caddyfile and a domain) and the optional analytics/ActivityPub profiles are left out, Ghost is published on 2368, images pinned to 6.73.0-alpine and mysql:8.0.44, a healthcheck added, and staff device verification switched off until SMTP is configured.
ghost-1 | 2026-11-01T20:04:37.769097135Z [2026-11-01 20:04:37] INFO "GET /wp-admin/admin-ajax.php?action=lwp_forgot_password&ID=<svg%20onload=alert(document.domain)>" 404 9ms ghost-1 | 2026-11-01T20:04:37.769236968Z [2026-11-01 20:04:37] INFO "GET /wp-admin/admin-ajax.php?action=lwp_forgot_password&ID=<svg%20onload=alert(document.domain)>" 404 9ms ghost-1 | 2026-11-01T20:04:37.769386468Z [2026-11-01 20:04:37] INFO "GET /bin/querybuilder.json.;%0aa.css?path=/etc&p.hits=full&p.limit=-1" 404 9ms ghost-1 | 2026-11-01T20:04:37.772308593Z [2026-11-01 20:04:37] ERROR URL empty or invalid. ghost-1 | 2026-11-01T20:04:37.772318593Z ghost-1 | 2026-11-01T20:04:37.772319177Z URL empty or invalid. ghost-1 | 2026-11-01T20:04:37.772319718Z ghost-1 | 2026-11-01T20:04:37.772320218Z "http://localhost:2368/favicon.ico" ghost-1 | 2026-11-01T20:04:37.772320802Z ghost-1 | 2026-11-01T20:04:37.772321260Z Error ID: ghost-1 | 2026-11-01T20:04:37.772321802Z ff0d23f3-d7f3-44d4-b6e9-cd710e1d5161 ghost-1 | 2026-11-01T20:04:37.772322343Z ghost-1 | 2026-11-01T20:04:37.772322760Z Error Code: ghost-1 | 2026-11-01T20:04:37.772323302Z IMAGE_SIZE_URL ghost-1 | 2026-11-01T20:04:37.772323802Z ghost-1 | 2026-11-01T20:04:37.772324260Z ---------------------------------------- ghost-1 | 2026-11-01T20:04:37.772324760Z ghost-1 | 2026-11-01T20:04:37.772325218Z InternalServerError: URL empty or invalid. ghost-1 | 2026-11-01T20:04:37.772325760Z at /var/lib/ghost/versions/6.73.0/core/server/lib/image/image-size.js:214:13 ghost-1 | 2026-11-01T20:04:37.772326302Z at runNextTicks (node:internal/process/task_queues:64:5) ghost-1 | 2026-11-01T20:04:37.772326802Z at process.processImmediate (node:internal/timers:452:9) ghost-1 | 2026-11-01T20:04:37.772327343Z at async CachedImageSizeFromUrl.getCachedImageSizeFromUrl (/var/lib/ghost/versions/6.73.0/core/server/lib/image/cached-image-size-from-url.js:68:19) ghost-1 | 2026-11-01T20:04:37.772327927Z at async Promise.all (index 4) ghost-1 | 2026-11-01T20:04:37.772328468Z at async getImageDimensions (/var/lib/ghost/versions/6.73.0/core/frontend/meta/image-dimensions.js:25:66) ghost-1 | 2026-11-01T20:04:37.772329052Z at async Object.ghost_head (/var/lib/ghost/versions/6.73.0/core/frontend/helpers/ghost_head.js:392:18) ghost-1 | 2026-11-01T20:04:37.772329635Z at async Object.returnAsync (/var/lib/ghost/versions/6.73.0/core/frontend/services/helpers/handlebars.js:16:24) ghost-1 | 2026-11-01T20:04:37.772330218Z ghost-1 | 2026-11-01T20:04:37.772790802Z [2026-11-01 20:04:37] INFO "GET /bin/querybuilder.json.css?path=/etc&p.hits=full&p.limit=-1" 404 7ms ghost-1 | 2026-11-01T20:04:37.772874635Z [2026-11-01 20:04:37] INFO "GET /.config/gcloud/access_tokens.db" 404 7ms ghost-1 | 2026-11-01T20:04:37.773008552Z [2026-11-01 20:04:37] INFO "GET /.config/gcloud/access_tokens.db" 404 7ms ghost-1 | 2026-11-01T20:04:37.773123218Z [2026-11-01 20:04:37] INFO "GET /cgi-bin/awstats/awredir.pl?url=%3Cscript%3Ealert(document.domain)%3C/script%3E" 404 8ms ghost-1 | 2026-11-01T20:04:37.773248635Z [2026-11-01 20:04:37] INFO "GET /static/shards.html" 404 8ms ghost-1 | 2026-11-01T20:04:37.774216635Z [2026-11-01 20:04:37] INFO "GET /config" 301 5ms ghost-1 | 2026-11-01T20:04:37.774477635Z [2026-11-01 20:04:37] INFO "GET /config" 301 5ms ghost-1 | 2026-11-01T20:04:37.777666760Z [2026-11-01 20:04:37] ERROR URL empty or invalid. ghost-1 | 2026-11-01T20:04:37.777674219Z ghost-1 | 2026-11-01T20:04:37.777674969Z URL empty or invalid. ghost-1 | 2026-11-01T20:04:37.777675552Z ghost-1 | 2026-11-01T20:04:37.777676094Z "http://localhost:2368/favicon.ico" ghost-1 | 2026-11-01T20:04:37.777676719Z ghost-1 | 2026-11-01T20:04:37.777677219Z Error ID: ghost-1 | 2026-11-01T20:04:37.777677927Z 9121da54-bbf1-4412-a35d-5e150abbb33f ghost-1 | 2026-11-01T20:04:37.777678510Z ghost-1 | 2026-11-01T20:04:37.777678969Z Error Code: ghost-1 | 2026-11-01T20:04:37.777679510Z IMAGE_SIZE_URL ghost-1 | 2026-11-01T20:04:37.777680010Z ghost-1 | 2026-11-01T20:04:37.777680469Z ---------------------------------------- ghost-1 | 2026-11-01T20:04:37.777681010Z ghost-1 | 2026-11-01T20:04:37.777681469Z InternalServerError: URL empty or invalid. ghost-1 | 2026-11-01T20:04:37.777682010Z at /var/lib/ghost/versions/6.73.0/core/server/lib/image/image-size.js:214:13 ghost-1 | 2026-11-01T20:04:37.777682594Z at runNextTicks (node:internal/process/task_queues:64:5) ghost-1 | 2026-11-01T20:04:37.777683094Z at process.processImmediate (node:internal/timers:452:9) ghost-1 | 2026-11-01T20:04:37.777683635Z ghost-1 | 2026-11-01T20:04:37.777684094Z [2026-11-01 20:04:37] ERROR URL empty or invalid. ghost-1 | 2026-11-01T20:04:37.777684635Z ghost-1 | 2026-11-01T20:04:37.777685094Z URL empty or invalid. ghost-1 | 2026-11-01T20:04:37.777685594Z ghost-1 | 2026-11-01T20:04:37.777686052Z "http://localhost:2368/favicon.ico" ghost-1 | 2026-11-01T20:04:37.777686594Z ghost-1 | 2026-11-01T20:04:37.777687010Z Error ID: ghost-1 | 2026-11-01T20:04:37.777687552Z 9121da54-bbf1-4412-a35d-5e150abbb33f ghost-1 | 2026-11-01T20:04:37.777688052Z ghost-1 | 2026-11-01T20:04:37.777688510Z Error Code: ghost-1 | 2026-11-01T20:04:37.777689010Z IMAGE_SIZE_URL ghost-1 | 2026-11-01T20:04:37.777693427Z ghost-1 | 2026-11-01T20:04:37.777693927Z ---------------------------------------- ghost-1 | 2026-11-01T20:04:37.777694510Z ghost-1 | 2026-11-01T20:04:37.777694927Z InternalServerError: URL empty or invalid. ghost-1 | 2026-11-01T20:04:37.777695802Z at /var/lib/ghost/versions/6.73.0/core/server/lib/image/image-size.js:214:13 ghost-1 | 2026-11-01T20:04:37.777696385Z at runNextTicks (node:internal/process/task_queues:64:5) ghost-1 | 2026-11-01T20:04:37.777708969Z at process.processImmediate (node:internal/timers:452:9) ghost-1 | 2026-11-01T20:04:37.777713469Z ghost-1 | 2026-11-01T20:04:37.778328469Z [2026-11-01 20:04:37] INFO "GET /index.php?option=com_smestorage&controller=../../../../../../../../../etc/passwd%00" 404 9ms ghost-1 | 2026-11-01T20:04:37.778736885Z [2026-11-01 20:04:37] INFO "GET /index.php?option=com_smestorage&controller=../../../../../../../../../etc/passwd%00" 404 9ms ghost-1 | 2026-11-01T20:04:37.779096885Z [2026-11-01 20:04:37] INFO "GET /static/shards.html" 404 10ms ghost-1 | 2026-11-01T20:04:37.782323177Z [2026-11-01 20:04:37] INFO "GET /js/../.htpasswd" 301 9ms ghost-1 | 2026-11-01T20:04:37.784002302Z [2026-11-01 20:04:37] INFO "GET /static/shards/html" 301 2ms ghost-1 | 2026-11-01T20:04:37.784267552Z [2026-11-01 20:04:37] INFO "GET /js/../.htpasswd" 301 3ms ghost-1 | 2026-11-01T20:04:37.784915469Z [2026-11-01 20:04:37] INFO "GET /static/shards/html" 301 1ms ghost-1 | 2026-11-01T20:04:37.788408802Z [2026-11-01 20:04:37] INFO "GET /bin/querybuilder.json.css?path=/home&p.hits=full&p.limit=-1" 404 15ms ghost-1 | 2026-11-01T20:04:37.788830094Z [2026-11-01 20:04:37] INFO "GET /src/read_body.php?mailbox=/etc/passwd&passed_id=1" 404 5ms ghost-1 | 2026-11-01T20:04:37.791967885Z [2026-11-01 20:04:37] INFO "GET /" 200 44ms ghost-1 | 2026-11-01T20:04:37.792322802Z [2026-11-01 20:04:37] INFO "GET /src/read_body.php?mailbox=/etc/passwd&passed_id=1" 404 8ms ghost-1 | 2026-11-01T20:04:37.792873635Z [2026-11-01 20:04:37] INFO "GET /sftp-config.json" 301 4ms ghost-1 | 2026-11-01T20:04:37.794083594Z [2026-11-01 20:04:37] INFO "GET /?query=SHOW%20DATABASES" 200 43ms ghost-1 | 2026-11-01T20:04:37.794183677Z [2026-11-01 20:04:37] INFO "GET /" 200 45ms ghost-1 | 2026-11-01T20:04:37.794185385Z [2026-11-01 20:04:37] INFO "GET /?query=SHOW%20DATABASES" 200 44ms ghost-1 | 2026-11-01T20:04:37.794357719Z [2026-11-01 20:04:37] INFO "GET /" 200 40ms ghost-1 | 2026-11-01T20:04:37.794882719Z [2026-11-01 20:04:37] INFO "GET /sftp-config.json" 301 2ms ghost-1 | 2026-11-01T20:04:37.795015802Z [2026-11-01 20:04:37] INFO "GET /bin/querybuilder.json.css?path=/etc&p.hits=full&p.limit=-1" 404 2ms ghost-1 | 2026-11-01T20:04:37.795421219Z [2026-11-01 20:04:37] INFO "GET /admin" 301 1ms ghost-1 | 2026-11-01T20:04:37.795585594Z [2026-11-01 20:04:37] INFO "GET /src/download.php?absolute_dl=true&passed_id=1&passed_ent_id=1&mailbox=/etc/passwd" 404 3ms ghost-1 | 2026-11-01T20:04:37.795752802Z [2026-11-01 20:04:37] INFO "GET /" 200 41ms ghost-1 | 2026-11-01T20:04:37.795839135Z [2026-11-01 20:04:37] INFO "GET /src/download.php?absolute_dl=true&passed_id=1&passed_ent_id=1&mailbox=/etc/passwd" 404 1ms ghost-1 | 2026-11-01T20:04:37.795993260Z [2026-11-01 20:04:37] INFO "GET /ftpsync.settings" 404 1ms ghost-1 | 2026-11-01T20:04:37.797157427Z [2026-11-01 20:04:37] INFO "GET /admin" 301 2ms ghost-1 | 2026-11-01T20:04:37.798497052Z [2026-11-01 20:04:37] INFO "GET /config.json" 301 1ms ghost-1 | 2026-11-01T20:04:37.798739969Z [2026-11-01 20:04:37] INFO "GET /ftpsync.settings" 404 2ms ghost-1 | 2026-11-01T20:04:37.798884844Z [2026-11-01 20:04:37] INFO "GET /view/action/download_file.php?filename=../../../../../../../../../etc/passwd&savename=kzxpf.txt" 404 2ms ghost-1 | 2026-11-01T20:04:37.799764802Z [2026-11-01 20:04:37] INFO "GET /html/portlet/ext/common/page_preview_popup.jsp?hostname=oast.me" 404 2ms ghost-1 | 2026-11-01T20:04:37.801834344Z [2026-11-01 20:04:37] INFO "GET /cache/backup/" 404 2ms ghost-1 | 2026-11-01T20:04:37.802070677Z [2026-11-01 20:04:37] INFO "GET /.config/gcloud/credentials.db" 404 2ms ghost-1 | 2026-11-01T20:04:37.802449844Z [2026-11-01 20:04:37] INFO "GET /eos/" 404 7ms ghost-1 | 2026-11-01T20:04:37.802545344Z [2026-11-01 20:04:37] INFO "GET /cache/backup/" 404 3ms ghost-1 | 2026-11-01T20:04:37.802802927Z [2026-11-01 20:04:37] INFO "GET /index.php?lvl=author_see&id=42691%27%3E%3Cscript%3Ealert%28document.domain%29%3C%2Fscript%3E" 404 1ms ghost-1 | 2026-11-01T20:04:37.803100719Z [2026-11-01 20:04:37] INFO "GET /eos/" 404 6ms ghost-1 | 2026-11-01T20:04:37.803307594Z [2026-11-01 20:04:37] INFO "GET /index.php?lvl=author_see&id=42691%27%3E%3Cscript%3Ealert%28document.domain%29%3C%2Fscript%3E" 404 1ms ghost-1 | 2026-11-01T20:04:37.803664094Z [2026-11-01 20:04:37] INFO "GET /cgi-bin/ExportLogs.sh" 404 1ms ghost-1 | 2026-11-01T20:04:37.803933552Z [2026-11-01 20:04:37] INFO "GET /cgi-bin/ExportLogs.sh" 404 0ms ghost-1 | 2026-11-01T20:04:37.804281635Z [2026-11-01 20:04:37] INFO "GET /secrets.yml" 404 1ms ghost-1 | 2026-11-01T20:04:37.804807635Z [2026-11-01 20:04:37] INFO "GET /secrets.yml" 404 1ms ghost-1 | 2026-11-01T20:04:37.805369010Z [2026-11-01 20:04:37] INFO "GET /cs/career/getSurvey.jsp?fn=../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../etc/passwd" 404 1ms ghost-1 | 2026-11-01T20:04:37.805846594Z [2026-11-01 20:04:37] INFO "GET /config/secrets.yml" 404 1ms ghost-1 | 2026-11-01T20:04:37.806030385Z [2026-11-01 20:04:37] INFO "GET /cs/career/getSurvey.jsp?fn=../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../etc/passwd" 404 1ms ghost-1 | 2026-11-01T20:04:37.806230635Z [2026-11-01 20:04:37] INFO "GET /config/secrets.yml" 404 1ms ghost-1 | 2026-11-01T20:04:37.806337635Z [2026-11-01 20:04:37] INFO "GET /Business/DownLoad.aspx?p=UploadFile/../Web.Config" 404 1ms ghost-1 | 2026-11-01T20:04:37.806759635Z [2026-11-01 20:04:37] INFO "GET /test/config/secrets.yml" 404 0ms ghost-1 | 2026-11-01T20:04:37.806880719Z [2026-11-01 20:04:37] INFO "GET /test/config/secrets.yml" 404 0ms ghost-1 | 2026-11-01T20:04:37.807181385Z [2026-11-01 20:04:37] INFO "GET /redmine/config/secrets.yml" 404 1ms ghost-1 | 2026-11-01T20:04:37.807831594Z [2026-11-01 20:04:37] INFO "GET /redmine/config/secrets.yml" 404 1ms