Deployment guide

Migrate from Ghost(Pro) to self-hosted Ghost on a VPS

Deploy on a VPS Cloud →

Tutorial

Migrate from Ghost(Pro) to self-hosted Ghost on a VPS

Self-hosting11 min read10 steps

Ghost(Pro) bills by member count: every tier you cross raises the subscription, and an agency running several publications pays one subscription per site. Because Ghost is open source, the same publication can run on your own server, with the same export format. The risk is not importing the content but everything around it: images, Stripe subscriptions, newsletter delivery and the domain cutover. This guide covers those four points step by step, with a rollback path for as long as Ghost(Pro) stays active.

Contents· Why leave Ghost(Pro), and what you leave behind1/11
  1. 01Why leave Ghost(Pro), and what you leave behind
  2. 02What you gain by leaving Ghost(Pro)
  3. 03What changes, item by item
  4. 04Sized prerequisites before touching Ghost(Pro)
  5. 05The migration in ten steps, from inventory to cutover
  6. 06Stripe subscriptions: what transfers, and what to test
  7. 07Newsletters: the email gap to fill
  8. 08The DNS cutover, with downtime kept to a minimum
  9. 09Back up from day one
  10. 10Troubleshooting: the most frequent errors
  11. 11For an agency: grouping your clients' publications

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 content folder 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

ItemGhost(Pro)Self-hosted Ghost
PricingPer plan, capped by member countServer price, whatever the member count
Newsletter deliveryIncluded, no setupMailgun account required, billed separately
Member sign-in emailsIncludedSMTP server to configure
Ghost updatesApplied by the vendorYour responsibility
BackupsHandled by the vendorMySQL database and `content` folder to back up yourself
Custom themeDepends on the planAlways possible
Commission on subscriptionsNone from Ghost on paid plansNone; 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

  1. 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.txt
  2. Lower 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.

  3. 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.yaml and redirects.yaml. Check that each file is there before moving on.

  4. 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/images folder, or rebuild it from the public URLs found in the export:

    grep -o '/content/images/[^"\\]*' export.json | sort -u > images.txt
    wc -l images.txt

    Then download each file while keeping the content/images/… tree: Ghost's importer expects that path.

  5. 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.

  6. Import content, theme and redirects

    In Settings → Advanced → Import/Export, import a .zip containing export.json and the content/images folder: 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.zip

    Activate it, and upload redirects.yaml and routes.yaml in Settings → Labs.

  7. 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.

  8. Wire up email: SMTP and Mailgun

    Configure the mail block of config.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.

  9. 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.

  10. 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/content

Close 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.

Your Ghost publications, grouped on your own servers

One space for the domains, VPS and renewal deadlines of your whole portfolio of publications. The VPS administration option covers running the server, not the migration itself: you manage the client relationship, we run the infrastructure.

Need help?

Browse our help center and FAQ, or reach our team — callback, WhatsApp or email. Support in French, English and Arabic.

Message us on WhatsAppopens in a new tab