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