How to back up and restore a Craft CMS site
A Craft CMS backup is a database dump, which php craft db/backup writes to storage/backups/, plus the project files Composer cannot rebuild: .env, config/ with its project config YAML, and local asset folders such as web/uploads. Keep CRAFT_SECURITY_KEY from .env safe: data encrypted with it cannot be read under a new key. To restore, run composer install, php craft db/restore and php craft up.
What to back up, and what to skip
This guide follows Craft 5, the current release, in a project at /var/www/craft. Craft 4 works the same way except where noted.
| Path | Back up? | Why |
|---|---|---|
| Database | Yes | Entries, asset records, users and everything editors create. |
.env | Yes, privately | CRAFT_SECURITY_KEY and the database credentials. |
config/ | Yes | general.php, license.key, and project/, the YAML record of sections, fields and settings. |
| Local asset folders | Yes | Uploaded files, often in web/uploads. |
storage/rebrand/ | Yes | A custom login logo and site icon. |
templates/, modules/, composer.lock | Yes, unless in git | Your code. |
storage/backups/ | Copy off the server | Craft's own database backups. |
vendor/ | No | composer install rebuilds it from composer.lock. |
storage/runtime/, storage/logs/ | No | Caches, compiled templates and logs. |
web/cpresources/ | No | Control panel files Craft publishes again. |
Project config belongs in git, but back up the server's copy too: when allowAdminChanges is on in production, changes made in the control panel are written there first. Files on a remote filesystem, such as Craft's Amazon S3 plugin, are not on the server; protect them in the bucket.
Keep CRAFT_SECURITY_KEY safe
Craft hashes and encrypts data with its security key. Its docs warn that if the key changes, anything encrypted with it becomes inaccessible, starting with user session cookies. In projects built from the Craft 4 or 5 starter project, the key and database settings live in .env:
CRAFT_SECURITY_KEY=your-security-key
CRAFT_DB_DRIVER=mysql
CRAFT_DB_SERVER=127.0.0.1
CRAFT_DB_PORT=3306
CRAFT_DB_DATABASE=craft
CRAFT_DB_USER=craft
CRAFT_DB_PASSWORD=your-password-here
CRAFT_DB_TABLE_PREFIX=Craft reads any CRAFT_ or CRAFT_DB_ variable that matches a setting as an override. Projects started on Craft 3 often use unprefixed names such as SECURITY_KEY and DB_SERVER, read by config/general.php and config/db.php; check those files. Keep a copy of .env in a password manager or encrypted storage.
Never run php craft setup/security-key on a restored site. It generates a new key and saves it in .env, which logs everyone out and leaves anything encrypted with the old key unreadable.
Back up the database with php craft db/backup
Run Craft's commands as the user PHP runs as, www-data on Debian and Ubuntu, so the files Craft creates in storage/ stay writable for the site. Everything else runs as root. From the project root:
cd /var/www/craftsudo -u www-data php craft db/backupWith no path, Craft writes to storage/backups/ and names the file from the system name, the UTC time and the Craft version, such as mysite--2026-10-03-031500--v5.11.4.sql, then prints Backup file: and the path. The path argument can instead be a directory, a full file path, or a bare file name saved in the working directory.
--zipcompresses the dump into a.zip, whichdb/restoreaccepts as it is.--overwritereplaces an existing file at a fixed path. Without it, a non-interactive run stops if the file exists.- On PostgreSQL,
--formatpickscustom,directory,tarorplain(Craft 5.2 and later). Craft 4 has neither this nor thebackupCommandFormatsetting that sets its default.
Craft runs mysqldump or pg_dump, which must be in the PATH of the user running it. Its MySQL command includes routines and triggers, and leaves out the rows of cache, session and index tables such as cache, sessions and resourcepaths, keeping their structure. Craft also backs up before applying updates (backupOnUpdate, on by default) and keeps the newest 20 backups in storage/backups/ (maxBackups). All of them stay on the server.
The backupCommand setting. Set in config/general.php or as CRAFT_BACKUP_COMMAND, backupCommand replaces the command Craft runs. A string can use the tokens {file}, {server}, {port}, {user}, {password}, {database} and {schema}; in Craft 5 it can also be a closure that receives Craft's default command and changes it. false turns database backups off completely, including the one before updates. restoreCommand does the same for restores.
Archive the project files
install -d -m 700 /var/backups/crafttar -czf /var/backups/craft/craft-files-$(date +%F).tar.gz --exclude='craft/vendor' --exclude='craft/node_modules' --exclude='craft/storage/runtime' --exclude='craft/storage/logs' --exclude='craft/storage/backups/*' --exclude='craft/web/cpresources' -C /var/www craftThat keeps .env, config/, templates/, web/ with its uploads, storage/rebrand/ and an empty storage/backups/. Volumes are configured under Settings → Assets, each on a filesystem; a Local filesystem's Base Path is where its files go. If a Base Path is outside the project, archive that directory too. The tar guide explains the exclude patterns.
Restore on a new server
Install PHP, Composer, the web server, and the database server with its client tools, then create an empty database and user:
CREATE DATABASE craft;
CREATE USER 'craft'@'localhost' IDENTIFIED BY 'your-password-here';
GRANT ALL PRIVILEGES ON craft.* TO 'craft'@'localhost';Clone the repository to /var/www/craft and check out the commit that was live when the backup was taken. Extract the archive over it, which brings back .env, uploads and the server's project config. Update CRAFT_DB_* in .env if the database differs here, and leave CRAFT_SECURITY_KEY alone.
tar -xzf /var/backups/craft/craft-files-2026-10-03.tar.gz -C /var/wwwcd /var/www/craftcomposer install --no-interactionRun Composer as the user that owns the project files, not root. PHP must be able to write to storage/, web/cpresources/ and config/license.key; Craft's requirements add .env, config/project/, composer.json, composer.lock and vendor/ during updates, and say never to use 777. Then load the database. db/restore reads a .sql file or Craft's .zip, so decompress the script's gzip first:
gunzip -c /var/backups/craft/craft-db-2026-10-03.sql.gz > storage/backups/restore.sqlsudo -u www-data php craft db/restore storage/backups/restore.sql --drop-all-tables --interactive=0--drop-all-tables (Craft 4.1 and later) empties the database first, so no newer tables linger; --interactive=0 skips the questions about backing up first and clearing caches. Delete restore.sql afterwards. Before applying anything, compare the project config on disk with the restored database:
sudo -u www-data php craft project-config/diffIt should print No pending project config YAML changes. If it lists changes, the next command would write them into the database, so check out the commit that matches the backup first.
sudo -u www-data php craft up --interactive=0sudo -u www-data php craft clear-caches/allup runs pending migrations and applies project config, backing up the database first unless you add --no-backup. Finally restore what lives outside the project: the web server's site config, cron entries and any queue runner service.
Verify the restore
sudo -u www-data php craft install/checkIt prints Craft is installed. when the database is connected and populated. Then check that every asset's file came back:
sudo -u www-data php craft index-assets/all --create-missing-assets=0 --interactive=0It re-indexes every volume and lists any recorded assets are missing their files. --create-missing-assets=0 stops it adding records for files the database does not know, and it deletes nothing unless you add --delete-missing-assets. Finally fetch the home page, log in to the control panel, and open an entry with images. Testing restores shows how to make this routine.
Automate it with cron
This root script has Craft dump to a fixed name, checks the dump is complete, compresses it outside the project, and archives the files. It assumes MySQL; for PostgreSQL, drop the Dump completed check.
#!/bin/bash
set -euo pipefail
APP_DIR="/var/www"
APP="craft"
BACKUP_DIR="/var/backups/craft"
KEEP_DAYS=14
STAMP="$(date +%Y-%m-%d_%H%M)"
DUMP="$APP_DIR/$APP/storage/backups/nightly.sql"
DB="$BACKUP_DIR/$APP-db-$STAMP.sql.gz"
FILES="$BACKUP_DIR/$APP-files-$STAMP.tar.gz"
mkdir -p "$BACKUP_DIR"
trap 'rm -f "$DB.partial" "$FILES.partial"' EXIT
sudo -u www-data php "$APP_DIR/$APP/craft" db/backup "$DUMP" --overwrite
tail -n 1 "$DUMP" | grep -q "Dump completed"
gzip -c "$DUMP" > "$DB.partial"
mv "$DB.partial" "$DB"
tar -czf "$FILES.partial" --exclude="$APP/vendor" --exclude="$APP/node_modules" \
--exclude="$APP/storage/runtime" --exclude="$APP/storage/logs" \
--exclude="$APP/storage/backups/*" --exclude="$APP/web/cpresources" \
-C "$APP_DIR" "$APP" || [ $? -eq 1 ]
mv "$FILES.partial" "$FILES"
find "$BACKUP_DIR" -name "$APP-*.gz" -type f -mtime +"$KEEP_DAYS" -deletechmod 755 /usr/local/bin/craft-backup.shPATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
20 3 * * * root flock -n /run/lock/craft-backup.lock /usr/local/bin/craft-backup.sh >> /var/log/craft-backup.log 2>&1Overwriting one fixed file keeps storage/backups/ from filling up; the dated copies live in /var/backups/craft. tar's exit code 1 means a file changed while it was read and counts as success. Copy the backups off the server each night, for example with rclone to Backblaze B2; the cron guide covers alerts.
Common errors
| Error | Fix |
|---|---|
[file or directory] doesn't exist or isn't writable by PHP. Please fix that. | Give the PHP user write access to storage/, web/cpresources/ and config/license.key. |
| The installer appears instead of the control panel | Craft reached a database without its tables: check CRAFT_DB_DATABASE and CRAFT_DB_TABLE_PREFIX. |
| Everyone is logged out, or encrypted values fail | CRAFT_SECURITY_KEY differs from the original. Restore the original .env. |
The backup fails because mysqldump or pg_dump is not found | Install the database client tools so they are in the PATH of the user running Craft. |
... already exists. Retry with the --overwrite flag to overwrite it. | Add --overwrite, or let Craft name the file. |
Backup path doesn't exist: ... | The path is wrong, or the user running Craft cannot read the file. |
craft up wants to apply project config changes | The YAML on disk differs from the database. Check out the commit that matches the backup. |
| Images return 404 | A local asset folder was not restored or is unreadable. index-assets/all lists what is missing. |
Frequently asked questions
- Is project config a backup of my Craft site?
- No. It records settings and structure, such as sections, fields and volumes, but not content: entries, assets and users live only in the database.
- What happens if I lose CRAFT_SECURITY_KEY?
- The database restores, but everything encrypted with the old key becomes unreadable and every user is logged out. The key cannot be recovered from the data.
- Where does Craft keep its database backups?
- In storage/backups/, from db/backup, the Database Backup utility and automatic backups before updates. It keeps the newest 20 by default; maxBackups changes that.
- Does php craft db/backup lock the site?
- Craft adds --single-transaction only on MariaDB and on MySQL 8.0 releases before 8.0.32. On later MySQL, mysqldump's default table locks apply, so writes wait while it reads. For a large, busy site, run mysqldump with --single-transaction yourself.
How this was checked
Commands, limits and prices were checked against these official pages, on October 3, 2026:
- Craft CMS 5 docs: Console Commands
- Craft CMS 5 docs: General Config Settings
- Craft CMS 5 docs: Configuration
- Craft CMS 5 docs: Directory Structure
- Craft CMS 5 docs: Project Config
- Craft CMS 5 docs: Deployment
- Craft CMS 5 docs: Assets
- Craft CMS 5 docs: Requirements
- Craft Knowledge Base: Troubleshooting Failed Installation
- Craft starter project: .env.example.production (5.x and 4.x branches)
- craftcms/cms source: DbController (db/backup and db/restore)
- craftcms/cms source: Connection (backup file name, skipped tables, maxBackups)
- craftcms/cms source: MySQL default backup command
- craftcms/cms source: IndexAssetsController (index-assets defaults)
- MySQL 8.4 Reference Manual: mysqldump