Every platform that hosts your writing also sits between you and your subscribers: a cut of every paid subscription, a policy that can change overnight, and an algorithm deciding who sees your next post. Ghost is the open-source publishing platform serious independent writers, from 404 Media to Platformer, run on their own infrastructure instead, with built-in paid memberships, newsletters, and a headless API if you ever want a custom frontend. This guide covers installing Ghost with Docker, configuring the two email paths it actually needs, and publishing from a domain you own with Localtonet.
Why Not Just Use Substack
Substack and Medium make starting free and starting fast, and that is exactly the trade. Substack keeps a percentage of every paid subscription on top of Stripe's own processing fee, and your entire publication lives on infrastructure you do not control, subject to policy changes you find out about after they happen.
Ghost flips that arrangement. You keep whatever Stripe's processing fee leaves you, because there is no second platform fee stacked on top, and your archive, your subscriber list, and your domain answer to you.
The writing does not change, only who gets paid for hosting it.
What Is Ghost?
Ghost is an open-source publishing platform under the MIT license, with over 50,000 GitHub stars. It combines a modern editor, built-in paid memberships through Stripe, and newsletter delivery in one tool, and its API doubles as a headless CMS if you ever want a fully custom frontend instead of a theme.
Ghost's own documentation lists 1 vCPU and 2GB of RAM as the minimum. If you expect a paid newsletter with thousands of subscribers rather than a small personal blog, plan for 2 vCPUs and 4GB instead.
Install Ghost With Docker Compose
services:
ghost:
image: ghost:5-alpine
restart: always
ports:
- "2368:2368"
environment:
database__client: mysql
database__connection__host: db
database__connection__user: root
database__connection__password: your_db_password
database__connection__database: ghost
url: https://blog.example.com
volumes:
- ghost_content:/var/lib/ghost/content
depends_on:
- db
db:
image: mysql:8
restart: always
environment:
MYSQL_ROOT_PASSWORD: your_db_password
MYSQL_DATABASE: ghost
volumes:
- ghost_db:/var/lib/mysql
volumes:
ghost_content:
ghost_db:
docker compose up -d
Open http://localhost:2368/ghost locally to complete the setup wizard and create your first admin account.
Publish From Your Own Domain With Localtonet
Ghost's own quickstart bundles a Caddy container just to get HTTPS in front of it. Since Localtonet already terminates HTTPS on a custom domain, the Compose file above skips that piece entirely.
Create an HTTP tunnel
Point it at 127.0.0.1:2368 and map a custom domain, such as blog.example.com.
Set the url config to match exactly
The url value in your Compose file must be the real public HTTPS address. Change it and recreate the Ghost container, Ghost does not pick up config changes on a simple restart.
Ghost's canonical links, sitemap, RSS feed, and Stripe webhook URLs are all generated from the url config value. Leave it pointing at localhost or an old domain and members can see broken links in emails, search engines index the wrong address, and Stripe webhooks silently fail to reach your instance.
Email: Two Different Jobs, Two Different Providers
Ghost splits email into two categories that are easy to conflate during setup. Transactional email, password resets, signup confirmations, works with any standard SMTP provider. Bulk newsletter delivery to your full subscriber list is built specifically around Mailgun's API, and that is the piece most self-hosted guides gloss over.
Configure SMTP for transactional email
Add your SMTP credentials to the environment section of your Compose file, any standard provider works here.
Connect Mailgun for newsletter sends
In the admin panel, go to Settings → Email newsletter → Mailgun configuration, and paste your Mailgun API key and sending domain. This is what lets a single newsletter reach a large subscriber list reliably.
Turn On Paid Memberships
In Settings → Membership, connect Stripe directly to your account. Use restricted API keys scoped to what Ghost actually needs rather than your full secret key. Ghost generates the checkout flow, the member portal, and content-gating rules from that one connection, no separate payment plugin required.
Migrating From Substack or Medium
Ghost's built-in importer, under Settings → Labs → Import, accepts the standard CSV export most platforms produce for subscriber lists. Subscriber data typically ports over cleanly, archived posts are worth a manual review afterward since formatting does not always survive the trip.
Keep It Running Permanently
Add restart: unless-stopped to every service in your Compose file so the stack survives a Docker restart on its own. To survive a full machine reboot, install the Localtonet client as a background service once with your AuthToken from Dashboard → My Tokens:
sudo localtonet --install-service --authtoken YOUR_TOKEN
sudo localtonet --start-service --authtoken YOUR_TOKEN
Only tunnels that were in Start state in the dashboard come back automatically, your booking domain included.
Self-Hosted Ghost vs. Substack vs. Medium
| Feature | Substack | Medium | Self-hosted Ghost |
|---|---|---|---|
| Platform cut of subscriptions | Percentage on top of Stripe fees | No direct paid subscriptions to your own account | None, only Stripe's own processing fee |
| Custom domain | Supported | Limited | Full control by default |
| Data and subscriber ownership | Exportable, but hosted on their infrastructure | Limited export options | Entirely yours, in your own database |
| Setup effort | None, sign up and write | None, sign up and write | An evening of Docker and email configuration |
The honest trade-off: Substack is genuinely the faster path to your first published post, there is no server to run and no email provider to configure. Ghost's advantage compounds over time, every paid subscriber you gain keeps more of what they pay, and no platform decision ever gets between you and your archive.
🛠 Tips for Running a Self-Hosted Publication
Troubleshooting Common Issues
| Symptom | Likely cause | Fix |
|---|---|---|
| Links in emails or the sitemap point to the wrong address | The url config still reflects an old or local address | Update url in your Compose file to the exact custom domain and recreate the container |
| Newsletter never arrives to subscribers | Mailgun is not configured, SMTP alone does not cover bulk sends | Add your Mailgun API key and domain under Settings → Email newsletter |
| Stripe checkout fails or webhooks never fire | The url config does not match what Stripe expects for webhook delivery | Confirm url is the live custom domain, then reconnect Stripe from Settings → Membership |
| Ghost container won't start after a config change | Environment variables only apply on container recreation, not a simple restart | Run docker compose up -d again after any .env or Compose file change |
Frequently Asked Questions
Does Ghost take a cut of my subscription revenue?
No, when self-hosted. Stripe connects directly to your own account, so you only pay Stripe's standard processing fee, there is no second platform percentage on top.
Why do I need both SMTP and Mailgun?
They handle different jobs. SMTP covers transactional email like password resets. Mailgun handles bulk newsletter delivery to your full subscriber list, which is built specifically around Mailgun's API for reliable delivery at scale.
Can I migrate my existing Substack subscribers?
Yes, Ghost's built-in importer accepts the standard CSV export most platforms provide. Subscriber lists generally port over cleanly, review archived posts afterward since formatting can shift.
Do I need to know how to code to use Ghost?
No, Ghost ships with themes and a full admin editor. Its API is there if you ever want a fully custom frontend, but it is entirely optional.
How much does self-hosting Ghost cost?
Ghost itself is free and open source. Your cost is whatever server it runs on, roughly the price of a small VPS if you are not already running suitable hardware, plus Mailgun's own pricing once your list grows past its free tier.
Does my server need to stay online for the newsletter to send?
Yes, both Ghost and the Localtonet client need to be running when you hit publish. Running both as background services keeps this from being a manual step every time.
Can I run more than one Ghost site on the same server?
Yes, run a separate Compose stack per site with its own database and content volume, then create a separate Localtonet tunnel and custom domain for each one.
Ready to Own Your Publication?
Deploy Ghost with Docker, connect Stripe and Mailgun, then publish from your own domain with Localtonet. Free to start, no credit card required.
Get Started Free →