Why leave Ghost(Pro), and what you leave behind
Ghost(Pro) pricing is indexed on audience: each plan caps the number of members, and Ghost asks you to upgrade as the publication grows. For a solo publisher that is a readable cost. For an agency running the newsletter or media site of five clients, it is five subscriptions, each following its own curve. The entry plan also excludes custom themes and custom integrations.
Self-hosting solves both: one server carries several publications, and you install whatever theme and integrations you need.
Be honest about the other column. Ghost(Pro) includes newsletter delivery with deliverability already configured, updates, backups and vendor support. When you leave, each of those tasks becomes yours. It is not an insurmountable project, but it is a real cost, to be priced before the decision rather than discovered afterwards.
What you gain by leaving Ghost(Pro)
- A cost that no longer tracks audience — the server is sized on traffic and image volume, not on the number of signed-up members.
- Several publications on one machine — an agency groups its Ghost instances on one or two VPS instead of stacking subscriptions.
- Themes and integrations with no plan restriction — custom theme, webhooks, analytics or search tools wired straight into the server.
- Control over the data — the MySQL database, the
contentfolder and the backups are yours; you choose where they live and how long they are kept. - The same format end to end — from Ghost to Ghost the import is native: no converter, no rewriting of posts.
- An update schedule you choose — you test a new release on a copy before applying it to the live publication.
What changes, item by item
Scroll the table
| Item | Ghost(Pro) | Self-hosted Ghost |
|---|---|---|
| Pricing | Per plan, capped by member count | Server price, whatever the member count |
| Newsletter delivery | Included, no setup | Mailgun account required, billed separately |
| Member sign-in emails | Included | SMTP server to configure |
| Ghost updates | Applied by the vendor | Your responsibility |
| Backups | Handled by the vendor | MySQL database and `content` folder to back up yourself |
| Custom theme | Depends on the plan | Always possible |
| Commission on subscriptions | None from Ghost on paid plans | None; only Stripe fees apply |
Sized prerequisites before touching Ghost(Pro)
Server: plan 2 vCPU and 2 GB of RAM for one publication, 4 GB if you group two or three instances or if the list exceeds a few tens of thousands of members. Size the disk on your media: measure the image folder, double it for local backups, and add 10 GB for the system.
Database: MySQL 8, not MariaDB. Ghost's official documentation is unambiguous: MySQL 8 is the only supported database in production, and Ghost recommends moving off MariaDB. The engine does not affect the JSON export, but the target instance must run on MySQL 8.
Version: the target instance must be the same version as Ghost(Pro) or newer. Note the version shown in Ghost Admin before installing anything.
Accounts and access: owner access to the Stripe account connected to the publication, a Mailgun account for newsletters, an SMTP relay for sign-in emails, and access to the domain's DNS zone.
The installation itself is not repeated here: follow our guide to installing Ghost on a VPS, then come back to step 5.
The migration in ten steps, from inventory to cutover
Record the baseline
Before any export, write down what the migration must find again: number of published posts, pages, free, paid and complimentary members (filters in the Ghost Admin Members view), Ghost version and the list of sitemap URLs. Keep that list in a file:
curl -s https://your-domain.com/sitemap-posts.xml | grep -o '<loc>[^<]*' | sed 's/<loc>//' > urls-before.txt wc -l urls-before.txtLower the domain TTL
In the DNS zone, bring the TTL of the publication's records down to 300 seconds, at least 24 hours before the cutover. That is what lets you switch IP address in minutes on the day, and switch back just as fast.
Export all content
In Ghost Admin, open Settings → Advanced → Import/Export, Export tab, and choose All data. The archive contains
export.json(content and settings),members.csv, the installed themes as.zip,routes.yamlandredirects.yaml. Check that each file is there before moving on.Recover images and media
Ghost's documentation is clear: images are not in the export. On Ghost(Pro) you have no server access, so there are two routes. Ask Ghost(Pro) support for an archive of the
content/imagesfolder, or rebuild it from the public URLs found in the export:grep -o '/content/images/[^"\\]*' export.json | sort -u > images.txt wc -l images.txtThen download each file while keeping the
content/images/…tree: Ghost's importer expects that path.Install the target instance on a test subdomain
Install Ghost on the VPS with a temporary URL, for example
https://new.your-domain.com, with MySQL 8 and a version at least equal to the source. The production publication stays on Ghost(Pro) the whole time: your readers see nothing.Import content, theme and redirects
In Settings → Advanced → Import/Export, import a
.zipcontainingexport.jsonand thecontent/imagesfolder: the importer accepts JSON and zip, and a large image archive can be split into several zips. Then check the theme before uploading it:npx gscan -z my-theme.zipActivate it, and upload
redirects.yamlandroutes.yamlin Settings → Labs.Connect the same Stripe account, then import members
Ghost asks you to connect the same Stripe account as the original publication before importing paid members. Import a sample of five to ten paid members first, check their tier and subscription on the member record, then import the rest of
members.csv.Wire up email: SMTP and Mailgun
Configure the
mailblock ofconfig.production.json(or the equivalent environment variables under Docker) with your SMTP relay, for sign-in emails. Then enter the Mailgun API keys in the Email newsletter settings of Ghost Admin. Test both: a sign-in link received on an internal address, then a test newsletter sent to an internal label.Compare before and after
Count posts and members on the new instance:
mysql -u ghost -p ghost_prod -e "SELECT status, COUNT(*) FROM posts GROUP BY status; SELECT status, COUNT(*) FROM members GROUP BY status;"The figures must match step 1. Open ten random posts, images included, and look for any trace of the old host in the page source.
Switch the domain and keep Ghost(Pro) as an overlap
Replace the temporary URL with the final domain in the configuration, restart Ghost, then change the DNS record. Replay the sitemap and check that every old URL answers 200:
while read u; do echo "$(curl -s -o /dev/null -w '%{http_code}' "$u") $u"; done < urls-before.txt | grep -v '^200'Keep the Ghost(Pro) subscription active for one to two weeks. For transactional email across several publications, see our SMTP relay on a VPS guide.
Stripe subscriptions: what transfers, and what to test
The members export carries, for each paid member, their Stripe customer ID (stripe_customer_id). The importer can also find it on its own: with the value auto, Ghost searches the connected Stripe account for a customer with the same email address. Hence the condition set by the documentation: the new instance must be connected to the same Stripe account before the import. Another account does not hold those customers, and your paying subscribers would arrive as free members.
The subscriptions themselves stay at Stripe: the migration does not cancel them and asks no one to subscribe again. The documentation does not, however, detail how each subscription finds its tier on the new instance. That is the reason for the sample in step 7: check on a few records that the tier, the renewal date and the paid status are right before importing the whole list.
After the cutover, open the webhook endpoints in the Stripe dashboard. The new instance's endpoint must receive events; the Ghost(Pro) one is removed only when the old subscription is closed.
Newsletters: the email gap to fill
This is the point most migrations discover too late. On Ghost(Pro), newsletter delivery is included. Self-hosted, Ghost's documentation is explicit: Mailgun is the only supported bulk email provider, and a plain SMTP server cannot send newsletters. SMTP only handles sign-in emails and invitations.
So you need two channels: an SMTP relay for transactional mail, and a Mailgun account for distribution. On cost, Mailgun has withdrawn its Flex plan for new accounts or made it hard to reach: check the current pricing on Mailgun's site before budgeting, based on your monthly send volume (subscribed members multiplied by publishing frequency).
Finally, prepare authentication of the sending domain: the SPF and DKIM records provided by Mailgun go into the DNS zone before the first real send. A newsletter sent from an unauthenticated domain lands in spam, and the reputation of the whole domain suffers.
The DNS cutover, with downtime kept to a minimum
Downtime comes down to two things: DNS propagation and the TLS certificate. The TTL lowered in step 2 handles the first. For the second, there are two options. If your server gets its certificate through an HTTP challenge (Caddy in Ghost's official Docker install, certbot with nginx), it can only do so once DNS has switched: expect a few seconds to a few minutes of TLS errors. If your DNS allows it, a DNS-01 challenge issues the certificate before the cutover and removes that window.
Pick a low-traffic slot, and send no newsletter that day. A post scheduled on Ghost(Pro) would still go out from the old instance: suspend scheduled sends before the export.
As long as Ghost(Pro) stays active, rolling back means restoring the old DNS record. That is your safety net. Only close the subscription after a week with no gap in the counts, the Stripe webhooks and deliverability.
Back up from day one
Ghost(Pro) backed up for you; now it is your job. Schedule at least a daily database dump and a copy of the content folder, to storage outside the VPS:
mysqldump --single-transaction -u ghost -p ghost_prod | gzip > /backup/ghost-$(date +%F).sql.gz
tar czf /backup/content-$(date +%F).tar.gz /var/www/ghost/contentClose every port other than 22, 80 and 443 at the firewall, and test a full restore on a blank machine at least once a quarter.
Troubleshooting: the most frequent errors
413 Request Entity Too Large on import: nginx rejects the archive. Raise client_max_body_size in the server block (for example 200m), reload nginx, or split the images into several zips.
Broken images after import: the archive did not follow the content/images/… path, or some files are missing. Compare images.txt with the folder contents on the server and reimport the missing ones.
Paid members imported as free: Stripe was not connected at import time, or it was a different account. Connect the right account and reimport the affected members.
Sign-in link never received: the mail block is missing or wrong. Check Ghost's logs (ghost log, or docker compose logs ghost) at the moment of the sign-in request.
No email option when publishing: Mailgun is not configured. SMTP is not enough for newsletters; enter the Mailgun API keys in Email newsletter.
For an agency: grouping your clients' publications
Migrating one publication takes half a day of preparation and an hour of cutover. The second goes faster, because the server, the SMTP relay and the method already exist. That is where self-hosting pays off for an agency: several Ghost instances on one VPS, each with its own database and domain, and a single infrastructure bill.
On ServOrbit, VPS plans start at 99 DH/month tax exclusive (VAT excluded), with root access and a dedicated IPv4, and your domains, servers and renewals are tracked in one place. If the question remains “who runs the server?”, the VPS administration option answers it: you manage the editorial relationship with your clients, we run the infrastructure.
This guide is provided as is: ServOrbit does not carry out the migration of your Ghost(Pro) publication; VPS administration covers the server, not this switchover.