Drupal: Backups and Disaster Recovery
|
This content was generated with the assistance of AI and should be verified against the official Drupal documentation before being relied on in production. This sub-section’s Drupal bibliography lists the reference material consulted while preparing these pages. |
A Drupal site is not one thing to back up. It is a database that holds content and, until it is exported, configuration; a directory of uploaded files; a codebase assembled by Composer; and a handful of environment-specific secrets. Losing any one of them while keeping the others produces a site that does not run, or runs with missing images or missing settings. This page explains what each part is, how to capture and restore it with Drush, DDEV and the contributed Backup and Migrate module, how to rehearse a restore, and how to refresh a staging site from production without copying personal data along with it.
It was checked on 2026-10-30 against Drupal 11.4.x, Drush 13 and 14 and PHP 8.3 or later. The commands also work on Drupal 10.2 and later with Drush 13; Drupal 10 reaches end of life on 2026-12-09, so new sites should start on Drupal 11 (see Versions, Release Cycle and Support). Drupal core has no backup feature of its own. The audience is the site administrator and the developer who operates the Meridian Museum site; the update workflow that depends on a backup is in Updates, Composer and Security Advisories.
What to back up
The Drupal User Guide concept page Data Backups lists four things: code and settings, the database, uploaded files and custom software. For a Composer-managed site (the only supported way to manage code since Drupal 11) the list is clearer when split by how each part can be recovered:
| Part | What it holds | How it is recovered |
|---|---|---|
Database |
Content entities and their revisions, users, roles, comments, the key/value and state tables, the |
Only from a database backup. This is the one part that cannot be rebuilt from anywhere else. |
Public and private files |
Uploaded images, documents and media files ( |
Only from a files backup, except for derivatives and aggregates, which Drupal regenerates on demand. The private directory is the one most often forgotten because it sits outside the web root. |
Configuration |
Exported YAML in the sync directory (see Configuration Management). |
From version control. A site that exports its configuration can recover it from Git; a site that never exports it has configuration only in the database. |
Code |
|
From version control plus |
Environment settings |
|
From a secret store. They must not be in the Git repository and are not in the database. |
Three points follow.
-
The database holds more than content. Until configuration is exported and imported through the sync directory, the active configuration lives in the
configtable. A restored database brings back the configuration that was active when the dump was taken, which is why a restore on a different code version can break (see the pitfalls below). -
Private files need their own path. The
file_private_pathsetting points outside the web root, so a backup of the document root alone omits it. Meridian’s loan agreements and donor documents are stored there. See File System and Private Files. -
Code is in Git, secrets are not. The recoverable parts are the ones in version control.
hash_saltand database credentials have to come from wherever the deployment keeps secrets, and a restore plan must say where.
Recovery point and recovery time objectives
Two numbers decide how much backup is enough. The recovery point objective (RPO) is how much content the museum can afford to lose: a site that changes twice a week can live with a nightly database dump, while a ticket-sales site cannot. The recovery time objective (RTO) is how long the site may be down. A dump that restores in two minutes on a laptop may take an hour on a large production database, so the restore has to be timed (see Restore drills). The User Guide’s advice to keep a series of backups rather than overwriting one applies here: a problem such as a bad migration may go unnoticed for days, and the only useful backup is one from before it.
Backing up the database with Drush
drush sql:dump (alias sql-dump) exports the database that settings.php points to, using mysqldump for MySQL and
MariaDB, pg_dump for PostgreSQL and the sqlite3 tool for SQLite. The client tools must be installed on the machine
that runs Drush. Without options it writes the dump to standard output.
# Dump to a file outside the web root, compressed.
vendor/bin/drush sql:dump --gzip --result-file=../backups/meridian-$(date +%F).sql
# Let Drush pick a dated file name under ~/drush-backups.
vendor/bin/drush sql:dump --gzip --result-file=auto
# Leave out the data of tables that are only caches, but keep their structure.
vendor/bin/drush sql:dump --structure-tables-list=cache_*,sessions,watchdog --gzip --result-file=../backups/meridian-lean.sql
Source: Drush: sql:dump
The options that matter for backups:
| Option | Effect |
|---|---|
|
Writes the dump to a file. The path is relative to the Drupal root. The value |
|
Compresses the dump with |
|
Comma-separated table names to dump as structure only, to leave out entirely, or to dump exclusively. |
|
The same selection by a key in the |
|
Dumps data without schema statements. |
|
Orders rows by primary key so consecutive dumps diff well. Slower; MySQL only. |
|
Passes extra arguments to |
|
Select another connection key from |
Skipping the data of cache tables makes dumps smaller and quicker and loses nothing, because caches are rebuilt on
demand. Do not skip the data of anything else without a reason: sessions only matters if users should stay logged in
after a restore, but key_value, state and semaphore can hold data that the next cron run expects.
Restoring a database dump
A dump is restored through the SQL client that Drush wraps. drush sql:cli opens that client with the site’s connection
details, so a restore does not need the password on the command line.
# Drop everything first so stale tables do not survive, then import.
vendor/bin/drush sql:drop --yes
gunzip -c ../backups/meridian-2026-10-30.sql.gz | vendor/bin/drush sql:cli
# Rebuild caches and apply any pending database updates for the code that is deployed.
vendor/bin/drush deploy
Source: Drush: sql:cli
drush deploy runs updatedb, config:import, cache:rebuild and deploy hooks, so it also brings a restored database
in line with the code that is currently checked out. Its steps are described in
Drush. If the dump is older than the code, run it so that update functions are applied; if
the dump is newer than the code (a rollback of code only), restore the matching code first, because a Drupal database
cannot be downgraded.
Backing up code, files and database in one archive
The Drush commands archive:dump (alias ard) and archive:restore (alias arr) produce and consume a single
.tar.gz archive. The official description of archive:dump is "Backup your code, files, and database into a single
file". The archive holds a database dump, the codebase (excluding some paths, see below), the Drupal files directory and
a MANIFEST.yml that records what it contains and which Drush version generated it.
# Full archive: code, files and database.
vendor/bin/drush archive:dump --destination=/var/backups/meridian/meridian-2026-10-30.tar.gz --overwrite \
--description="Before the 11.4.x update" --tags=pre-update
# Only the database, or only the files.
vendor/bin/drush archive:dump --db --destination=/var/backups/meridian/db-only.tar.gz
vendor/bin/drush archive:dump --files --destination=/var/backups/meridian/files-only.tar.gz
# Leave out per-site settings so the archive contains no credentials.
vendor/bin/drush archive:dump --exclude-code-paths='web/sites/.+/settings.php,web/sites/.+/settings.local.php' \
--destination=/var/backups/meridian/no-secrets.tar.gz
Source: Drush: archive:dump
The options are:
| Option | Effect |
|---|---|
|
Restrict the archive to one component. With none of them, all three are included. |
|
The full path and file name of the archive. A relative path is resolved from the Drupal root. Without it the archive is written to the configured temporary directory, which is easy to lose. |
|
Replaces the destination if it already exists. Without it the command fails. |
|
Text and comma-separated labels stored in the manifest. |
|
Comma-separated paths or regular expressions to leave out of the code part. |
|
Replaces symlinks with copies of the targets. By default only symlinks that point outside the project root are converted. |
|
The same database selection options as |
archive:restore reverses it. Its arguments are the path of a .tar.gz archive (or a directory holding the components)
and an optional destination site alias.
# Restore everything into a new directory, with a fresh database connection.
vendor/bin/drush archive:restore /var/backups/meridian/meridian-2026-10-30.tar.gz \
--destination-path=/var/www/meridian-restore \
--db-url="mysql://meridian:${MERIDIAN_DB_PASSWORD}@127.0.0.1/meridian_restore"
# Restore only the database from an archive into the current site.
vendor/bin/drush archive:restore /var/backups/meridian/meridian-2026-10-30.tar.gz --db
Source: Drush: archive:restore
Options of note: --destination-path (where code and files are restored), --code, --files and --db (restore one
component), --code-source-path, --files-source-path and --db-source-path (take that component from a directory or
dump file instead of the archive), --files-destination-relative-path, --overwrite, --site-subdir (the site directory
that receives the generated settings.local.php, default by default) and the --db-* connection options and
--db-url. When connection options are given, the restore writes the database connection into settings.local.php
(disabled with --no-setup-database-connection).
Limits of the archive commands
-
The archive is a convenience for small and medium sites and for moving a site between machines. It is a full copy each time, with no incremental mode and no encryption, and it is written by the same account that runs Drush.
-
The code part contains
vendorand core unless it is excluded, so archives are large, and they containsettings.phpunless it is excluded, so they hold database credentials. Treat an archive as a secret: restrict its permissions, and do not leave it in a web-accessible or shared directory. -
archive:dumpis not a substitute for an off-site copy. A backup that lives on the same disk as the site dies with it. -
Private files outside the project root are not part of the archive unless the files path is inside it. Back up the private directory separately, and verify that it is in the archive before relying on it.
-
Check
drush archive:restore --helpon the installed Drush: the options are documented per release, and the Drush 12.4 documentation lists a smaller set than the command in Drush 13.
Backing up files
Files are copied with the tool that suits the storage: rsync for a server, an object-storage versioning or lifecycle
rule for S3-compatible storage, a volume snapshot for a container volume.
# Public and private files to a backup host, keeping deleted files for 30 days.
rsync -a --delete --backup --backup-dir="/srv/backups/meridian-files-deleted/$(date +%F)" \
/var/www/meridian/web/sites/default/files/ backup.example.org:/srv/backups/meridian-files/public/
rsync -a /var/www/private/ backup.example.org:/srv/backups/meridian-files/private/
The generated image derivatives (styles/) and aggregated assets (css/, js/) can be left out, since Drupal
regenerates them; --exclude=styles --exclude=css --exclude=js keeps the copy small. A flush of image styles after the
restore is not needed, because missing derivatives are produced on the first request.
Backup and Migrate
The contributed Backup and Migrate module (drupal/backup_migrate) adds
a UI under Administration > Configuration > Development for dumping and restoring the database, the public and private
files and, if configured, the code, with compression (gzip, bzip or zip), selectable tables and scheduled automatic
backups. The Drupal User Guide points to it for automated backups. On the check date the project page lists release
5.1.5 as a stable release that works with Drupal ^10.3 || ^11 and is covered by the Drupal Security Team.
ddev composer require 'drupal/backup_migrate:^5.1'
ddev drush pm:install backup_migrate
ddev drush backup_migrate:backup --help
Source: Backup and Migrate project page
What to know before relying on it:
-
Destinations. For Drupal 10 and later the supported destination on the project page is the local file system. Sending backups to remote storage needs a separate add-on module (the project page names Backup Migrate Flysystem). Backups stored on the web server’s own disk are not a disaster-recovery copy.
-
Where the files land. Point the destination at a directory outside the web root, or confirm the module’s
.htaccessprotection applies, because a database dump served over HTTP exposes every user’s hashed password and every unpublished node. -
Scheduling uses cron. Scheduled backups run when cron runs (see Cron, Queues and Maintenance); a site whose cron is broken silently stops producing backups.
-
Code stays in Git. The module is a convenience for the database and files. The code of a Composer site is restored with
composer install, not from a module-made archive. -
Check the project page before installing. Release numbers, supported core versions and security coverage change; the figures above are those on the check date.
Staging refreshes and sanitisation
A staging site is only useful if it resembles production, so teams copy the production database to it regularly. That copy
contains real e-mail addresses, password hashes, form submissions, loan agreements and revision logs, and staging is
usually less protected than production. drush sql:sanitize (aliases sqlsan and sql-sanitize) removes or obfuscates
user data in the copy.
By default it randomises every account’s password and replaces each e-mail address with user+%uid@localhost.localdomain
(the documented default pattern; %uid, %mail and %name are available placeholders). Contributed modules and custom
code add their own sanitisers by implementing the Drush sanitize command hook, so Webform submissions, for example, can
be cleared as part of the same command.
# On the staging host: pull production's database, sanitise it, then apply the code's updates.
vendor/bin/drush sql:sync @meridian.prod @self --yes
vendor/bin/drush sql:sanitize --sanitize-email='user+%uid@example.invalid' --yes
vendor/bin/drush deploy
# Keep the administrators' accounts usable by exempting a role.
vendor/bin/drush sql:sanitize --ignored-roles=administrator --yes
Source: Drush: sql:sanitize
| Option | Effect |
|---|---|
|
By default passwords are randomised. |
|
The replacement address pattern, or |
|
Comma-separated fields exempt from sanitisation. |
|
Comma-separated roles whose users are exempt from sanitisation. |
Source: Drush: sql:sync
Pitfalls of a refresh:
-
Sanitising after the data has been exposed. If the unsanitised dump is copied to a laptop or a shared bucket before
sql:sanitizeruns, the damage is done. Sanitise on a host that already has the data, or dump with a sanitiser in the pipeline, and delete the intermediate file. -
Mail that really sends. A staging site that keeps real addresses and sends mail will e-mail visitors. Sanitise, and also route outgoing mail to a catch-all service in the staging
settings.php(see settings.php and Environments). -
Fields the sanitiser does not know. Custom fields such as the donor’s phone number on a Meridian
donorprofile are not touched by core sanitisation. Review the schema for personal data in custom tables and fields, and sanitise those with a custom sanitiser or an SQL step. -
Configuration overwritten by the dump. The database carries production’s active configuration, so run
drush config:import(insidedrush deploy) afterwards to apply the staging configuration from Git, and use configuration splits or$configoverrides for values such as API endpoints. -
Files are not sanitised. Copying production’s
filesdirectory copies uploaded documents. Copy only the public directory, or none, and let missing images fall back to placeholders.
Local snapshots with DDEV
On a developer machine, the DDEV snapshot commands are the fast way to save and roll back the local database before a
risky change such as a schema update. They work on the database only, and they are not a backup of anything that
matters in production.
ddev snapshot --name before-meridian-collection-update
ddev drush updatedb --yes
# Something went wrong: return to the saved state.
ddev snapshot restore before-meridian-collection-update
ddev export-db --file=../backups/meridian.sql.gz
ddev import-db --file=../backups/meridian.sql.gz
Source: DDEV: Commands
Snapshots live in the project’s .ddev area, are tied to the database type and version of the project, and are removed
with ddev delete, so export a dump if the data must survive deleting the project.
Restore drills
A backup that has never been restored is an assumption. Schedule a drill, at least each quarter and after any change to hosting, and follow a written checklist.
-
Take the most recent production backup, as the person on call would, from the real off-site location.
-
Restore the database into an empty database, and the files into an empty directory, on a clean machine or a DDEV project. For an archive, use
drush archive:restorewith--destination-path. -
Run
composer install --no-dev, supply the secrets from the secret store, and rundrush deploy. -
Check that the front page renders, that an editor can log in and edit, that images and a private file download work, that
drush statusreports a successful bootstrap, and that the Status report has no new errors (see Logging, Status Report and Troubleshooting). -
Record the elapsed time against the RTO and the age of the backup against the RPO, then fix whatever was missing and update the runbook.
#!/usr/bin/env bash
# Restore-drill helper for the Meridian Museum site. Run on a scratch machine.
set -euo pipefail
ARCHIVE="${1:?path to .tar.gz archive}"
TARGET="${2:-/tmp/meridian-drill}"
start=$(date +%s)
vendor/bin/drush archive:restore "$ARCHIVE" --destination-path="$TARGET" \
--db-url="mysql://meridian:${MERIDIAN_DB_PASSWORD}@127.0.0.1/meridian_drill"
cd "$TARGET"
composer install --no-dev --no-interaction
vendor/bin/drush deploy --yes
vendor/bin/drush status --field=bootstrap
echo "Restore took $(( $(date +%s) - start )) seconds"
Source: Drush: archive:restore
Common errors and pitfalls
-
Backing up only the database. Content is restored but every image is missing, because the files were not copied.
-
Forgetting private files and secrets. They are in neither the database nor Git.
-
Dumps in the web root. A
.sqlfile inweb/can be downloaded by anyone. Keep backups outside the document root and out of the Git repository; the same applies tosettings.php.bakcopies (see Security Hardening). -
Restoring over a running site. Put the site in maintenance mode (
drush maint:set 1) while a restore runs, then rebuild caches and take it out again. -
Restoring a database onto older code. The schema version recorded in the database may be newer than the code knows about, and Drupal does not support downgrades. Restore the code that matches the dump, or restore an older dump.
-
Configuration drift after a restore. A restored database brings back the active configuration of the dump date. If the sync directory in Git has moved on,
drush config:importchanges it again, so import deliberately and review the difference first. -
A truncated or failed dump. A full disk produces a partial file without failing loudly in every shell. Check the exit code, the file size, and the last line of the dump.
-
Different MySQL, MariaDB or PostgreSQL versions. Dumps move between MySQL and MariaDB versions in the same family most reliably; a move between database engines is a migration, not a restore.
-
Backups never tested, never rotated. Unbounded copies fill the disk, and unrotated ones are stolen as often as current ones. Define retention (for example daily for a week, weekly for a month) and delete the rest.
-
Unencrypted off-site copies. A database dump contains password hashes, e-mail addresses and unpublished content. Encrypt it in transit and at rest.
Version notes
-
drush archive:dumpanddrush archive:restoreare documented from Drush 11 onwards and are present in Drush 13 and 14; Drush 13 serves Drupal 10.2 and later and Drush 14 serves Drupal 11.3 and later. Thedrush.services.ymlfile was removed in Drush 13, which matters only to authors of custom Drush commands. -
The Backup and Migrate 5.1 branch works with Drupal ^10.3 || ^11 on the check date. Drupal 12 is scheduled for the week of 2026-12-07 and requires PHP 8.5; check the project page for Drupal 12 support before upgrading a site that depends on it (see Preparing for Drupal 12).
-
Automatic Updates, which is contributed and limited to patch-level core updates, has no rollback after a completed update; take a database and files backup first (see Automatic Updates and Project Browser).
Corrections to older books and tutorials
Older books such as Drupal 10 Development Cookbook (2023) and Drupal 10 Masterclass (2023) target Drupal 10.0 and 10.1,
Drush 12 and PHP 8.1. Where they describe backing up a site, they predate Drush 13 and 14 and the Composer-only code
workflow: code is restored from composer.lock rather than copied, drush.services.yml is no longer how Drush commands
are registered, and drush archive:dump is the single-file option that older tutorials replace with separate tar and
mysqldump steps. Tutorials that suggest the Module and theme upload screen or the UI core updater to restore code
describe features removed in Drupal 11.0, 11.1 and 11.2. Drupal 10 itself ends on 2026-12-09.