Why self-host Ghost on a VPS
Ghost combines what WordPress does laboriously and what Substack charges dearly for: a clean editor, excellent native performance (Node.js), integrated member and paid-subscription management via Stripe, and newsletter sending without a plugin. But Ghost(Pro) bills by audience, and newsletter platforms take a commission on your revenue.
By hosting it on your VPS, you keep 100% of your paying subscribers (excluding Stripe fees), you customise the theme without restriction, and you control your email list — your most valuable asset. With Ghost v6, creators monetising their newsletters also benefit from automatic VAT collection via Stripe Tax, with no dependency on a third-party service.
The benefits of a self-hosted Ghost
- No platform commission on your paid subscriptions (excluding Stripe)
- Native performance: loading time is measured from the reader's own connection — it depends on the operator, the time of day and the route taken
- Integrated newsletters and members, without a stack of plugins
- Full customisation of the Handlebars theme and complete API access
- Control of your subscriber list and your data
- A fixed VPS cost regardless of the number of readers
- Ghost v6: integrated VAT/tax collection via Stripe Tax for monetised creators
Realistic prerequisites
Ghost v6 runs on Node.js 20 LTS minimum (Node 22 recommended for performance). Plan for a VPS with at least 1 vCPU and 2 GB of RAM for a blog with comfortable traffic; move up to 4 GB if your audience grows fast or if you send high-volume newsletters. For production use, reserve at least 20 GB of disk for media and the database.
The recommended production database is MySQL 8 (SQLite is supported for development only — see the troubleshooting section). You also need a transactional SMTP service (Mailgun, Postmark, Brevo) for reliable delivery of newsletters and member authentication emails, a blog.yourdomain.com domain pointed at your VPS IP, and Caddy or Nginx for the HTTPS reverse proxy. Caddy is preferred: it provisions and renews Let's Encrypt certificates without extra configuration.
Installation via Ghost CLI (recommended method)
Prepare the server
On Debian/Ubuntu, install Node.js 20, MySQL 8, Nginx (required by Ghost CLI for its compatibility check) and Caddy. Create a dedicated user (not root):
adduser --disabled-login ghost-user
su - ghost-userInstall Ghost CLI
npm install ghost-cli@latest -gGhost CLI manages installation, database migrations, updates and PM2 process-manager restarts.
Create the directory and run the install
mkdir -p /var/www/ghost
cd /var/www/ghost
ghost installThe wizard asks for your blog URL (e.g. https://blog.yourdomain.com), MySQL credentials, SMTP settings and whether to configure Nginx. Answer no to the Nginx step if you are using Caddy — you will configure it in the next step.
Configure the TLS reverse proxy with Caddy
Create or edit your Caddyfile:
blog.yourdomain.com {
reverse_proxy localhost:2368
encode gzip
header /ghost/* Cache-Control "no-store"
header /members/* Cache-Control "no-store"
}Caddy obtains and renews the Let's Encrypt certificate automatically. Ghost listens locally on port 2368; only Caddy is exposed on ports 80 and 443.
Secure the first access
Open /ghost immediately after installation: Ghost displays its account-creation wizard (name, email, password). Complete it without delay — until the wizard is finished, anyone reaching this URL can become the owner of the blog.
Connect your SMTP service
In Settings → Email, verify that your SMTP credentials are correct and send a test email. Newsletter delivery and member login emails go through this configuration — a malformed MAIL_FROM (without a verified domain) is one of the most common causes of silent failures at sign-up.
Connect Stripe and activate subscriptions
In Settings → Members, connect your Stripe account to enable paid subscriptions. Define your tiers, monthly and annual prices, then publish your first article and send your first newsletter.
Starting Caddy and verifying the reverse proxy
Once the Caddyfile is written, start or reload Caddy to apply the configuration:
caddy run --config /etc/caddy/CaddyfileIf Caddy is managed as a systemd service (which is the case after an APT install), prefer the graceful reload command:
systemctl reload caddyCaddy immediately contacts Let's Encrypt to obtain a TLS certificate for your domain via the HTTP-01 challenge. Within 30 seconds, https://blog.yourdomain.com should respond. Verify the certificate state:
curl -sv https://blog.yourdomain.com/ 2>&1 | grep -E 'SSL|subject|issuer'If the command shows issuer: O=Let's Encrypt, the certificate is valid. If you get connection refused, check that Ghost is running on port 2368 with ghost status from the installation directory. If port 443 is closed in your firewall, open it before starting Caddy: ufw allow 443.
Ghost v6 and Stripe Tax: the new tax_id_collection field
Ghost v6 introduces support for the tax_id_collection field in the Stripe API. This field enables Stripe Tax to automatically collect the buyer's VAT or tax identification number at the point of payment, in line with European and global tax regulations.
For existing instances migrated to Ghost v6, this feature is a configuration change on the Stripe side, not in Ghost itself. Two things to verify:
1. In the Stripe Dashboard, enable Stripe Tax and configure your tax rules by country.
2. In Stripe → Checkout → Settings, enable tax ID collection if required for your market.
Ghost v5 instances upgrading to v6 without adjusting their Stripe settings will continue to work, but the new fiscal compliance features will remain inactive until you configure Stripe Tax on your side.
Updating Ghost with Ghost CLI
Ghost CLI simplifies both minor and major updates. From the installation directory:
cd /var/www/ghost
ghost updateTo target a specific version, for example when migrating from v5 to v6:
ghost update 6.x.xGhost CLI stops the PM2 process, applies database migrations, updates the files and restarts Ghost. If a migration fails, Ghost CLI performs an automatic rollback and displays the error detail in the terminal.
Before any major update (v5 → v6), two preventive steps are essential: back up the content directory /var/www/ghost/content (media and custom themes) and export the MySQL database:
mysqldump -u ghost -p ghost_db > ghost_backup_$(date +%F).sqlFinally, verify the installed version after the update:
ghost versionAn incorrect version number after an update usually indicates a permissions issue on the /var/www/ghost directory — check that the dedicated user is the owner of the folder.
Troubleshooting: common startup errors
Ghost starts but returns a blank page or a 500 error
Check the logs with ghost log. The most common cause is an incorrect url value in config.production.json — Ghost generates all its internal links from this value, and an HTTP URL when Caddy is serving over HTTPS causes redirect loops.
Error "ER_NOT_SUPPORTED_AUTH_MODE" at startup
Ghost v6 requires MySQL 8 with the mysql_native_password authentication plugin. If your MySQL defaults to caching_sha2_password, modify the user:
ALTER USER 'ghost'@'localhost' IDENTIFIED WITH mysql_native_password BY 'your_password';
FLUSH PRIVILEGES;SQLite in production: why it is not an option
Ghost accepts SQLite in development mode, but this engine is not supported in production: it does not handle concurrent access, and high-volume newsletters or traffic spikes cause irreversible database locks. Migrate to MySQL before going live.
Theme upload fails in production
Ghost limits theme upload sizes. If an upload fails silently or returns a 413 error, adjust the limit on the Caddy side:
blog.yourdomain.com {
request_body {
max_size 50MB
}
reverse_proxy localhost:2368
}MAIL_FROM misconfigured: members not receiving emails
The mail.from field in config.production.json must carry an address whose domain is verified with your SMTP provider (active SPF and DKIM records). An unverified domain results in silent failures at sign-up or SMTP rejections that Ghost rarely surfaces clearly in its logs.
Place Cloudflare in proxy mode in front of Caddy to absorb traffic spikes: almost all of Ghost's public content is static and caches very well. Exclude /ghost/, /members/ and /api/ from the cache via Page Rules or a custom cache rule — these routes must always reach Ghost directly for member authentication and admin to work correctly.
Automatic backups
Ghost's dynamic content (media, custom themes) lives in /var/www/ghost/content. Add a cron job that backs up this directory and a daily MySQL export to remote storage. Ghost CLI does not back up automatically.
# Daily MySQL export — add to crontab
0 2 * * * mysqldump -u ghost -p ghost_db | gzip > /backups/ghost-$(date +%F).sql.gzOfficial documentation
For advanced configuration and options specific to the tool, refer to the official Ghost documentation. This guide covers going live on a VPS; the vendor's docs remain the reference for fine-tuning, major updates and specific use cases.