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.
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.
| Part | Where | Back it up? |
|---|---|---|
| Database | MySQL 8, named in database.connection.database | Yes. This is the site. |
| Uploads | content/images, content/media, content/files | Yes. They cannot be rebuilt. |
| Themes | content/themes | Yes. Custom and edited themes exist nowhere else. |
| Routes, redirects, adapters | content/settings, content/data, content/adapters | Yes. Small and easy to forget. |
config.production.json | Install root | Yes, privately. |
Ghost-CLI's .ghost-cli file | Install root | Yes. It records the Ghost version. |
| Logs | content/logs | No. |
| Ghost itself | current, versions, system | No. 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.
ghost config database.connection.databaseRepeat 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:
[client]
user=ghost-123
password="your-password-here"
host=localhostchmod 600 /etc/mysql/ghost-backup.cnfDump the database
install -d -m 700 /var/backups/ghostmysqldump --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-transactiondumps InnoDB tables, MySQL 8's default, from one consistent snapshot without locking them, so Ghost keeps serving.--no-tablespacesis needed because the Ghost user lacks the globalPROCESSprivilege 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:
gunzip -c /var/backups/ghost/ghost-db-2026-10-03.sql.gz | tail -n 1The 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.
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/wwwstores paths asghost/content/..., so you can later extractcontenton 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/andcontent/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:
tar -xzf /var/backups/ghost/ghost-files-2026-10-03.tar.gz -O ghost/.ghost-cliFollow 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:
ghost install <version>ghost stopThe 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:
gunzip -c /var/backups/ghost/ghost-db-2026-10-03.sql.gz | mysql --defaults-extra-file=/etc/mysql/ghost-backup.cnf ghost_prodExtract only content over the new install, then give it back to the ghost user, as Ghost's restore steps do:
tar -xzf /var/backups/ghost/ghost-files-2026-10-03.tar.gz -C /var/www ghost/contentchown -R ghost:ghost /var/www/ghost/contentPrint the old config and copy across the mail block and anything else you had added. Leave the new database block as it is:
tar -xzf /var/backups/ghost/ghost-files-2026-10-03.tar.gz -O ghost/config.production.jsonghost startTo 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
ghost lsThe site should show as running in production. Then fetch the home page and expect 200:
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.
#!/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" -deletechmod 755 /usr/local/bin/ghost-backup.shPATH=/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>&1flock -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
| Error | Fix |
|---|---|
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 operation | Add --no-tablespaces to mysqldump. |
Ghost instance is not currently running | ghost 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 restore | content is not owned by the ghost user: run chown -R ghost:ghost /var/www/ghost/content. |
| Ghost will not start after the import | Run ghost run to see why, and check the installed version matches the backup's active-version. |
| The site still looks freshly installed | The 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:
- Ghost Docs: Ghost CLI
- Ghost Docs: How can I backup my site data?
- Ghost Docs: Reinstalling Ghost
- Ghost Docs: How to install Ghost on Ubuntu
- Ghost Docs: Configuration
- Ghost Docs: What databases are supported in production?
- Ghost Docs: Migrating from Ghost to Ghost
- Ghost Docs: Root user permissions fix
- Ghost Docs: Admin API (staff access tokens)
- Ghost-CLI source: the backup task (what the zip contains, where it is written)
- Ghost-CLI source: export authentication
- Ghost-CLI source: MySQL user setup and grants
- MySQL 8.4 Reference Manual: mysqldump