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 watchdog log, caches and sessions, and the active configuration.

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 (sites/default/files or a configured path), generated image-style derivatives and aggregated CSS and JavaScript, and the private file directory.

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

composer.json, composer.lock, custom modules such as meridian_collection, the meridian theme and patches.

From version control plus composer install. The vendor directory and the web-root core, contributed projects and libraries are rebuilt from the lock file and are not worth backing up.

Environment settings

settings.php overrides, settings.local.php, the hash_salt, the database credentials, API keys and the deployment’s environment variables.

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 config table. 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_path setting 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_salt and 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.

flowchart LR subgraph Site[Production site] DB[(Database)] FILES[Public and private files] CODE[Code in Git plus composer.lock] SEC[Secrets and environment] end DB -->|drush sql:dump| B1[Nightly dump, off-site copy] FILES -->|rsync or object storage versioning| B2[Files copy] CODE -->|git push| B3[Repository host] SEC -->|secret store| B4[Secret manager] B1 --> R{Restore drill} B2 --> R B3 --> R B4 --> R R -->|passes: site renders and logs in| OK[Recovery plan proven]

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

--result-file=PATH

Writes the dump to a file. The path is relative to the Drupal root. The value auto creates a date-named file under ~/drush-backups.

--gzip

Compresses the dump with gzip, which must be on the PATH. The file name gains a .gz suffix.

--structure-tables-list, --skip-tables-list, --tables-list

Comma-separated table names to dump as structure only, to leave out entirely, or to dump exclusively.

--structure-tables-key, --skip-tables-key, --tables-key

The same selection by a key in the $structure_tables, $skip_tables and $tables arrays of a Drush configuration file (drush.yml), so the list is defined once for the whole team.

--data-only

Dumps data without schema statements.

--ordered-dump

Orders rows by primary key so consecutive dumps diff well. Slower; MySQL only.

--extra-dump=ARGS

Passes extra arguments to mysqldump or the equivalent, for example --single-transaction on a busy InnoDB database.

--database, --target, --db-url

Select another connection key from settings.php, or a Drupal 6 style URL, for sites with several database connections.

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

The options are:

Option Effect

--code, --files, --db

Restrict the archive to one component. With none of them, all three are included.

--destination=PATH

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.

--overwrite

Replaces the destination if it already exists. Without it the command fails.

--description, --tags

Text and comma-separated labels stored in the manifest.

--exclude-code-paths

Comma-separated paths or regular expressions to leave out of the code part.

--convert-symlinks

Replaces symlinks with copies of the targets. By default only symlinks that point outside the project root are converted.

--extra-dump, --skip-tables-*, --structure-tables-*, --tables-*

The same database selection options as sql:dump.

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

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 vendor and core unless it is excluded, so archives are large, and they contain settings.php unless 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:dump is 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 --help on 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

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 .htaccess protection 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
Option Effect

--sanitize-password

By default passwords are randomised. no leaves them unchanged; any other value sets every password to that value. Use a throwaway value only on a private staging site.

--sanitize-email

The replacement address pattern, or no to keep addresses unchanged.

--allowlist-fields

Comma-separated fields exempt from sanitisation.

--ignored-roles

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:sanitize runs, 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 donor profile 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 (inside drush deploy) afterwards to apply the staging configuration from Git, and use configuration splits or $config overrides for values such as API endpoints.

  • Files are not sanitised. Copying production’s files directory 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.

  1. Take the most recent production backup, as the person on call would, from the real off-site location.

  2. 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:restore with --destination-path.

  3. Run composer install --no-dev, supply the secrets from the secret store, and run drush deploy.

  4. Check that the front page renders, that an editor can log in and edit, that images and a private file download work, that drush status reports a successful bootstrap, and that the Status report has no new errors (see Logging, Status Report and Troubleshooting).

  5. 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"

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 .sql file in web/ can be downloaded by anyone. Keep backups outside the document root and out of the Git repository; the same applies to settings.php.bak copies (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:import changes 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:dump and drush archive:restore are 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. The drush.services.yml file 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.