In short
- A replica copies every change within seconds, mistakes included, and a snapshot usually lives on the same storage or in the same account. Only a backup, a separate copy with history kept somewhere else, survives deletion, bad data, ransomware and losing the provider.
- Follow 3-2-1: three copies of the data, on two kinds of storage, one of them off site. Make the off-site copy something the server itself cannot delete.
- Copying the files of a running database gives you a torn copy. Let the database make the copy: pg_dump, mysqldump with --single-transaction, or a base backup plus archived logs once the database is large.
- Decide how much recent work you can afford to lose (the recovery point) and how long you can be down (the recovery time), then choose the schedule and retention that meet both.
- Encrypt backups before they leave the server, raise an alarm when a backup does not arrive, and restore one onto a spare server regularly. Until you have restored from it, a backup is a hope.
On this page
- Replica, snapshot, backup: three copies that fail differently
- The 3-2-1 rule, translated for a server
- Why a copy of a running database can be useless
- Dumps: let the database copy itself
- RPO and RTO: how much you can lose, and for how long
- Retention: keep enough history to go back past the problem
- Encrypt it, automate it, and listen for silence
- The restore drill: proving the backup works
- Questions
- Sources
On 31 January 2017 an engineer at GitLab, trying to get a lagging database replica back in sync, ran the command that empties a PostgreSQL data directory. It ran on the primary, not the replica. The engineer stopped it a second or two later, and by then about 300 GB of data was gone.1 The team went for the backups. The daily pg_dump job had been failing, because it ran version 9.2 of pg_dump against a 9.6 database, and the emails reporting the failure were being rejected by the receiving mail server. The storage bucket meant to hold the dumps was empty. What saved most of the data was an LVM snapshot an engineer had taken by hand six hours earlier to refresh a staging database.1
GitLab lost the changes made between 17:20 and midnight UTC, roughly 5,000 projects, 5,000 comments and 700 new user accounts, and copying the data back took around 18 hours because the disks involved were throttled to about 60 Mbps.1 It did not lack copies. It had several kinds, and the one that worked was the one nobody had planned as a backup. A backup is real only once you have restored from it.
Replica, snapshot, backup: three copies that fail differently
A replica is a second live database on another server that applies every change the primary makes. PostgreSQL's documentation states the default plainly: a standby replays the primary's log as fast as it can. It offers a setting to hold changes back on purpose, since a standby that runs behind leaves you time to catch a mistake before the standby applies it too.3 That is the whole story of a replica. It protects you from a dead disk or a dead server, because it can take over, and from nothing that arrives as a change. A DROP TABLE, a bad migration or an attacker's DELETE is a change like any other, and the replica applies it within seconds.
A snapshot at the storage level, the kind LVM or ZFS takes, freezes a volume as it was at one instant, and it is quick because it copies almost nothing. In the Linux device-mapper's snapshot target, only the chunks that change after the snapshot are copied to a separate copy-on-write device, and reads of everything else come from the original disk.4 MySQL's manual describes the snapshots that LVM or ZFS take the same way: a logical copy at a point in time, without a physical copy of the whole file system.5 Such a snapshot is a view of the original, not a second copy of it. If the disk underneath fails, or the server or account it lives in is lost, the snapshot goes with it.
Some providers copy a snapshot to separate storage, and then it survives a dead disk, but it still lives in the same account with the same provider. GitLab's snapshot helped because it had been copied to a staging host, and the restore came from that copy.1 Ask where yours are kept. What a snapshot does well is undo something recent on a machine that still exists: a failed upgrade, a deleted directory.
A backup, in CISA's words, is "a secure copy of your business's critical data, stored separately from your primary systems".2 Separately is the word that matters: another machine, other storage, ideally another place. A useful backup also has history, many restore points rather than one, so you can go back to before a problem you noticed late.
The grid at the top of this page sets the copies against the five ways data gets lost. Two rows deserve a second look. A backup kept on the same server dies with the server and can be deleted by anyone who gets root on it. And an off-site backup is only as safe as the credentials the server holds for it: CISA's ransomware guide warns that many ransomware variants look for reachable backups and delete or encrypt them, so the ransom becomes the only way back.6
The 3-2-1 rule, translated for a server
CISA's version of the rule is three copies of important files, on two different types of storage media, with one copy stored off site.2 CISA's example of two media is a hard drive and the cloud. On a server the rule maps like this.
Copy one is the live data on the server's disk: the database, uploaded files, configuration. Snapshots on that disk do not add a copy, because they share its blocks, and even a snapshot the provider keeps on separate storage sits in the same account as the server, so it cannot be the off-site copy. Copy two is a backup repository on storage that is not that disk: a second machine, a storage server, object storage. The point of "two kinds of media" is that no single failure takes out both. Copy three is off site: another region, and preferably another provider and another account. CISA suggests spreading cloud backups over more than one vendor in case every account with one vendor is affected at once.6
The last detail in the drawing is the easiest to get wrong. If the server holds credentials that can delete the off-site copy, an attacker who takes the server holds them too. CISA's guidance is offline, encrypted backups, and delete protection or object lock on the storage that holds them.6
Why a copy of a running database can be useless
A database's state at any instant is spread across several files and some memory. One sale changes an order page and a stock page in memory, writes a record of both to the write-ahead log, and commits once that record is on disk. The changed pages reach their data files whenever the database flushes them. The only rule is that their log records get there first,12 so a page can land before the commit or long after it. A copy is consistent only if every part of it comes from the same instant, or if the database can repair it back to one.
Copying the files one by one, with cp, tar or rsync, breaks that. PostgreSQL's manual says a file-level backup needs the server shut down, and that even refusing all connections is not enough, partly because tar and similar tools do not take an atomic snapshot of the file system and partly because the server buffers data internally.7 SQLite's documentation describes the same failure for its single-file database: a copy made while a transaction is under way can end up holding part of the change and not the rest, which leaves the file corrupt.8
Crash-consistent: the power-cut copy
A snapshot of the whole volume taken at one instant is a different thing. PostgreSQL's manual accepts a frozen snapshot of a running server's volume, with conditions: the restored database will think the previous server crashed and will replay its write-ahead log, so the log has to be inside the snapshot, and if the data and the log sit on different disks the snapshots must be simultaneous, which may not be possible.7 This is called crash-consistent: the copy is as good as the disk after a power cut, which a well-built database is designed to survive. It is not as good when the data spans volumes that were not frozen together, or when the application keeps related state in two places, say the database on one disk and uploaded files on another.
Application-consistent: the database takes part
The stronger kind involves the application. Either the database writes the copy itself, which is what a dump is, or it is told to flush and hold still before the snapshot. On KVM hosts the QEMU guest agent inside the virtual machine has a command that flushes the guest's local file systems to disk and pauses writes to them,10 and it can run a hook script with a freeze or thaw argument,11 which is where a database gets told to checkpoint first. Whether a provider's snapshots go through that step is a question to ask, not something to assume. The dependable route for a database is a dump that you schedule yourself.
Dumps: let the database copy itself
A dump asks the database for its contents through the same interface your application uses, inside one transaction. PostgreSQL's pg_dump produces an internally consistent dump that represents the database at the moment the dump began, and it does not block other work while it runs, apart from operations that need an exclusive lock, such as most forms of ALTER TABLE.13 Its output can generally be loaded into newer PostgreSQL versions, whereas file-level copies and archived logs are tied to the server's version,13 in practice to its major version: minor releases never change the storage format, and major releases may.14
Two details catch people. pg_dump covers one database and leaves out roles and tablespaces, because those belong to the whole cluster,13 so dump them separately with pg_dumpall --globals-only.16 Without them, a restore on a fresh server cannot give objects their original owners and permissions, and the application's login does not exist either.13 And the custom format, -Fc, compresses as it writes and lets pg_restore bring back single tables, which a plain SQL file cannot.13
PostgreSQL's own reference page adds a warning worth taking seriously: except in simple cases, pg_dump is generally not the right choice for regular backups of production databases.15 The alternative is a base backup of the data files plus continuous archiving of the write-ahead log. Replaying that log can stop at any point, which gives a consistent database as it was at that moment: point-in-time recovery.17 A nightly dump is a sound start for a small shop or a side project. When the dump takes too long to restore, or a day of lost work is too much, that is the next step.
For MySQL, the equivalent is mysqldump --single-transaction. It sets the isolation level to REPEATABLE READ and starts a transaction before dumping, so InnoDB tables come out in the state they had at that moment without blocking applications.18 The limits are in the same paragraph of the manual: only InnoDB tables are consistent, MyISAM or MEMORY tables can still change during the dump, and no other connection should run ALTER, CREATE, DROP, RENAME or TRUNCATE TABLE while it runs.18 Stored routines and events are left out unless you ask for them with --routines and --events, and since MySQL 8.4 that holds for --all-databases too.18
pg_dump -Fc -f /var/backups/shop.dump shoppg_dumpall --globals-only -f /var/backups/globals.sqlmysqldump --single-transaction --routines --events --databases shop > /var/backups/shop.sqlrm -f /var/backups/app.db && sqlite3 /srv/app/app.db "VACUUM INTO '/var/backups/app.db'"| Database | Consistent copy while running | Restore with | Watch for |
|---|---|---|---|
| PostgreSQL | pg_dump -Fc, plus pg_dumpall --globals-only | pg_restore | Roles are not in the dump. Large, busy databases need a base backup and log archiving. |
| MySQL (InnoDB) | mysqldump --single-transaction | The mysql client | Only InnoDB is consistent. No schema changes during the dump. Slow to restore at size. |
| SQLite | VACUUM INTO a new file | Copy the file back with the application stopped | Never copy the live database file while it is being written. |
SQLite's documentation lists VACUUM INTO among the safe ways to copy a live database,8 though it fails if the target file already exists and is not empty, hence the rm before it.9 MySQL's manual names the mysql client as the way to load an SQL-format dump.5 For MySQL the size warning matters most. The manual says mysqldump is not meant as a fast or scalable way to back up large amounts of data, because restoring it replays every statement, with the inserts and index builds that implies, and it points large installations to physical backups instead.18 How big is too big is a number your restore drill will give you. How much memory and disk the database itself needs is covered in sizing a database server.
RPO and RTO: how much you can lose, and for how long
Two numbers decide what a backup setup has to do. NIST defines the recovery point objective as the point in time to which data must be recovered after an outage,19 and the recovery time objective as how long systems can be in recovery before the business is harmed.20 Put simply, the RPO is how much recent work you are prepared to lose and the RTO is how long you can be down. CISA suggests using scheduled recovery tests to refine both.2
Take a small shop on PostgreSQL. A pg_dump runs at 02:00 and produces a 6 GB file that goes off site. At 16:40 the server's disk fails.
The recovery point is fixed by the schedule. The newest copy is from 02:00, so 14 hours 40 minutes of orders are gone, and the worst case for a nightly job is just under a day. Dumping every hour caps the loss at an hour; here it would be the 40 minutes since 16:00. Archiving PostgreSQL's log continuously goes further: the manual calls an archive_timeout of a minute or so usually reasonable, which puts the loss at about a minute.17
The recovery time is a sum, and it pays to write it out. Twenty-five minutes pass before anyone notices, at 17:05. Getting a fresh server with PostgreSQL installed takes, say, 20 minutes if it is scripted. Fetching the 6 GB dump is quick on a fast link: 1 Gbit/s is 125 MB/s, so 6,000 MB takes 48 seconds. At the 60 Mbps GitLab's disks were throttled to, about 7.5 MB/s, the same file takes 800 seconds, over 13 minutes, and every 100 GB takes close to four hours. Say fetching and restoring take 70 minutes on a fast link. Most of that is pg_restore loading every row and rebuilding every index. Fifteen minutes of checks and switching traffic, and the shop is back at 18:50, 2 hours 10 minutes after the failure: 25 + 20 + 70 + 15 = 130 minutes. (For converting link speeds into transfer times, see bandwidth versus data transfer.)
- Orders lost by restoring the 02:00 dump
- 14 h 40min
- Down, from the failure to back online
- 2 h 10min
- Fetching 6 GB at 1 Gbit/s, best case
- 48s
Notice which step is the biggest and which one you cannot work out on paper. The restore itself dominates, and its length depends on your data, your indexes and your disks. The only way to know it is to time a real restore.
Retention: keep enough history to go back past the problem
Many problems are found late: a bad import, a bug that has been writing wrong totals for a week, an intruder who sat quietly before acting. If you keep only last night's backup, every copy you own already contains the problem by the time you notice it. CISA's advice is to make sure you can roll data back at least seven days.2
The usual answer keeps many recent restore points and fewer old ones. In restic, an open-source backup tool, that is a forget policy such as --keep-daily 7 --keep-weekly 5 --keep-monthly 12. The tiers overlap: restic's documentation points out that seven daily snapshots already include one weekly one.21 forget only removes the snapshot records; prune then deletes the data that nothing references any more, and --prune runs both together.21
Twenty-one full copies of the 6 GB dump would take 126 GB. A deduplicating tool stores much less: restic cuts files into variable-length pieces by their content, and for a modified file only the modified pieces are saved in the next backup.22 That only works if most of the bytes are the same night after night, and a compressed dump defeats it. pg_dump's custom format compresses its table data with gzip by default,15 and a compressed stream differs from the first changed byte onward, so a few updated rows early in a large table make most of that table look new. Turn pg_dump's compression off with -Z 015 and let restic compress instead, which it does by default in its current repository format.2423 How much deduplication then saves depends on how much of your data changes between backups, which is another number to read off a real repository rather than guess.
Encrypt it, automate it, and listen for silence
A backup holds everything an attacker would want from your server, on storage you control less. CISA lists encryption next to offline copies among the protections a backup needs,2 and the simplest place to do it is on the server, before anything leaves. restic encrypts everything it writes to a repository (AES-256) and attaches a message authentication code to each piece, so tampering with the stored data is detected.22 The catch is the key. restic's documentation is blunt that losing the repository password means the data is irrecoverably lost.23 Keep that password in a password manager and on paper somewhere safe, never only on the server the backup exists to replace.
For the copy the server must not be able to delete, restic's rest-server has an append-only mode: it accepts new snapshots and refuses to delete or change old ones, so someone who breaks into the server cannot erase the backups it has already made.26 Pruning old snapshots then runs from a different machine that holds full credentials. Object storage with object lock does the same job; CISA recommends either kind of delete protection.6 Keeping the server itself hard to get into is its own subject, covered in securing a Linux server.
Then take yourself out of the loop. A small script run by a systemd timer is enough. Note how the dump reaches restic: --stdin-from-command runs pg_dump itself and cancels the backup if it exits with an error. The tempting shortcut, piping a dump into restic backup --stdin, hides that error; restic's documentation shows a mysqldump that cannot connect producing an empty backup that restic reports as a success.27
cat /usr/local/bin/backup-db#!/bin/shset -eurestic backup --stdin-filename globals.sql --stdin-from-command -- pg_dumpall --globals-onlyrestic backup --stdin-filename shop.dump --stdin-from-command -- pg_dump -Fc -Z 0 shopcurl -fsS -m 30 "$PING_URL" > /dev/nullcat /etc/systemd/system/backup-db@.service[Unit]Description=Nightly database backup to %iOnFailure=backup-alert.service[Service]Type=oneshotUser=postgresEnvironmentFile=/etc/backup-db-%i.envExecStart=/usr/local/bin/backup-dbcat /etc/systemd/system/backup-db@.timer[Timer]OnCalendar=*-*-* 02:00:00Persistent=true[Install]WantedBy=timers.targetsudo systemctl daemon-reloadsudo systemctl enable --now backup-db@nearby.timer backup-db@offsite.timerThe @ makes both units templates. systemd puts whatever follows the @ in an enabled unit's name wherever %i appears,29 and each timer starts the service with its own name,28 so backup-db@nearby.timer fills copy two from /etc/backup-db-nearby.env and backup-db@offsite.timer fills copy three, the append-only one, from /etc/backup-db-offsite.env. Each run takes its own dump, so one repository being unreachable does not stop the other. Each environment file sets RESTIC_REPOSITORY and RESTIC_PASSWORD_FILE, the variables restic reads for unattended runs,23 plus the PING_URL that the last line calls.
Persistent=true makes the timer run a missed job as soon as the machine is back if it was off at 02:00.28 OnFailure= starts another unit, here one you write to send a message to your phone or chat, whenever the backup service fails.29 And the script covers the database and nothing else. Uploaded files, configuration, certificates, cron jobs and the backup script itself are part of getting back to a working server too, so list what a rebuild needs and add a restic backup of those paths.
That covers a job that fails loudly. GitLab's failed because the alert itself never arrived.1 The stronger signal is the absence of success, a pattern usually called a dead man's switch. Because of set -e, the curl on the last line runs only after both backups have succeeded, and the monitoring service it calls, running anywhere but this server, raises the alarm when a ping is late. A machine that can read the repository can do the same check directly: restic snapshots --json gives the time each backup started,25 and an alarm fires when the newest is more than a day and a bit old. A dead server, a broken timer and a lost alert all look the same to either check, which is the point.
The restore drill: proving the backup works
CISA's advice is to test the backup procedure so that you can restore data both fully and partially, and quickly.2 A drill is a rehearsal of the day you lose the server, done on a quiet afternoon when nothing depends on it, and it is the only step in this guide that turns a copy into a backup.
Checking the repository is not the same thing. By default restic check does not read the stored data back; --read-data reads all of it and --read-data-subset reads a share on each run.30 A clean check proves the repository is intact. It does not prove the dump inside loads, that the application starts, or how long any of it takes.
Pick a restore point, not always the newest
Usually last night's. Now and then take one from the weekly or monthly tier, so you learn whether old restore points still load with today's tools.
Start from nothing
Create a spare server with none of production's files, keys or logins. Give yourself only what you would have if the server were gone: the repository address and the password from wherever you keep it.
Fetch and restore, with a clock running
Pull the dump from the off-site copy, restore the globals, create the database and load it. Note how long each part takes.
Check the data, not only the exit code
Count the rows in the tables that matter, find the newest order and compare its time with your recovery point, then start the application against the restored database and log in.
Write down the two numbers
The time from starting to a working application is your measured recovery time; the age of the newest data is your measured recovery point. Compare both with what the business needs.
Fix what broke, then clean up
If the drill turned up a missing file, an outdated step or a password nobody could find, fix it and update the written procedure. Then delete the spare server, which now holds a copy of production data.
restic dump --path /globals.sql latest globals.sql > /tmp/globals.sqlrestic dump --path /shop.dump latest shop.dump > /tmp/shop.dumppsql -X -f /tmp/globals.sql postgrescreatedb -T template0 shoptime pg_restore --exit-on-error -d shop /tmp/shop.dumprestic dump writes one file from a snapshot to standard output. With two kinds of snapshot in one repository, name the path: restic's documentation warns that latest can otherwise pick a snapshot that does not contain the file at all.31 Stop the data restore at the first error. By default pg_restore carries on past errors and reports a count at the end,32 and a plain SQL dump loaded with psql keeps going unless ON_ERROR_STOP is set, leaving a partially restored database.13 The globals file is the one exception. It creates every role, including the superuser the new server already has, and the pg_dumpall manual says the "role already exists" error that follows is harmless and should be ignored.16 Read what psql prints there: that one line is expected, anything else is not. A restore that finishes with errors nobody read is the GitLab failure in a smaller form.
Then put the next drill in the calendar, monthly for anything that earns money, and after every change to the database version, the backup script or the storage. GitLab's dumps broke on exactly that kind of change, a version mismatch nobody noticed,1 and one drill after the upgrade would have found an empty bucket.
Questions
Is a snapshot a backup?
Not on its own. A snapshot on the same storage shares its unchanged blocks with the disk it was taken from, so it goes when that disk goes, and one copied elsewhere still sits in the same account with the same provider. It is a quick way to undo a bad upgrade; keep a real backup somewhere else as well.
What is the 3-2-1 backup rule?
Keep three copies of your data, on two different kinds of storage, with at least one copy off site. For a server that means the live data, a backup on storage other than the server's own disk, and a second backup in another region or with another provider.
Can I back up a database by copying its data directory?
Not while it is running. A file copy catches different files at different moments and can mix old and new data. Use pg_dump, mysqldump with --single-transaction or the database's own physical backup tool, or stop the database before you copy.
How often should I back up a server?
As often as the amount of work you can afford to lose. A nightly backup can lose up to a day of changes, hourly dumps cap it at an hour, and archiving a database's write-ahead log every minute or so brings it down to about a minute.
Is RAID a backup?
No. RAID spreads every write across several disks so the server keeps running when one of them fails. A deletion, a bad update or ransomware is written across all of them at the same moment.
How long should I keep backups?
Long enough to reach back past the problems you are slow to notice. CISA advises being able to roll back at least seven days; keeping daily copies for a week, weekly ones for a month and monthly ones for a year gives you about twenty restore points.
Sources
- Postmortem of database outage of January 31GitLab
- Back Up Business DataCISA
- Replication: recovery_min_apply_delay (PostgreSQL 18 documentation, 19.6)PostgreSQL Global Development Group
- Device-mapper snapshot supportThe Linux kernel documentation
- Backup and Recovery Types (MySQL 8.4 Reference Manual, 9.1)Oracle (MySQL)
- #StopRansomware GuideCISA
- File System Level Backup (PostgreSQL 18 documentation, 25.2)PostgreSQL Global Development Group
- How To Corrupt An SQLite Database FileSQLite
- VACUUMSQLite
- QEMU Guest Agent Protocol ReferenceThe QEMU Project
- QEMU Guest AgentThe QEMU Project
- Write-Ahead Logging (PostgreSQL 18 documentation, 28.3)PostgreSQL Global Development Group
- SQL Dump (PostgreSQL 18 documentation, 25.1)PostgreSQL Global Development Group
- Upgrading a PostgreSQL Cluster (PostgreSQL 18 documentation, 18.6)PostgreSQL Global Development Group
- pg_dump (PostgreSQL 18 documentation)PostgreSQL Global Development Group
- pg_dumpall (PostgreSQL 18 documentation)PostgreSQL Global Development Group
- Continuous Archiving and Point-in-Time Recovery (PostgreSQL 18 documentation, 25.3)PostgreSQL Global Development Group
- mysqldump, a database backup program (MySQL 8.4 Reference Manual)Oracle (MySQL)
- Recovery Point Objective (glossary, from NIST SP 800-34 Rev. 1)NIST Computer Security Resource Center
- Recovery Time Objective (glossary, from NIST SP 800-34 Rev. 1)NIST Computer Security Resource Center
- Removing backup snapshotsrestic documentation
- References: repository designrestic documentation
- Preparing a new repositoryrestic documentation
- Tuning parameters: compressionrestic documentation
- Scripting: JSON outputrestic documentation
- rest-serverThe restic project (GitHub)
- Backing uprestic documentation
- systemd.timer(5), Debian trixieDebian Manpages
- systemd.unit(5), Debian trixieDebian Manpages
- Working with repositoriesrestic documentation
- Restoring from backuprestic documentation
- pg_restore (PostgreSQL 18 documentation)PostgreSQL Global Development Group