Skip to main content

Database Sync

Rsync handles files brilliantly, but most web applications also depend on a database. Without a coordinated file + database backup strategy, your restores will be incomplete — files from one point in time, database from another.

info

For details on why you should never rsync live database files directly, see Live Replication Edge Cases.

The Problem​

What You BackupWhat You Get Back
Files onlySite looks broken — no content, no users, no settings
Database onlyDatabase without application code, themes, uploads
Files + Database togetherComplete, functional restore

Coordinated Backup Workflow​

MySQL / MariaDB​

#!/bin/bash
# coordinated-backup.sh — Files + Database together
set -euo pipefail

TIMESTAMP=$(date +%F_%H%M)
BACKUP_DIR="/backup/$TIMESTAMP"
mkdir -p "$BACKUP_DIR"

# 1. Export database (consistent snapshot)
mysqldump --defaults-file=/root/.my.cnf \
--single-transaction \
--routines --triggers --events \
--all-databases | gzip > "$BACKUP_DIR/databases.sql.gz"

# 2. Backup files (incremental)
rsync -av --link-dest=/backup/latest \
/var/www/html/ "$BACKUP_DIR/files/"

# 3. Update latest pointer
ln -sfn "$BACKUP_DIR" /backup/latest

echo " Files + DB backed up: $BACKUP_DIR"

PostgreSQL​

# Export all PostgreSQL databases
pg_dumpall -U postgres | gzip > "$BACKUP_DIR/postgres.sql.gz"

# Or export a specific database
pg_dump -U postgres -d myapp | gzip > "$BACKUP_DIR/myapp.sql.gz"

# Then rsync alongside files
rsync -av --link-dest=/backup/latest \
/var/www/html/ "$BACKUP_DIR/files/"

Credential Management​

warning

Never put database passwords in scripts or crontabs. Use config files:

MySQL​

/root/.my.cnf
[mysqldump]
user = backup_user
password = secure_password
chmod 600 /root/.my.cnf
# Now mysqldump works without -u/-p flags:
mysqldump --defaults-file=/root/.my.cnf --all-databases

PostgreSQL​

/root/.pgpass
localhost:5432:*:backup_user:secure_password
chmod 600 /root/.pgpass
# Now pg_dump works without password prompt:
pg_dump -U backup_user myapp

Migration: Moving to a New Server​

Full Site Migration​

# On old server: create backup
mysqldump --defaults-file=/root/.my.cnf \
--single-transaction --all-databases | \
gzip > /tmp/databases.sql.gz

# Transfer everything to new server
rsync -avzP /var/www/html/ user@newserver:/var/www/html/
rsync -avzP /tmp/databases.sql.gz user@newserver:/tmp/

# On new server: import
gunzip < /tmp/databases.sql.gz | mysql -u root -p
sudo chown -R www-data:www-data /var/www/html

Staging Refresh from Production​

# Pull latest production data to staging
rsync -avz user@production:/var/www/html/ /var/www/staging/

# Pull and import production database
ssh user@production "mysqldump --single-transaction app_db" | \
mysql staging_db

# Update URLs if needed (WordPress example)
wp search-replace 'production.com' 'staging.example.com' \
--path=/var/www/staging/ --skip-columns=guid

High-Frequency Database Backups​

For high-traffic sites where data changes rapidly:

# Hourly database export (cron)
0 * * * * mysqldump --defaults-file=/root/.my.cnf \
--single-transaction myapp_db | \
gzip > /backup/db/hourly_$(date +\%F_\%H).sql.gz

# Daily file backup (cron)
0 2 * * * rsync -av --link-dest=/backup/latest \
/var/www/html/ /backup/$(date +\%F)/ && \
ln -sfn /backup/$(date +\%F) /backup/latest

# Weekly cleanup (cron)
0 3 * * 0 find /backup/db/ -name "hourly_*" -mtime +7 -delete

Verifying Database Backups​

# Test that the dump is valid (check header)
zcat /backup/latest/databases.sql.gz | head -20

# Check file size (should be non-zero and reasonable)
ls -lh /backup/latest/databases.sql.gz

# Full restore test to a temporary database
gunzip < /backup/latest/databases.sql.gz | mysql test_restore_db
mysql test_restore_db -e "SHOW TABLES;" | wc -l
mysql -e "DROP DATABASE test_restore_db;"

Common Pitfalls​

PitfallConsequencePrevention
Backing up files without databaseIncomplete restoreAlways export DB alongside files
Password in crontab/scriptSecurity risk, visible in psUse .my.cnf or .pgpass
No --single-transactionTable locks block live siteAlways use for InnoDB/live servers
Database and files from different timesInconsistent restoreExport DB first, then rsync files
Not testing restoreCorrupt dump goes unnoticedPeriodically restore to test DB

Quick Reference​

# Export MySQL + rsync files
mysqldump --defaults-file=/root/.my.cnf --single-transaction \
--all-databases | gzip > /backup/db.sql.gz
rsync -av /var/www/html/ /backup/files/

# Export PostgreSQL + rsync files
pg_dumpall | gzip > /backup/postgres.sql.gz
rsync -av /var/www/html/ /backup/files/

# Restore MySQL
gunzip < /backup/db.sql.gz | mysql -u root -p

# Restore PostgreSQL
gunzip < /backup/postgres.sql.gz | psql -U postgres

# Verify dump
zcat /backup/db.sql.gz | head -20

What's Next​