VPS Snaps

How to back up and restore a self-hosted Ghost site

A complete Ghost backup is a dump of its MySQL database plus the content folder, which holds images, media, files, themes, routes and redirects, with config.production.json kept privately beside them. ghost backup and the JSON export in Ghost Admin are for moving content between sites: Ghost's own docs say JSON exports leave out comments, email analytics and member engagement data. To restore, install the same Ghost version, stop it, import the dump, unpack content, and hand it back to the ghost user.

8 min readUpdated Checked against official documentation

What a complete backup includes

This guide assumes Ghost-CLI's standard install at /var/www/ghost, the path Ghost's own guides use. Posts, pages, members, newsletters, comments, settings, staff users and analytics live in the MySQL database. Everything else that matters is under content.

PartWhereBack it up?
DatabaseMySQL 8, named in database.connection.databaseYes. This is the site.
Uploadscontent/images, content/media, content/filesYes. They cannot be rebuilt.
Themescontent/themesYes. Custom and edited themes exist nowhere else.
Routes, redirects, adapterscontent/settings, content/data, content/adaptersYes. Small and easy to forget.
config.production.jsonInstall rootYes, privately.
Ghost-CLI's .ghost-cli fileInstall rootYes. It records the Ghost version.
Logscontent/logsNo.
Ghost itselfcurrent, versions, systemNo. ghost install and ghost setup recreate them.

config.production.json holds the database password and any mail credentials. Keep backups that contain it in private storage only you can read.

If the config sets paths.contentPath, back up that directory instead. If it configures a storage adapter, uploads live in that adapter's storage, not in content/images.

Read the database settings

Ghost-CLI refuses to run as root, so run ghost commands as the regular user you installed Ghost with, from /var/www/ghost. Everything else here runs as root, for example after sudo -i.

Terminal
ghost config database.connection.database

Repeat with database.connection.user and database.connection.password, or read config.production.json. Given MySQL's root login at install, Ghost-CLI creates a user named ghost- plus a number, with a random password and every privilege on that one database. The default database name is the install directory's name plus _prod: ghost_prod here. Put the credentials in an option file, so the password stays out of ps and your shell history:

/etc/mysql/ghost-backup.cnf
[client]
user=ghost-123
password="your-password-here"
host=localhost
Terminal
chmod 600 /etc/mysql/ghost-backup.cnf

Dump the database

Terminal
install -d -m 700 /var/backups/ghost
Terminal
mysqldump --defaults-extra-file=/etc/mysql/ghost-backup.cnf --single-transaction --no-tablespaces ghost_prod | gzip > /var/backups/ghost/ghost-db-$(date +%F).sql.gz
  • --single-transaction dumps InnoDB tables, MySQL 8's default, from one consistent snapshot without locking them, so Ghost keeps serving.
  • --no-tablespaces is needed because the Ghost user lacks the global PROCESS privilege mysqldump otherwise requires.
  • The dump holds everything JSON exports miss: comments, member subscriptions, newsletters and email analytics.

A complete dump ends with a -- Dump completed on line. Check it:

Terminal
gunzip -c /var/backups/ghost/ghost-db-2026-10-03.sql.gz | tail -n 1

The mysqldump guide covers the other options.

Archive the content folder and config

Dump the database first, then the files. An image uploaded in between is then present but unused, which is harmless; the other order can leave posts pointing at images the archive lacks.

Terminal
tar -czf /var/backups/ghost/ghost-files-$(date +%F).tar.gz --exclude='ghost/content/logs' -C /var/www ghost/content ghost/config.production.json ghost/.ghost-cli
  • -C /var/www stores paths as ghost/content/..., so you can later extract content on its own.
  • The exclude must come before the paths, or GNU tar ignores it.
  • Casper and Source, the bundled themes, are symlinks into current. tar keeps them as links, which work again once Ghost is installed at the same path.

The tar guide explains excludes and how to test an archive.

What ghost backup and the Admin export are for

Run in the install directory, ghost backup writes a zip to the current directory, named backup-from-v plus the Ghost version and a timestamp. Ghost's docs list its contents: the content as JSON, a CSV of all members, installed themes, images, files and media, and the routes and redirects files.

  • Ghost must be running; the command offers to start it.
  • It exports through Ghost's Admin API, so it asks you to sign in: from Ghost 5.129.0 with the Staff access token on your user's settings page, before that with an email and password.
  • It never reads the database directly. Ghost's docs say JSON exports leave out email analytics, member engagement data and comments, and paid members only import once Stripe is reconnected.
  • It leaves copies of the JSON and the members CSV in backup/ and content/data/. The CSV lists every member's email address: delete those copies or keep them private.

Ghost Admin's Import/Export settings (Labs on older versions) export the same content JSON, and the Members page exports the CSV. Use them to move content to another Ghost site. For disaster recovery or an exact copy, Ghost's docs say to back up the database and content folder directly, as above.

Restore on a new server

Install the Ghost version the backup came from, so the dump matches the schema Ghost expects. The active-version line in the archived .ghost-cli shows it:

Terminal
tar -xzf /var/backups/ghost/ghost-files-2026-10-03.tar.gz -O ghost/.ghost-cli

Follow Ghost's Ubuntu guide on the new server, with MySQL 8, the same install path and the same site URL, but install that version. Then, as your Ghost-CLI user in /var/www/ghost:

Terminal
ghost install <version>
Terminal
ghost stop

The install created a fresh database and a new MySQL user. Keep the new config.production.json, write its credentials into /etc/mysql/ghost-backup.cnf on this server, and import as root. mysqldump wrote DROP TABLE IF EXISTS before each table, so the import replaces the fresh tables:

Terminal
gunzip -c /var/backups/ghost/ghost-db-2026-10-03.sql.gz | mysql --defaults-extra-file=/etc/mysql/ghost-backup.cnf ghost_prod

Extract only content over the new install, then give it back to the ghost user, as Ghost's restore steps do:

Terminal
tar -xzf /var/backups/ghost/ghost-files-2026-10-03.tar.gz -C /var/www ghost/content
Terminal
chown -R ghost:ghost /var/www/ghost/content

Print the old config and copy across the mail block and anything else you had added. Leave the new database block as it is:

Terminal
tar -xzf /var/backups/ghost/ghost-files-2026-10-03.tar.gz -O ghost/config.production.json
Terminal
ghost start

To roll back on the same server, skip the install: ghost stop, import the dump, extract content, fix ownership, ghost start. Update to a newer Ghost afterwards with ghost update, not before.

Verify the restore

Terminal
ghost ls

The site should show as running in production. Then fetch the home page and expect 200:

Terminal
curl -sS -o /dev/null -w '%{http_code}\n' https://example.com/

Log in to Ghost Admin and compare the post and member counts with the old site, open a post with images, and send yourself a test email. If Ghost will not start, ghost run starts it in the foreground and shows the error, and ghost log -e shows the error log. Practice this on a spare server first; testing restores explains how.

Automate it with cron

This root script needs no Ghost-CLI and no sign-in. Each file gets its real name only after it is complete, and tar's exit code 1, meaning a file changed while it was read, counts as success.

/usr/local/bin/ghost-backup.sh
#!/bin/bash
set -euo pipefail

SITE_DIR="/var/www"
SITE="ghost"
DB_NAME="ghost_prod"
BACKUP_DIR="/var/backups/ghost"
KEEP_DAYS=14
STAMP="$(date +%Y-%m-%d_%H%M)"
DB="$BACKUP_DIR/$SITE-db-$STAMP.sql.gz"
FILES="$BACKUP_DIR/$SITE-files-$STAMP.tar.gz"

mkdir -p "$BACKUP_DIR"
trap 'rm -f "$DB.partial" "$FILES.partial"' EXIT

mysqldump --defaults-extra-file=/etc/mysql/ghost-backup.cnf \
  --single-transaction --no-tablespaces "$DB_NAME" | gzip > "$DB.partial"
gunzip -c "$DB.partial" | tail -n 1 | grep -q "Dump completed"
mv "$DB.partial" "$DB"

tar -czf "$FILES.partial" --exclude="$SITE/content/logs" -C "$SITE_DIR" \
  "$SITE/content" "$SITE/config.production.json" "$SITE/.ghost-cli" || [ $? -eq 1 ]
mv "$FILES.partial" "$FILES"

find "$BACKUP_DIR" -name "$SITE-*.gz" -type f -mtime +"$KEEP_DAYS" -delete
Terminal
chmod 755 /usr/local/bin/ghost-backup.sh
/etc/cron.d/ghost-backup
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

45 3 * * * root flock -n /run/lock/ghost-backup.lock /usr/local/bin/ghost-backup.sh >> /var/log/ghost-backup.log 2>&1

flock -n stops two runs overlapping, and -mtime +14 keeps about two weeks. Backups on the server's own disk die with it, so copy them off each night, for example with rclone to Cloudflare R2. The cron guide covers alerts.

Common errors

ErrorFix
You can't run commands as the 'root' user.Run ghost as the regular user you installed Ghost with.
Access denied; you need (at least one of) the PROCESS privilege(s) for this operationAdd --no-tablespaces to mysqldump.
Ghost instance is not currently runningghost backup needs Ghost running. Start it, or answer yes when asked.
Staff Device Verification is enabled, so backups might fail with password auth.Update Ghost to 5.129.0 or later, which signs in with a Staff access token.
Images are missing or uploads fail after a restorecontent is not owned by the ghost user: run chown -R ghost:ghost /var/www/ghost/content.
Ghost will not start after the importRun ghost run to see why, and check the installed version matches the backup's active-version.
The site still looks freshly installedThe dump went into a different database than database.connection.database, or Ghost was not restarted.

Frequently asked questions

Is ghost backup a full backup?
No. It is a JSON content export plus members CSV, themes and uploads. Ghost's docs say JSON exports leave out comments, email analytics and member engagement data; for disaster recovery, back up the database and content folder directly.
Do I need to stop Ghost to back it up?
No. mysqldump with --single-transaction reads a consistent snapshot while Ghost runs. Stop Ghost only to restore.
Can I restore a backup into a newer Ghost version?
Install the version the backup came from, restore into it, then run ghost update. That way Ghost's own update path migrates the data.
My Ghost site uses SQLite. What changes?
The database is the file named in database.connection.filename, normally under content/data, so it is in the content archive; copy it with Ghost stopped or use the sqlite3 .backup command. Ghost supports only MySQL 8 in production and recommends a reinstall to move to it.

How this was checked

Commands, limits and prices were checked against these official pages, on October 3, 2026: