db-backup.sh - Setup guide (2026 edition)
This guide is written for beginners. Follow the steps in order. Everything is typed in a terminal (SSH) on your server, as your normal user. You do NOT need root.
What you get at the end: every night, cron makes a full, compressed and encrypted backup of your database, checks that it can really be decrypted, and e-mails it to you. If anything goes wrong, you get an alert e-mail.
One backup file = one complete database. No incremental backups, nothing to chain together: to restore, you only need the most recent good file.
Step 1 - Put the files in place
Create a folder OUTSIDE your web folder (never inside
public_html or
www), and put
db-backup.sh and
backup.conf.example in it:
Code:
mkdir -p ~/db-backup
cd ~/db-backup
cp backup.conf.example backup.conf
chmod 700 ~/db-backup
chmod 700 db-backup.sh
chmod 600 backup.conf
Step 2 - Store your encryption passphrase in a file
The backup is encrypted with a passphrase. The script reads it from a file, so it never appears in the script, on the command line or in
ps.
Type these two lines, then type your passphrase, press Enter, then press Ctrl+D:
Code:
umask 077
cat > ~/.backup_key
Then check that only you can read it:
Code:
chmod 600 ~/.backup_key
ls -l ~/.backup_key
The line must start with
-rw-------. (Using
cat like this keeps the passphrase out of your shell history;
umask 077 makes the file private from the very first second.)
Rules for the passphrase:
- Make it long: 20 characters or more, or 5-6 random words.
- Only the FIRST line of the file is used.
- Keep a copy somewhere else (password manager, paper in a drawer). If your server dies, this file dies with it, and without the passphrase your e-mailed backups are useless. This is the most important rule of this whole guide.
Step 3 - Store your database credentials
The script never contains your database password. Pick ONE of the two methods below.
Method A (recommended): an option file - works with MySQL AND MariaDB
Create one small file per database. Replace the three values:
Code:
cat > ~/.my-backup-your_database_name.cnf <<'EOF'
[client]
host="your_database_server.example.com"
user="your_database_user"
password="your_database_password"
EOF
chmod 600 ~/.my-backup-your_database_name.cnf
Keep the double quotes: they protect special characters such as
#.
Do NOT put a
database= line in the
[client] section. The database name comes from
DB_NAME in
backup.conf, and
mysqldump has no
database option: MySQL 8 and newer stop with
unknown variable 'database=...'. The script checks for this and tells you.
You already have .my.cnf files with database= for other scripts? You can reuse them. Move the
database= line under a
[mysql] section, which only the
mysql client reads, so your other scripts keep working:
Code:
[client]
host="your_database_server.example.com"
user="your_database_user"
password="your_database_password"
[mysql]
database="your_database_name"
Then, in
backup.conf, check that
DB_CNF_FILE points to this file. The script reads ONLY this file (
--defaults-file), so a
~/.my.cnf made for another site can never replace these credentials.
Test it (prints the first lines of a dump of the table structures):
Code:
mysqldump --defaults-file=$HOME/.my-backup-your_database_name.cnf \
--no-data --no-tablespaces your_database_name | head -n 20
On MariaDB 11 and newer, replace
mysqldump with
mariadb-dump.
Several databases? One credentials file per database user:
.my-backup-shop.cnf,
.my-backup-blog.cnf, and so on. If one user can read several databases (common: one user per site, several databases per site), one file is enough: point several config files to it, each with its own
DB_NAME.
Method B: mysql_config_editor - Oracle MySQL only
mysql_config_editor stores all your logins in ONE file,
~/.mylogin.cnf. Two things to know before choosing it:
- It exists only with Oracle MySQL. MariaDB does not have it.
- The file is obfuscated, not truly encrypted. The MySQL manual itself says a determined attacker with access to your account can read it. Its real benefit is the same as Method A: the password is not in your scripts. The file must be readable by you only, or MySQL ignores it.
Create one "login path" per database (you are asked for the password):
Code:
mysql_config_editor set --login-path=shop --host=your_database_server.example.com --user=shop_user --password
mysql_config_editor set --login-path=blog --host=your_database_server.example.com --user=blog_user --password
List them (passwords are hidden):
Code:
mysql_config_editor print --all
Change one (run
set again with the same name) or delete one:
Code:
mysql_config_editor remove --login-path=blog
Then, in each config file, set
DB_CNF_FILE='' and
DB_LOGIN_PATH='shop'.
Step 4 - Edit backup.conf
All settings are grouped at the top of two files. Nothing to hunt for in the code.
backup.conf (one per database). Open it with your editor (
ee backup.conf on FreeBSD, or
nano backup.conf). The settings are in the box at the top; explanations are in the HELP section at the bottom.
- Required:
DB_NAME, DB_CNF_FILE (the file from Step 3) and EMAIL_TO.
- Everything else can stay empty (
''): the default value is used.
- Recommended:
ALERT_EMAIL. If your backups go to a folder you never read (spam filter, archive), put an address you DO read.
DB_CHARSET: keep utf8mb4 for a modern database. If you used another value for years (latin1 was common with old PHP sites), keep your old value. Changing it can break accented characters.
db-backup.sh (shared by all your databases). The first lines hold a "DEFAULT SETTINGS" box: the values used whenever a
backup.conf leaves a setting empty. Change a value there to change it for all your databases at once (for example the alert address, or where the passphrase file is). Do not change anything under that box.
Step 5 - First run, by hand
Code:
~/db-backup/db-backup.sh ~/db-backup/backup.conf
echo $?
0 means success. Then check:
Code:
cat ~/backups/db/backup.log
You should see
backup verified,
e-mail sent and
done, and receive an e-mail with the encrypted file attached.
Then do Step 8 once with that e-mail, to be sure you can restore.
Step 6 - Run it every night with cron
Add these lines (example: every night at 3:17):
Code:
MAILTO=your_email@example.com
17 3 * * * /bin/sh $HOME/db-backup/db-backup.sh $HOME/db-backup/backup.conf
MAILTO makes cron e-mail you anything the script prints on errors, as a second safety net.
Online shop? The database changes with every order, so back it up more often. Every hour, at minute 10:
Code:
10 * * * * /bin/sh $HOME/db-backup/db-backup.sh $HOME/db-backup/backup.conf
Each backup e-mail is a full copy, so count: one backup per hour kept 30 days in your mailbox is 720 e-mails per database. Check your mailbox quota.
Several databases? One config file and one cron line per database:
Code:
17 3 * * * /bin/sh $HOME/db-backup/db-backup.sh $HOME/db-backup/shop.conf
27 3 * * * /bin/sh $HOME/db-backup/db-backup.sh $HOME/db-backup/blog.conf
Step 7 - Keeping a month of backups
The server only keeps
KEEP_DAYS days of copies. Your mailbox is the real archive: for example, a mail filter that moves these e-mails into a folder and deletes them after 30 days gives you 30 days of history, without filling the server.
Big backups are cut into pieces, like big archives on floppy disks. A backup bigger than
MAX_ATTACH_MB is sent as several e-mails, one piece each: "part 1 of 3", "part 2 of 3"... Each e-mail explains how to glue the pieces back together (Step 8.0).
An attachment makes the e-mail about 37% bigger than the file, so keep
MAX_ATTACH_MB well under your provider's limit: 15 for Outlook/Hotmail (25 MB per e-mail), 35 for GMX (50 MB attachments).
MAX_PARTS (default 10) limits the number of e-mails per backup. Beyond that, the backup is kept on the server and you get an alert.
Remember: a backup in pieces needs ALL its pieces. If one e-mail is lost, that backup cannot be restored, so use the one before it.
Step 8 - Restore a backup
8.0 Only if the backup came in several e-mails
- Save all the attachments with the same date and time (
...part-aa, ...part-ab, ...) in the same folder.
- Glue them back together. The exact command is in each e-mail; copy only the ONE line for your system: - Linux, FreeBSD or macOS:
cat mydb_2026-11-03_03h17.sql.gz.enc.part-* > mydb_2026-11-03_03h17.sql.gz.enc - Windows (cmd.exe, not PowerShell): copy /b mydb_...part-aa + mydb_...part-ab + mydb_...part-ac mydb_2026-11-03_03h17.sql.gz.enc
- Optional check:
openssl dgst -sha256 mydb_2026-11-03_03h17.sql.gz.enc must print the code written in the e-mails.
Then continue with 8.1 as for a normal backup.
8.1 Decrypt
The exact command is printed in every backup e-mail. It looks like this:
Code:
openssl enc -d -aes-256-cbc -pbkdf2 -iter 600000 -md sha256 -in mydb_2026-11-03_03h17.sql.gz.enc -out mydb_2026-11-03_03h17.sql.gz
OpenSSL asks for your passphrase.
On Windows:
- Install OpenSSL. Two easy choices: "Win64 OpenSSL Light" from slproweb.com, or Git for Windows (it includes
openssl.exe).
- Save the attachment, for example in your Downloads folder.
- Open a terminal (Start menu, type
cmd) and go to that folder: cd %USERPROFILE%\Downloads
- Paste the command from the e-mail. If Windows says
openssl is not found, type its full path instead, for example: "C:\Program Files\OpenSSL-Win64\bin\openssl.exe" enc -d ...
- Type your passphrase.
8.2 Decompress
- Windows: open the
.sql.gz file with 7-Zip and extract the .sql file.
- Unix:
gunzip mydb_2026-11-03_03h17.sql.gz
8.3 Put the data back
Warning: this replaces the current content of the database.
Code:
mysql --defaults-file=$HOME/.my-backup-your_database_name.cnf your_database_name < mydb_2026-11-03_03h17.sql
(
mariadb instead of
mysql on MariaDB 11+.) You can also import the
.sql file with phpMyAdmin or Adminer.
Note: dumps made by recent
mariadb-dump start with a special "sandbox" line. It imports fine into MariaDB, but an Oracle MySQL client refuses it. To move data from MariaDB to MySQL, delete that first line.
Coming from the 2011 or 2018 version of this script?
- Backups made by the old script (still in your mailbox) decrypt with:
openssl enc -d -aes-256-cbc -pbkdf2 -in OLD_FILE.sql.gz.ENC -out OLD_FILE.sql.gz (no -iter: the old script used OpenSSL's default of 10000).
- Remove the database password and the encryption passphrase from the old script once the new one works. They should exist only in the files of Steps 2 and 3.
- The
sent / sent.lock lines are gone: the new script tells mutt not to keep a copy of sent mail (set copy=no), which was the cause of that growing file.
mailmessage.txt is no longer needed: the e-mail text is generated, with the exact decrypt command and the real file name.
What changed in 2026, and why
| Problem in older versions | 2026 fix |
|---|
Passphrase on the command line (-k / -pass pass:), visible to other users with ps | Read from a file only you can read (-pass file:) |
Unencrypted .sql.gz left on the server | Dump, compression and encryption in one stream; only the encrypted file touches the disk |
| A failed or half-finished dump was still e-mailed as if all was fine | Exit code checked, and the encrypted file is decrypted again to confirm the dump ends with -- Dump completed |
| Silent failures | Alert e-mail plus a log line, and cron's MAILTO as a backup |
| 10000 PBKDF2 iterations (OpenSSL default) | 600000 iterations, the current OWASP figure for PBKDF2-HMAC-SHA256 |
: in file names, which Windows refuses | 2026-11-03_03h17 |
#!/bin/bash (the 2024 draft): bash is not at that path on FreeBSD | Plain /bin/sh, tested with dash and bash |
mysqldump only, and mysql_config_editor only | Works with MySQL and MariaDB (mariadb-dump auto-detected); credentials in an option file that both understand |
PROCESS privilege error with MySQL 8.0.21+ on shared hosting | --no-tablespaces |
| Two runs at the same time could mix files | Lock folder per database |
Good habits
- Once every few months, restore one backup into a test database. A backup you have never restored is a hope, not a backup.
- Keep your database server up to date. Oracle MySQL 8.0 stopped receiving security fixes at the end of April 2026; the supported long-term version is 8.4. If you are on a managed or shared server, ask your host which version you run.
- If you change the database password, update the file from Step 3.