How to back up and restore a WordPress site on your own server
A complete WordPress backup has two parts: a dump of the database and a copy of the site directory, including wp-content and wp-config.php. With WP-CLI, wp db export dumps the database using the credentials in wp-config.php, and tar archives the files. To restore, load both on any server, fix ownership, and run wp search-replace only if the URL changed.
What a complete backup includes
Posts, pages, comments, users, settings and most plugin data live in the MySQL or MariaDB database. Everything else is files in the site directory. You need both to restore the site.
| Part | Where | Back it up? |
|---|---|---|
| Database | MySQL or MariaDB, named in DB_NAME | Yes. This is the site's content. |
| Uploads | wp-content/uploads | Yes. Media cannot be rebuilt. |
| Themes and plugins | wp-content/themes, wp-content/plugins | Yes. Paid, custom and edited ones cannot be downloaded again. |
wp-config.php | Site root, or one directory above it | Yes, and treat it as a secret. |
.htaccess | Site root (Apache only) | Yes. It holds permalink and custom rules. |
| WordPress core | wp-admin, wp-includes, root PHP files | Optional. Reinstallable, but small, and keeping it makes restores simpler. |
| Caches | wp-content/cache, wp-content/upgrade | No. Temporary or rebuilt. |
wp-config.php holds the database password and the security keys and salts behind every login cookie. Keep backups in private storage that only you can read. Keep the original keys too: if they change, every user has to log in again.
Read the database settings from wp-config.php
The commands in this guide run as root, for example after sudo -i. WP-CLI is the exception: it refuses to run as root, since plugin code would then get root's privileges, so run it as the user PHP runs as, www-data on Debian and Ubuntu. --path points it at the site.
sudo -u www-data wp --path=/var/www/example.com config get DB_NAMERepeat with DB_USER, DB_HOST and table_prefix. Without WP-CLI, read the file:
grep -E "DB_(NAME|USER|PASSWORD|HOST)|table_prefix" /var/www/example.com/wp-config.phpDB_HOST can carry a port (127.0.0.1:3307) or a socket (127.0.0.1:/var/run/mysqld/mysqld.sock). WP-CLI understands both; mysqldump needs them as separate settings. If wp-config.php is missing from the site root, look one directory up: WordPress loads it from there too.
Export the database with WP-CLI
Create a private directory for the backups, then export:
install -d -m 700 /var/backups/wordpresssudo -u www-data wp --path=/var/www/example.com db export --single-transaction - | gzip > /var/backups/wordpress/example.com-db-$(date +%F).sql.gz-sends the dump to standard output. gzip compresses it and root's shell writes the file, sowww-datanever needs access to the backup directory.- WP-CLI passes flags it does not know to mysqldump.
--single-transactiondumps InnoDB tables from one consistent snapshot without locking them, so the site stays up. - WP-CLI adds
--no-tablespacesitself, so the WordPress database user does not need the globalPROCESSprivilege. - It dumps the whole database named in
DB_NAME, including tables of other apps that share it.
A complete dump ends with a -- Dump completed on line. Check it:
gunzip -c /var/backups/wordpress/example.com-db-2026-10-03.sql.gz | tail -n 1A pipe returns gzip's exit status. If the export fails, you still get a small file that looks fine. Scripts need set -o pipefail and the check above.
Export without WP-CLI
Use mysqldump with the credentials from wp-config.php in an option file, so the password stays out of ps and your shell history:
[client]
user=wp_user
password="your-password-here"
host=localhostchmod 600 /etc/mysql/wp-backup.cnfmysqldump --defaults-extra-file=/etc/mysql/wp-backup.cnf --single-transaction --no-tablespaces wordpress | gzip > /var/backups/wordpress/example.com-db-$(date +%F).sql.gzHere --no-tablespaces matters: without it mysqldump needs PROCESS, which a WordPress user rarely has. For a port or socket from DB_HOST, add port=3307 or socket=/var/run/mysqld/mysqld.sock to the option file. The mysqldump guide covers the other options.
Archive the site files
Dump the database first, then the files. A file uploaded in between is then present but unused, which is harmless. The other order can leave the database pointing at a file the archive lacks.
tar -czf /var/backups/wordpress/example.com-files-$(date +%F).tar.gz --exclude='example.com/wp-content/cache' --exclude='example.com/wp-content/upgrade' -C /var/www example.com-C /var/www example.comstores paths asexample.com/..., so the archive extracts anywhere.- Exclude patterns match those stored names, and must come before the directory or GNU tar ignores them.
- If backup plugins keep archives inside
wp-content, exclude that folder too, or each backup carries old backups.du -sh /var/www/example.com/wp-content/*shows what is large. - If wp-config.php sits one level up, add
wp-config.phpafterexample.comat the end of the command.
tar run as root records owners and permissions, which a restore needs. The tar guide explains excludes and how to test an archive.
Restore on the same or a new server
On a new server, install the web server, PHP and MySQL or MariaDB, then create an empty database and user. wp db import does not create the database.
CREATE DATABASE wordpress;
CREATE USER 'wp_user'@'localhost' IDENTIFIED BY 'your-password-here';
GRANT ALL PRIVILEGES ON wordpress.* TO 'wp_user'@'localhost';On the same server, move the broken site aside instead of extracting over it, so files added after the backup do not mix with the restored ones:
mv /var/www/example.com /var/www/example.com.oldtar -xzf /var/backups/wordpress/example.com-files-2026-10-03.tar.gz -C /var/wwwIf the database name, user, password or host differ, update them in wp-config.php. Then import; - reads the dump from standard input:
gunzip -c /var/backups/wordpress/example.com-db-2026-10-03.sql.gz | sudo -u www-data wp --path=/var/www/example.com db import -mysqldump writes DROP TABLE IF EXISTS before each table, so the import replaces tables of the same name. Without WP-CLI, pipe the dump into mysql --defaults-extra-file=/etc/mysql/wp-backup.cnf wordpress instead.
Root's tar restores the original owners when the same users exist. If PHP runs as a different user here, give the files to the user that owned them before; a common setup is the PHP user. Then apply WordPress's recommended modes: 755 for directories, 644 for files, 440 for wp-config.php.
chown -R www-data:www-data /var/www/example.comfind /var/www/example.com -type d -exec chmod 755 {} \;find /var/www/example.com -type f -exec chmod 644 {} \;chmod 440 /var/www/example.com/wp-config.phpMoving the site to a new host for good, with DNS, HTTPS and email to switch over as well? See how to move a WordPress site to a new host.
Change the URL, then clear caches
If the domain changed, update it everywhere. WordPress stores full URLs in its settings, posts and plugin options, some inside PHP serialized data that records each string's length. Editing the SQL file with a text replace breaks those values. wp search-replace rewrites them correctly. Do a dry run first:
sudo -u www-data wp --path=/var/www/example.com search-replace 'https://old.example.com' 'https://new.example.com' --skip-columns=guid --all-tables-with-prefix --dry-run--dry-runreports the replacements per table without saving them. Run the command again without it to apply.--skip-columns=guidleaves post GUIDs alone. WordPress's docs say never to change them: feed readers use them, and changed GUIDs show old posts as new.--all-tables-with-prefixincludes plugin tables with your prefix that WordPress does not register.
If wp-config.php defines WP_HOME or WP_SITEURL, update them too; they override the database values. Whether or not the URL changed, clear anything still holding old state:
sudo -u www-data wp --path=/var/www/example.com cache flushsudo -u www-data wp --path=/var/www/example.com transient delete --allsudo -u www-data wp --path=/var/www/example.com rewrite flush --hardcache flush empties the object cache; on multisite with a persistent object cache, that is every site's cache. transient delete --all removes cached values that may still hold the old address. rewrite flush rebuilds the permalink rules; --hard also updates .htaccess, and works on single sites only. A caching plugin's page cache is cleared from its settings page.
Verify the restore
sudo -u www-data wp --path=/var/www/example.com core verify-checksumsSuccess: WordPress installation verifies against checksums.That compares core files with WordPress.org's checksums, which also catches a tampered core file. wp plugin verify-checksums --all does the same for plugins from WordPress.org. wp db check checks every table with mysqlcheck, and wp option get home shows the address the site now uses. Then fetch key pages and expect 200:
curl -sS -o /dev/null -w '%{http_code}\n' https://example.com/Finally, log in to wp-admin and open the Media Library. Practice this on a spare server before you need it; testing restores explains how.
Automate it with cron
This script writes each file under a temporary name, checks the dump, and only then gives it its real name. tar exits with 1 when a file changed while it was read, which is common for uploads, so 1 counts as success and anything higher as failure.
#!/bin/bash
set -euo pipefail
SITE_DIR="/var/www"
SITE="example.com"
BACKUP_DIR="/var/backups/wordpress"
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
sudo -u www-data /usr/local/bin/wp --path="$SITE_DIR/$SITE" db export --single-transaction - | gzip > "$DB.partial"
gunzip -c "$DB.partial" | tail -n 1 | grep -q "Dump completed"
mv "$DB.partial" "$DB"
tar -czf "$FILES.partial" --exclude="$SITE/wp-content/cache" --exclude="$SITE/wp-content/upgrade" -C "$SITE_DIR" "$SITE" || [ $? -eq 1 ]
mv "$FILES.partial" "$FILES"
find "$BACKUP_DIR" -name "$SITE-*.gz" -type f -mtime +"$KEEP_DAYS" -deletechmod 755 /usr/local/bin/wp-backup.shPATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
15 3 * * * root flock -n /run/lock/wp-backup.lock /usr/local/bin/wp-backup.sh >> /var/log/wp-backup.log 2>&1-mtime +14 keeps about two weeks of nightly backups, and flock -n stops two runs overlapping. Backups on the server's own disk die with it, so copy them off it each night, for example with rclone to Backblaze B2 or an S3 bucket. The cron guide covers logging and alerts.
Running a WooCommerce store? Orders arrive between backups, so a restore needs more care than a blog does: see how to back up a WooCommerce store.
Multisite networks
A multisite network shares one database and one wp-content. Each site has its own tables, such as wp_2_posts, so the commands above back up every site at once. To export one site's tables on their own:
sudo -u www-data wp --path=/var/www/example.com db export --tables=$(sudo -u www-data wp --path=/var/www/example.com db tables --url=sub.example.com --format=csv) - > /var/backups/wordpress/sub-site.sqlWhen the domain changes, add --network to search-replace so it covers every site's tables, and --url= with the old main address so WP-CLI can load the network. WordPress's moving guide also says to review the wp_site and wp_blogs tables by hand and update wp-config.php, where DOMAIN_CURRENT_SITE holds the main domain.
Common errors
| Error | Fix |
|---|---|
Error: YIKES! It looks like you're running this as root. | Run WP-CLI as the site's user: sudo -u www-data wp .... |
Error establishing a database connection | wp-config.php has the wrong name, user, password or host for this server, or the user has no grant on the database. |
| The install screen appears after a restore | The database is empty, or $table_prefix does not match the table names in the dump. |
Access denied; you need (at least one of) the PROCESS privilege(s) for this operation | Add --no-tablespaces to mysqldump. WP-CLI adds it for you. |
The mysqldump or mariadb-dump binary is not available. | Install the MySQL or MariaDB client package on the server. |
| The site redirects to the old domain | Run search-replace, check wp option get home, and look for WP_HOME or WP_SITEURL in wp-config.php. |
| Every page but the home page returns 404 | On Apache, run wp rewrite flush --hard. On Nginx, check the new server block's try_files rule. |
| Images are missing | wp-content/uploads was not restored, or the PHP user cannot read it. Check ownership. |
On a cPanel server, WHM can back up whole accounts: see how to back up a cPanel server. Moving the site to a new host? See moving a server to a new provider.
Frequently asked questions
- Is backing up wp-content enough?
- No. Posts, pages, users and settings live in the database, and wp-config.php sits outside wp-content. Back up the database and the whole site directory.
- Does wp db export take the site offline?
- No. With --single-transaction, InnoDB tables are dumped from a snapshot and stay writable. Without it, mysqldump locks the tables while it dumps them, so writes wait.
- How do I move WordPress to a new domain?
- Restore the backup on the new server, then run wp search-replace with the old and new URLs and --skip-columns=guid. Never replace URLs in the SQL file with a text editor: it breaks serialized data.
- Will restoring a backup log users out?
- Not if you restore the original wp-config.php. New security keys and salts invalidate all existing login cookies, so everyone would have to log in again.
How this was checked
Commands, limits and prices were checked against these official pages, on October 3, 2026:
- WordPress Advanced Administration: WordPress Backups
- WordPress Advanced Administration: Backing Up Your Database
- WordPress Advanced Administration: Moving WordPress
- WordPress Advanced Administration: Hardening WordPress (file permissions)
- WordPress Developer Resources: wp-config.php
- WP-CLI: wp db export
- WP-CLI: wp db import
- WP-CLI: wp search-replace
- WP-CLI: wp core verify-checksums
- WP-CLI: wp plugin verify-checksums
- WP-CLI: wp config get
- WP-CLI: wp cache flush, wp transient delete, wp rewrite flush
- WP-CLI: wp db tables
- WP-CLI source: DB_Command (export and import defaults)
- WP-CLI Handbook: Common issues
- MySQL 8.4 Reference Manual: mysqldump