تقریباً همه نسخه‌ی پشتیبان دارند؛ تعداد کسانی که مطمئن‌اند می‌شود از آن بازگردانی کرد خیلی کمتر است. نسخه‌ی پشتیبانی که هرگز امتحان نشده در بهترین حالت یک امید است. در بدترین حالت فایلی است که به اندازه‌ی کافی بزرگ به نظر می‌رسد، هر شب هم ساخته می‌شود، اما وسطش بریده شده و روزی که لازمش دارید تازه این را می‌فهمید.

در این نوشته یک سامانه‌ی پشتیبان‌گیری ساده اما قابل‌اعتماد برای یک سرور معمولی می‌سازیم: از چه چیزهایی نسخه بگیریم، چطور dump سالم بگیریم، چطور سالم‌بودنش را بسنجیم، کجا نگهش داریم و چطور بازگردانی را تمرین کنیم.

قاعده‌ی ۳-۲-۱

قدیمی‌ترین و هنوز کاربردی‌ترین قاعده‌ی این حوزه این است:

  • ۳ نسخه از داده: نسخه‌ی اصلی و دست‌کم دو نسخه‌ی پشتیبان.
  • روی ۲ نوع رسانه یا سامانه‌ی متفاوت: مثلاً دیسک خود سرور و یک فضای ذخیره‌سازی جدا.
  • ۱ نسخه بیرون از محل: در سرور، دیتاسنتر یا حتی سرویس‌دهنده‌ای دیگر.

منطق پشتش ساده است: هر نسخه در برابر یک نوع فاجعه محافظت می‌کند. نسخه‌ی روی همان سرور در برابر DROP TABLE اشتباهی کمک می‌کند، اما اگر دیسک خراب شود یا حساب سرویس‌دهنده بسته شود، با خود سرور از بین می‌رود. نسخه‌ی بیرونی برای همین روز است.

امروزه معمولاً یک قید دیگر هم اضافه می‌کنند: دست‌کم یک نسخه باید از سرور اصلی قابل پاک‌کردن نباشد. اگر کسی به سرور نفوذ کند و همان کلید SSH که برای ارسال پشتیبان استفاده می‌شود اجازه‌ی پاک‌کردن نسخه‌های بیرونی را هم بدهد، قاعده‌ی ۳-۲-۱ روی کاغذ رعایت شده اما در عمل نه.

از چه چیزهایی نسخه بگیریم

پایگاه‌داده: dump، نه کپی فایل

کپی‌کردن پوشه‌ی /var/lib/mysql یا /var/lib/postgresql در حالی که سرویس روشن است، معمولاً نسخه‌ای ناسازگار می‌دهد: بخشی از فایل‌ها پیش از یک تراکنش کپی شده‌اند و بخشی بعد از آن. راه درست، گرفتن dump منطقی با ابزار خود پایگاه‌داده است.

برای MySQL و MariaDB با جدول‌های InnoDB:

bash
mysqldump --single-transaction --quick --routines --triggers --events app | gzip > app.sql.gz

--single-transaction کل dump را داخل یک تراکنش با یک snapshot سازگار می‌گیرد، بدون اینکه جدول‌ها را قفل کند؛ یعنی سایت در حین پشتیبان‌گیری کار می‌کند. این فقط برای InnoDB صادق است؛ جدول‌های MyISAM با این گزینه سازگار نمی‌مانند. --quick ردیف‌ها را یکی‌یکی می‌خواند و جدول‌های بزرگ را یکجا در حافظه بار نمی‌کند.

برای PostgreSQL، pg_dump ذاتاً snapshot سازگار می‌گیرد. قالب custom (-Fc) فشرده است و بعداً با pg_restore اجازه‌ی بازگردانی انتخابی می‌دهد:

bash
sudo -u postgres pg_dump --format=custom app > app.dump

رمز عبور پایگاه‌داده را هرگز در خط فرمان ننویسید؛ در فهرست پردازه‌ها برای همه‌ی کاربران دیده می‌شود. برای MySQL یک فایل تنظیمات با دسترسی 600 بسازید و برای PostgreSQL از کاربر سیستمی postgres یا فایل ~/.pgpass استفاده کنید:

/root/.backup.my.cnf
[client]
user=backup
password=CHANGE_ME

کاربر backup فقط باید دسترسی خواندن داشته باشد:

sql
CREATE USER 'backup'@'localhost' IDENTIFIED BY 'CHANGE_ME';
GRANT SELECT, SHOW VIEW, TRIGGER, LOCK TABLES, EVENT, PROCESS ON *.* TO 'backup'@'localhost';

فایل‌ها

کنار پایگاه‌داده معمولاً این‌ها هم لازم‌اند:

  • فایل‌هایی که کاربران بارگذاری کرده‌اند (مثلاً storage/app در Laravel).
  • تنظیمات سرور: /etc/nginx، /etc/caddy، cron‌ها، unit‌های systemd.
  • فایل .env و کلیدهای رمزنگاری برنامه. بدون APP_KEY بعضی داده‌های رمزشده در پایگاه‌داده دیگر خواندنی نیستند. این فایل‌ها حاوی اسرارند؛ نسخه‌ی پشتیبانشان را رمزنگاری‌شده یا در جایی با دسترسی محدود نگه دارید.

کد برنامه معمولاً در git هست و لازم نیست جداگانه پشتیبان بگیرد، به شرطی که مطمئن باشید آنچه روی سرور است واقعاً با مخزن یکی است.

انواع پشتیبان در یک نگاه

نوع چه چیزی ذخیره می‌شود بازگردانی فضای لازم نکته
کامل (full) همه‌ی داده در هر اجرا ساده‌ترین: فقط یک فایل زیاد برای پایگاه‌داده‌های کوچک و متوسط بهترین شروع
افزایشی (incremental) فقط تغییرات از آخرین پشتیبان کندتر: آخرین نسخه‌ی کامل به‌علاوه‌ی همه‌ی افزایشی‌ها به ترتیب کم اگر یک حلقه‌ی زنجیر خراب باشد، نسخه‌های بعدی هم بی‌فایده‌اند
snapshot تصویر دیسک یا volume در یک لحظه سریع، اما معمولاً کل دیسک را برمی‌گرداند بستگی به سرویس‌دهنده برای پایگاه‌داده‌ی روشن فقط در حد crash-consistent است و معمولاً نزد همان سرویس‌دهنده می‌ماند

برای بیشتر پروژه‌های کوچک، dump کامل روزانه به‌علاوه‌ی snapshot هفتگی سرویس‌دهنده ترکیب معقولی است. snapshot را جایگزین dump نکنید: هم بیرون از محل حساب نمی‌شود، هم سازگاری پایگاه‌داده را تضمین نمی‌کند.

یک اسکریپت کوچک و قابل‌اعتماد

اسکریپت زیر هر بار یک dump با نام زمان‌دار می‌سازد، پیش از پذیرفتنش سالم‌بودنش را می‌سنجد، نسخه‌های قدیمی را پاک می‌کند و همه را به سرور دیگری می‌فرستد:

/usr/local/bin/backup-db.sh
#!/usr/bin/env bash
# Daily database backup: dump, verify, rotate, copy off-site.
set -euo pipefail
umask 077

DB_NAME="app"
BACKUP_DIR="/var/backups/db"
KEEP_DAYS=14
MIN_BYTES=10240
REMOTE="backup@backup.example.com:/srv/backups/app/"
SSH_KEY="/root/.ssh/backup_ed25519"

STAMP="$(date +%F_%H%M)"
OUT="$BACKUP_DIR/${DB_NAME}_${STAMP}.sql.gz"

mkdir -p "$BACKUP_DIR"
# A half-written file must never look like a finished backup.
trap 'rm -f "$OUT.part"' EXIT

# 1. Dump into a temporary name
mysqldump --defaults-extra-file=/root/.backup.my.cnf \
  --single-transaction --quick --routines --triggers --events \
  "$DB_NAME" | gzip -6 > "$OUT.part"

# 2. Verify before accepting it
gzip -t "$OUT.part"

size="$(stat -c %s "$OUT.part")"
if (( size < MIN_BYTES )); then
  echo "ERROR: dump is only $size bytes" >&2
  exit 1
fi

last_line="$(zcat "$OUT.part" | tail -n 1)"
if [[ "$last_line" != "-- Dump completed"* ]]; then
  echo "ERROR: dump does not end cleanly: $last_line" >&2
  exit 1
fi

mv "$OUT.part" "$OUT"

# 3. Rotate local copies
find "$BACKUP_DIR" -maxdepth 1 -type f -name "${DB_NAME}_*.sql.gz" \
  -mtime +"$KEEP_DAYS" -delete

# 4. Copy off-site
rsync -a -e "ssh -i $SSH_KEY -o BatchMode=yes" "$BACKUP_DIR/" "$REMOTE"

echo "$(date -Is) OK $OUT ($size bytes)"
bash
sudo install -m 700 backup-db.sh /usr/local/bin/backup-db.sh
sudo /usr/local/bin/backup-db.sh

چند جزء این اسکریپت ظاهراً کوچک‌اند اما همه‌ی ارزشش در همین‌هاست.

چرا pipefail حیاتی است

در mysqldump ... | gzip > file وضعیت خروج کل خط، به‌طور پیش‌فرض وضعیت آخرین دستور یعنی gzip است. اگر mysqldump وسط کار به خطا بخورد، gzip با خیال راحت همان نیمه را فشرده می‌کند و اسکریپت موفق تمام می‌شود. set -o pipefail باعث می‌شود شکست هر جزء خط لوله، کل خط را شکست‌خورده حساب کند.

سه آزمون برای اینکه dump بریده نباشد

  • gzip -t: یکپارچگی فایل فشرده را بررسی می‌کند. فایلی که نوشتنش نیمه‌کاره مانده، معمولاً همین‌جا رد می‌شود.
  • اندازه: اگر فایل dump ناگهان فقط چند کیلوبایت باشد، یعنی چیزی درست کار نکرده؛ مثلاً اتصال به پایگاه‌داده برقرار نشده یا پایگاه‌داده‌ی اشتباهی خالی بوده. MIN_BYTES را متناسب با داده‌ی خودتان تنظیم کنید.
  • خط آخر: mysqldump وقتی کارش را کامل تمام کند، در پایان فایل خطی با -- Dump completed on می‌نویسد. اگر این خط نباشد، dump پیش از پایان قطع شده است. (این خط فقط وقتی هست که --skip-comments استفاده نکرده باشید.)

نوشتن در .part و mv در پایان هم تضمین می‌کند فایلی با نام نهایی همیشه فایلی است که از هر سه آزمون گذشته است.

نسخه‌ی PostgreSQL

برای PostgreSQL فقط بخش dump و آزمون عوض می‌شود. قالب custom خودش فشرده است، پس gzip -t و خط آخر معنا ندارد؛ به جایش کل فایل را با pg_restore می‌خوانیم و خروجی را دور می‌ریزیم. اگر فایل بریده یا خراب باشد، pg_restore با خطا تمام می‌شود:

bash
OUT="$BACKUP_DIR/${DB_NAME}_${STAMP}.dump"
runuser -u postgres -- pg_dump --format=custom "$DB_NAME" > "$OUT.part"
pg_restore --file=/dev/null "$OUT.part"

pg_restore --list هم هست، اما فقط فهرست محتوا را از ابتدای فایل می‌خواند و برای کشف بریدگی کافی نیست.

چرخش با find -mtime

-mtime +14 یعنی فایل‌هایی که بیش از ۱۴ روز کامل از آخرین تغییرشان گذشته است. دو احتیاط: -maxdepth 1 و الگوی دقیق -name جلوی پاک‌شدن چیزی خارج از قصد را می‌گیرند، و پیش از اضافه‌کردن -delete، یک بار همان دستور را بدونش اجرا کنید تا فهرست فایل‌ها را ببینید.

نسخه‌ی بیرون از سرور با rsync

در دستور rsync عمداً --delete نگذاشته‌ایم. اگر روزی پوشه‌ی محلی خالی یا خراب شود، --delete همان خرابی را با دقت به نسخه‌ی بیرونی هم منتقل می‌کند. چرخش نسخه‌های بیرونی را با یک find جداگانه روی خود سرور پشتیبان انجام دهید و آنجا مدت نگهداری را طولانی‌تر بگیرید.

بهتر از آن، کشیدن به جای فرستادن است: سرور پشتیبان خودش با rsync فایل‌ها را از سرور اصلی بخواند. این‌طوری سرور اصلی هیچ کلیدی برای نوشتن یا پاک‌کردن روی سرور پشتیبان ندارد و نفوذ به آن، نسخه‌های بیرونی را تهدید نمی‌کند.

زمان‌بندی با cron

یک فایل در /etc/cron.d بسازید. در این پوشه، برخلاف crontab کاربر، ستون ششم نام کاربر اجراکننده است:

/etc/cron.d/backup-db
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
30 3 * * * root /usr/local/bin/backup-db.sh >> /var/log/backup-db.log 2>&1

دو جزئیات که cron را بی‌صدا از کار می‌اندازد: نام فایل در /etc/cron.d نباید نقطه داشته باشد، و فایل باید با یک خط خالی تمام شود. ساعت اجرا را هم زمانی بگذارید که بار سرور کم است.

اسکریپتی که شکست می‌خورد و کسی نمی‌فهمد، از نبود اسکریپت بهتر نیست. دست‌کم هفته‌ای یک بار tail /var/log/backup-db.log را نگاه کنید، یا اگر ارسال ایمیل روی سرور تنظیم شده، با MAILTO در همین فایل خطاها را به خودتان برسانید.

مهم‌ترین بخش: آزمودن بازگردانی

همه‌ی آنچه تا اینجا گفتیم فقط احتمال سالم‌بودن نسخه را بالا می‌برد. تنها راه مطمئن‌شدن، بازگردانی واقعی است. بهترین جا برای این کار یک سرور یا ماشین مجازی جداست و بهترین فایل، همان نسخه‌ی بیرونی؛ این‌طوری مسیر rsync را هم آزموده‌اید.

برای MySQL روی ماشین آزمون:

bash
sudo mysql -e 'CREATE DATABASE restore_test'
zcat app_2026-10-06_0330.sql.gz | sudo mysql restore_test
sudo mysql -e 'SELECT COUNT(*) FROM restore_test.users'
sudo mysql -e 'DROP DATABASE restore_test'

یک هشدار: اگر dump را با --databases یا --all-databases گرفته باشید، داخل فایل دستورهای CREATE DATABASE و USE با نام اصلی هست و بازگردانی روی همان پایگاه‌داده‌ی اصلی انجام می‌شود، نه restore_test. به همین دلیل اسکریپت بالا نام پایگاه‌داده را بدون --databases می‌دهد. هرگز آزمون بازگردانی را روی سرور production انجام ندهید.

برای PostgreSQL:

bash
sudo -u postgres createdb restore_test
sudo -u postgres pg_restore --no-owner --dbname=restore_test app_2026-10-06_0330.dump
sudo -u postgres psql -d restore_test -c 'SELECT COUNT(*) FROM users'
sudo -u postgres dropdb restore_test

در هر آزمون این چند سؤال را جواب دهید و جایی یادداشت کنید:

  • آیا بازگردانی بدون خطا تمام شد؟
  • تعداد ردیف‌های چند جدول مهم با انتظار شما می‌خواند؟ تازه‌ترین رکورد مال چه زمانی است؟
  • کل کار چقدر طول کشید؟ این عدد همان زمانی است که در روز حادثه سایت پایین خواهد بود.
  • آیا برنامه با پایگاه‌داده‌ی بازگردانده‌شده بالا می‌آید؟ فایل‌های بارگذاری‌شده و .env هم سر جایشان‌اند؟

این آزمون را ماهی یک بار در تقویم بگذارید و بعد از هر تغییر جدی در ساختار پایگاه‌داده یا اسکریپت پشتیبان هم تکرارش کنید.

جمع‌بندی

پشتیبان خوب سه ویژگی دارد: سازگار گرفته می‌شود (--single-transaction و pg_dump)، پیش از پذیرفته‌شدن سنجیده می‌شود (pipefail، gzip -t، اندازه و خط آخر) و دست‌کم یک نسخه‌اش جایی است که نه خرابی سرور به آن می‌رسد و نه نفوذگر. اما هیچ‌کدام از این‌ها جای آزمون بازگردانی را نمی‌گیرد. تا وقتی یک بار نسخه را روی ماشینی دیگر بالا نیاورده‌اید، فقط فرض کرده‌اید که پشتیبان دارید.