# بک‌آپ خودکار دیتابیس روی سی‌پنل: mysqldump، چرخش نسخه‌ها و تست بازیابی

- منبع: https://di9.ir/blog/%d8%a8%da%a9-%d8%a7%d9%be-%d8%ae%d9%88%d8%af%da%a9%d8%a7%d8%b1-%d8%af%db%8c%d8%aa%d8%a7%d8%a8%db%8c%d8%b3-%d8%b1%d9%88%db%8c-%d8%b3%db%8c-%d9%be%d9%86%d9%84
- سایت: دیباگ
- نویسنده: danidiba
- تاریخ انتشار: 2026-09-08
- آخرین به‌روزرسانی: 2026-09-08

یک شب پنجشنبه، وسط تمیز کردن داده‌های تکراری، یک `DELETE` را روی سرور اصلی اجرا کردیم که شرطش ناقص نوشته شده بود. بخش بزرگی از جدول سفارش‌ها رفت. اولین کاری که کردیم باز کردن بخش بک‌آپ سی‌پنل بود. تنها چیزی که آنجا بود، بک‌آپ خودکار خود هاستینگ از دو روز قبل بود؛ روی همان دیسک، و برای برگرداندنش باید تیکت می‌زدیم و منتظر می‌ماندیم.

آن شب فهمیدیم چیزی که «بک‌آپ» صدایش می‌کردیم یک وعده بود، نه یک برنامه. از فردایش یک اسکریپت بک‌آپ خودکار دیتابیس روی همان هاست سی‌پنل نوشتیم که خودمان کنترلش می‌کنیم: هر شب یک دامپ فشرده با نام تاریخ‌دار، چرخش نسخه‌ها، لاگ، و از همه مهم‌تر یک تست بازیابی که هر ماه واقعاً اجرا می‌شود.

این مقاله همان اسکریپت است، تمیزشده. فرض‌ها: هاست اشتراکی با سی‌پنل، دسترسی SSH یا دست‌کم بخش Cron Jobs، بدون دسترسی root، دیتابیس MySQL یا MariaDB، و یک اپلیکیشن لاراول که فایل `.env` دارد. قطعه‌ها همین شش‌تاست: خواندن اطلاعات اتصال، گرفتن دامپ، فشرده‌سازی در همان لوله، ذخیره با نام تاریخ‌دار، حذف نسخه‌های کهنه، و ثبت نتیجه در لاگ.

![زنجیره‌ی اجرای اسکریپت بک‌آپ از خواندن env تا ثبت لاگ](/blog/wp-content/uploads/2026/09/fig-fig-svg1-a3ngv-ukeveb.svg)

## چرا بک‌آپ خودکار خودِ سی‌پنل کافی نیست#

بک‌آپ هاستینگ بد نیست. فقط برای نقشی که ما به آن داده بودیم ساخته نشده. سه مشکل دارد.

**چرخه‌اش کوتاه است و شما تنظیمش نمی‌کنید.** بیشتر هاست‌های اشتراکی یک نسخه‌ی روزانه و شاید یک نسخه‌ی هفتگی نگه می‌دارند. اینکه دقیقاً چند نسخه و چند روز، به سیاست همان شرکت بستگی دارد و می‌تواند بدون اطلاع شما عوض شود. اگر خرابی داده را دیر بفهمید — که حالت رایج است — ممکن است هر نسخه‌ای که مانده، خودش هم خراب باشد.

**روی همان دیسک است.** بک‌آپ‌های محلی سی‌پنل معمولاً در همان اکانت یا همان سرور نگه داشته می‌شوند. این نسخه شما را از پاک‌کردن اشتباهی نجات می‌دهد، ولی از خرابی دیسک، تعلیق اکانت، یا از دست رفتن دسترسی به اکانت نجات نمی‌دهد. بک‌آپی که با اصلْ یک سرنوشت دارد، بک‌آپ نیست.

**بازیابی‌اش کند و همه‌یاهیچ است.** خیلی وقت‌ها فقط یک جدول را می‌خواهید، نه کل اکانت را. با یک فایل `.sql.gz` که خودتان ساخته‌اید، می‌توانید همان جدول را در یک دیتابیس موقت بالا بیاورید و فقط داده‌ی لازم را بردارید. با ریستور کامل اکانت، این کار یعنی برگرداندن همه‌چیز به دو روز قبل.

نتیجه‌ی عملی: بک‌آپ هاستینگ را نگه دارید، ولی به آن تکیه نکنید. بک‌آپ خودتان لایه‌ی اول است.

## خواندن اطلاعات اتصال از فایل env#

اسم دیتابیس را در اسکریپت هاردکد نکنید. روی سی‌پنل نام دیتابیس با پیشوند نام کاربری ساخته می‌شود و موقع مهاجرت یا ساخت محیط staging عوض می‌شود. منبع درست همان `.env` است.

یک تابع کوچک برای خواندن هر کلید:

Bashکپی`ENV_FILE=&quot;$HOME/app/.env&quot;

env_get() {
  sed -n &quot;s/^$1=//p&quot; &quot;$ENV_FILE&quot; | head -n1 | tr -d &#039;\r&#039; | sed -e &#039;s/^&quot;//&#039; -e &#039;s/&quot;$//&#039;
}

DB_NAME=&quot;$(env_get DB_DATABASE)&quot;`

سه نکته در همین چهار خط هست.

`head -n1` می‌گذاریم چون فایل `.env` ممکن است یک کلید را دوبار داشته باشد (مثلاً یک نسخه‌ی کامنت‌شده که کامنتش پاک شده). کلید تکراری در `.env` خودش یک ابهام است و بهتر است اصلاً نگذاریدش؛ ما در این اسکریپت صریحاً اولین مقدار را برمی‌داریم تا رفتار قابل پیش‌بینی باشد.

`sed -e &#039;s/^&quot;//&#039; -e &#039;s/&quot;$//&#039;` گیومه‌ی احتمالی دور مقدار را برمی‌دارد. اگر رمز شما کاراکتر `#` یا فاصله دارد، احتمالاً در `.env` داخل گیومه است و آن گیومه نباید وارد رشته‌ی اتصال شود.

و مهم‌ترینش: `tr -d &#039;\r&#039;`. اگر فایل `.env` یک بار با ویرایشگر ویندوزی ذخیره شده باشد، انتهای هر خط `\r\n` است. مقداری که می‌خوانید یک کاراکتر اضافه‌ی نامرئی دارد و اسکریپت با خطایی شکست می‌خورد که هیچ ربطی به علت ندارد — مثلاً «Unknown database» با نامی که دقیقاً درست به‌نظر می‌رسد. این دقیقاً همان تله‌ای است که قبلاً سرش وقت گذاشتیم و در [پایان‌خط ویندوزی در فایل .env](/blog/crlf-در-فایل-env) جدا نوشتیمش. اگر یک بار پیش بیاید، همیشه یادتان می‌ماند.

## رمز دیتابیس را روی خط فرمان ننویسید#

وسوسه‌ی اول این است:

Bashکپی`mysqldump -u &quot;$DB_USER&quot; -p&quot;$DB_PASS&quot; &quot;$DB_NAME&quot; &gt; dump.sql`

این کار می‌کند و همین باعث می‌شود دیر متوجه اشکالش شوید. دو مشکل دارد.

اول، `mysqldump` خودش هشدار می‌دهد که دادن رمز روی خط فرمان ناامن است، و راست می‌گوید: روی سرور اشتراکی، فهرست پروسه‌ها لزوماً برای شما تنها نیست. رمز در آرگومان‌های پروسه دیده می‌شود.

دوم، رمز داخل فایل اسکریپت می‌ماند و آن فایل ممکن است در گیت برود، در بک‌آپ فایل‌ها کپی شود، یا با دسترسی اشتباه ساخته شود.

راه درست، فایل تنظیمات کلاینت است. یک بار بسازید:

INIکپی`&#91;client]
user = &quot;cpuser_shop&quot;
password = &quot;Th3-r34l-p@ss&quot;
default-character-set = utf8mb4`

آن را در `~/.my.cnf` ذخیره کنید و دسترسی‌اش را ببندید:

Bashکپی`chmod 600 ~/.my.cnf`

اگر فایل تنظیمات برای گروه یا دیگران قابل نوشتن باشد، کلاینت MySQL آن را نادیده می‌گیرد؛ ضمن اینکه رمز نباید برای هیچ کاربر دیگری روی سرور خواندنی باشد. پس این `chmod` تشریفاتی نیست.

بعد در اسکریپت:

Bashکپی`mysqldump --defaults-extra-file=&quot;$HOME/.my.cnf&quot; &quot;$DB_NAME&quot;`

یک تله‌ی ریز اینجا هست که وقت‌گیر است: `--defaults-extra-file` باید **اولین** آرگومان بعد از نام دستور باشد. اگر بعد از سوییچ‌های دیگر بیاید، کلاینت با خطای «unknown variable» بالا نمی‌آید. همین یک قاعده را رعایت کنید و رد شوید.

اگر رمز را جای دیگری هم لازم دارید، همان `env_get` را برای `DB_USERNAME` و `DB_PASSWORD` صدا بزنید و فایل `.my.cnf` را با اسکریپت بسازید؛ ولی ساده‌ترین حالت این است که یک بار دستی بسازیدش و بعد از هر تغییر رمز به‌روزش کنید.

## سوییچ‌های mysqldump که واقعاً فرق می‌کنند#

دستور نهایی ما این است:

Bashکپی`mysqldump --defaults-extra-file=&quot;$HOME/.my.cnf&quot; \
  --single-transaction \
  --quick \
  --no-tablespaces \
  --routines \
  --default-character-set=utf8mb4 \
  &quot;$DB_NAME&quot;`

هر کدام دلیل دارد.

`--single-transaction` دامپ را داخل یک تراکنش با ایزولیشن repeatable read می‌گیرد. یعنی همه‌ی جدول‌های InnoDB از یک لحظه‌ی واحد خوانده می‌شوند، بدون قفل‌کردن سایت. این همان چیزی است که جای `--lock-tables` را می‌گیرد.

و همین‌جا جواب سؤال رایج: روی InnoDB به `--lock-tables` نیاز ندارید. `--lock-tables` جدول‌ها را برای خواندن قفل می‌کند و در عمل نوشتن روی سایت را تا پایان دامپ می‌خواباند. برای موتور MyISAM چاره‌ای نیست، ولی اگر جدول‌هایتان InnoDB است — که در لاراول مدرن پیش‌فرض است — این سوییچ فقط ضرر دارد. ضمناً `--single-transaction` و `--lock-tables` با هم جمع نمی‌شوند؛ دومی اولی را خنثی می‌کند. اگر مطمئن نیستید موتور جدول‌ها چیست، یک بار چک کنید:

SQLکپی`SELECT TABLE_NAME, ENGINE FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE();`

`--quick` می‌گوید ردیف‌ها را ردیف‌به‌ردیف بخوان و کل جدول را در حافظه نگه ندار. روی هاست اشتراکی که سقف حافظه دارید، این تفاوت بین «کار کرد» و «پروسه کشته شد» است. این سوییچ بخشی از `--opt` است و به‌صورت پیش‌فرض روشن است؛ صریح نوشتنش فقط برای این است که اگر جایی `--skip-opt` وارد دستور شد، از قلم نیفتد.

`--no-tablespaces` روی نسخه‌های تازه‌تر لازم می‌شود. `mysqldump` جدید برای نوشتن دستور `CREATE TABLESPACE` به دسترسی `PROCESS` نیاز دارد و کاربر شما روی هاست اشتراکی این دسترسی را ندارد. علامتش یک خطای «Access denied; you need (at least one of) the PROCESS privilege(s)» است که وسط دامپ می‌آید و فایل ناقص می‌ماند. این سوییچ همان بخش را کلاً حذف می‌کند.

`--default-character-set=utf8mb4` تضمین می‌کند اتصالِ دامپ روی utf8mb4 است. اگر این را ننویسید و کلاینت روی latin1 یا utf8 قدیمی بیاید، متن فارسی می‌تواند در فایل خروجی خراب ذخیره شود و شما تازه موقع ریستور بفهمید. این همان خانواده‌ی مشکلاتی است که در بحث کاراکترست و collation فارسی سراغش را دارید.

`--routines` رویه‌ها و توابع ذخیره‌شده را هم می‌آورد. تریگرها به‌صورت پیش‌فرض می‌آیند. `--events` را عمداً ننوشتیم: روی خیلی از هاست‌های اشتراکی دسترسی `EVENT` را نداده‌اند و دامپ با خطا می‌ایستد. اگر ایونت دارید، اضافه‌اش کنید و تست کنید؛ اگر خطا داد، بردارید.

## فشرده‌سازی با gzip و نام‌گذاری با تاریخ#

دامپ SQL متن است و متن خوب فشرده می‌شود. فایل واسط ننویسید؛ همان‌جا از لوله رد کنید:

Bashکپی`STAMP=&quot;$(date +%F-%H%M)&quot;
OUT=&quot;$DAILY/${DB_NAME}-${STAMP}.sql.gz&quot;

mysqldump --defaults-extra-file=&quot;$HOME/.my.cnf&quot; ... &quot;$DB_NAME&quot; | gzip -9 &gt; &quot;$OUT&quot;`

`date +%F-%H%M` چیزی مثل `2026-09-07-0305` می‌دهد. این قالب دو خاصیت دارد: در `ls` مرتب‌شده به ترتیب زمانی می‌آید، و بدون فکر کردن می‌فهمید کدام فایل برای کِی است.

یک تله‌ی جدی در لوله هست: کد خروجی یک لوله در بش، کد خروجی **آخرین** دستور است. اگر `mysqldump` وسط کار بترکد ولی `gzip` سالم تمام شود، اسکریپت فکر می‌کند همه‌چیز خوب بوده و یک فایل ناقص می‌ماند که ظاهرش سالم است. علاجش این خط در ابتدای اسکریپت است:

Bashکپی`set -euo pipefail`

`pipefail` کاری می‌کند که شکست هر حلقه‌ی لوله، کل لوله را شکست‌خورده حساب کند. بعد از نوشتن فایل هم سلامت آرشیو را بررسی کنید:

Bashکپی`gzip -t &quot;$OUT&quot;`

و اندازه‌ی فایل را نگاه کنید. یک دامپ سالم از یک دیتابیس واقعی هیچ‌وقت چند صد بایت نیست. اگر ناگهان فایل خیلی کوچک شد، یعنی چیزی شکسته — معمولاً دسترسی یا نام دیتابیس.

## اسکریپت کامل بک‌آپ دیتابیس#

این همان چیزی است که روی سرور می‌گذاریم، مثلاً در `~/scripts/db-backup.sh`:

Bashکپی`#!/bin/bash
set -euo pipefail

APP_DIR=&quot;$HOME/app&quot;
ENV_FILE=&quot;$APP_DIR/.env&quot;
BASE=&quot;$HOME/backups&quot;
DAILY=&quot;$BASE/daily&quot;
WEEKLY=&quot;$BASE/weekly&quot;
MONTHLY=&quot;$BASE/monthly&quot;
LOG=&quot;$HOME/logs/db-backup.log&quot;
MYCNF=&quot;$HOME/.my.cnf&quot;
MYSQLDUMP=&quot;/usr/bin/mysqldump&quot;

mkdir -p &quot;$DAILY&quot; &quot;$WEEKLY&quot; &quot;$MONTHLY&quot; &quot;$(dirname &quot;$LOG&quot;)&quot;

log() { printf &#039;%s %s\n&#039; &quot;$(date &#039;+%F %T&#039;)&quot; &quot;$*&quot; &gt;&gt; &quot;$LOG&quot;; }

env_get() {
  sed -n &quot;s/^$1=//p&quot; &quot;$ENV_FILE&quot; | head -n1 | tr -d &#039;\r&#039; | sed -e &#039;s/^&quot;//&#039; -e &#039;s/&quot;$//&#039;
}

DB_NAME=&quot;$(env_get DB_DATABASE)&quot;
if &#91; -z &quot;$DB_NAME&quot; ]; then
  log &quot;ERROR: DB_DATABASE not found in $ENV_FILE&quot;
  exit 1
fi

STAMP=&quot;$(date +%F-%H%M)&quot;
OUT=&quot;$DAILY/${DB_NAME}-${STAMP}.sql.gz&quot;

&quot;$MYSQLDUMP&quot; --defaults-extra-file=&quot;$MYCNF&quot; \
  --single-transaction --quick --no-tablespaces \
  --routines --default-character-set=utf8mb4 \
  &quot;$DB_NAME&quot; | gzip -9 &gt; &quot;$OUT&quot;

if ! gzip -t &quot;$OUT&quot;; then
  log &quot;ERROR: corrupt archive $OUT&quot;
  rm -f &quot;$OUT&quot;
  exit 1
fi

SIZE=&quot;$(stat -c %s &quot;$OUT&quot;)&quot;
if &#91; &quot;$SIZE&quot; -lt 20000 ]; then
  log &quot;WARN: dump looks too small ($SIZE bytes) $OUT&quot;
fi

log &quot;OK $OUT ($SIZE bytes)&quot;`

قابل اجرا کنید:

Bashکپی`chmod +x ~/scripts/db-backup.sh
~/scripts/db-backup.sh`

بار اول را حتماً دستی اجرا کنید و به لاگ نگاه کنید. اگر همین‌جا کار نکند، در کرون هم کار نمی‌کند — فقط ساکت‌تر شکست می‌خورد.

## چرخش نسخه‌ها: هفت روزانه، چهار هفتگی، سه ماهانه#

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

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

![طرح نگه‌داری نسخه‌ها در سه ردیف روزانه، هفتگی و ماهانه](/blog/wp-content/uploads/2026/09/fig-fig-svg2-clpiq-lj4upx.svg)

این تکه را به انتهای همان اسکریپت اضافه کنید:

Bashکپی`# هفت نسخه‌ی روزانه
find &quot;$DAILY&quot; -maxdepth 1 -type f -name &#039;*.sql.gz&#039; -mtime +7 -delete

# جمعه‌ها یک کپی هفتگی، چهار هفته نگه‌داری
if &#91; &quot;$(date +%u)&quot; = &quot;5&quot; ]; then
  cp &quot;$OUT&quot; &quot;$WEEKLY/&quot;
  find &quot;$WEEKLY&quot; -maxdepth 1 -type f -name &#039;*.sql.gz&#039; -mtime +28 -delete
fi

# روز اول هر ماه میلادی یک کپی ماهانه، حدود سه ماه نگه‌داری
if &#91; &quot;$(date +%d)&quot; = &quot;01&quot; ]; then
  cp &quot;$OUT&quot; &quot;$MONTHLY/&quot;
  find &quot;$MONTHLY&quot; -maxdepth 1 -type f -name &#039;*.sql.gz&#039; -mtime +93 -delete
fi`

چند نکته که در عمل به آن خوردیم.

`-mtime +7` یعنی فایل‌هایی که بیش از هفت شبانه‌روز کامل از آخرین تغییرشان گذشته. پس در عمل هشت نسخه روی دیسک می‌بینید نه هفت‌تا. اگر دقت روز برایتان مهم است، عدد را با همین ذهنیت انتخاب کنید.

`-maxdepth 1` و `-type f` را بی‌خود نگذاشته‌ایم. `find` با `-delete` ابزار بی‌رحمی است؛ محدودش کنید به همان پوشه و همان الگوی نام. و قبل از اینکه `-delete` را بنویسید، یک بار بدون آن اجرا کنید تا فهرست فایل‌هایی که قرار است حذف شوند را ببینید:

Bashکپی`find &quot;$HOME/backups/daily&quot; -maxdepth 1 -type f -name &#039;*.sql.gz&#039; -mtime +7`

`date +%u` شماره‌ی روز هفته را از دوشنبه (۱) تا یکشنبه (۷) می‌دهد، پس `5` یعنی جمعه. برای نسخه‌ی ماهانه از روز اول ماه میلادی استفاده کرده‌ایم چون `date` استاندارد سرور شمسی نمی‌شناسد؛ اگر روز اول ماه شمسی برایتان مهم است، باید تاریخ را خودتان حساب کنید و آن یک بحث دیگر است.

## بک‌آپ روی همان دیسک، بک‌آپ نیست#

تا اینجا فقط جای فایل‌ها را عوض کرده‌ایم. اگر دیسک سرور خراب شود یا اکانت تعلیق شود، هم سایت رفته هم بک‌آپ. این مرحله را جدی بگیرید، چون همان چیزی است که اکثراً به «بعداً» موکول می‌شود.

سه قاعده‌ی ساده.

**بک‌آپ را هیچ‌وقت زیر `public_html` نگذارید.** یک فایل `.sql.gz` که از اینترنت قابل دانلود است، یعنی کل دیتابیس شما عمومی است — رمزهای هش‌شده، ایمیل‌ها، آدرس‌ها. پوشه‌ی `~/backups` بیرون از ریشه‌ی وب باشد.

**یک نسخه را دوره‌ای از سرور بیرون بکشید.** ساده‌ترین راهی که نیاز به سرویس اضافه ندارد این است که از یک ماشین دیگر (لپ‌تاپ خودتان یا یک سرور کوچک) با `rsync` روی SSH بکشید:

Bashکپی`rsync -avz --delete \
  -e &quot;ssh -p 22&quot; \
  cpuser@example.com:~/backups/weekly/ \
  /home/me/offsite/shop-weekly/`

کشیدن بهتر از فرستادن است: اگر سرور سایت را کسی در دست بگیرد، کلید مقصد روی آن سرور نیست. اگر SSH ندارید، همان کار را دوره‌ای با FTP و به‌صورت دستی انجام دهید — دستی و ماهانه، بی‌نهایت بهتر از هرگز است.

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

## کرون‌جاب بک‌آپ روی سی‌پنل: مسیر مطلق و لاگ خطا#

در بخش Cron Jobs سی‌پنل یک ورودی روزانه بسازید:

Bashکپی`5 3 * * * /bin/bash /home/cpuser/scripts/db-backup.sh &gt;&gt; /home/cpuser/logs/cron-db-backup.log 2&gt;&amp;1`

چند نکته که همه‌شان یک بار ما را زده‌اند.

**همه‌ی مسیرها مطلق.** شل کرون یک شل غیرتعاملی است: نه `.bashrc` شما را می‌خواند، نه `PATH` کاملتان را دارد، نه alias‌هایتان را می‌شناسد. چیزی که در ترمینال بی‌عیب کار می‌کند ممکن است در کرون اصلاً پیدا نشود. همین قصه برای نسخه‌ی PHP هم صادق است و جدا نوشتیمش: [تله‌ی alias در شل غیرتعاملی کرون](/blog/php84-در-کرون-جاب-سی-پنل). به همین دلیل در اسکریپت هم `MYSQLDUMP=&quot;/usr/bin/mysqldump&quot;` را صریح نوشتیم. اگر مطمئن نیستید مسیرش کجاست، یک بار `command -v mysqldump` بزنید.

**خروجی را خاموش کنید، ولی خطا را نه.** نوشتن `&gt;/dev/null 2&gt;&amp;1` وسوسه‌انگیز است چون جلوی ایمیل‌های روزانه‌ی کرون را می‌گیرد. ولی همان دستور خطاها را هم دور می‌ریزد. آن‌وقت اسکریپت شما می‌تواند ماه‌ها بی‌صدا شکست بخورد و شما دقیقاً روزی بفهمید که به بک‌آپ نیاز دارید. راه درست `&gt;&gt; فایل 2&gt;&amp;1` است: خروجی از ایمیل بیرون می‌رود ولی روی دیسک می‌ماند.

**ایمیل کرون را در پنل خالی کنید** (یا `MAILTO=&quot;&quot;` بگذارید) تا هر شب یک ایمیل بی‌محتوا نگیرید و در نتیجه ایمیل‌های کرون را نادیده نگیرید.

**گاهی به لاگ سر بزنید.** یک لاگی که کسی نمی‌خواندش با نبودنش فرقی ندارد. ساده‌ترین عادت: هر بار که به SSH وصل شدید، `tail -n 20 ~/logs/db-backup.log`.

و یک هشدار: اگر وسوسه شدید بک‌آپ را از داخل خود لاراول و با `shell_exec` بگیرید، احتمالاً به دیوار [disable_functions روی هاست اشتراکی](/blog/disable-functions-هاست-اشتراکی) می‌خورید. روی بسیاری از هاست‌های اشتراکی `exec` و `shell_exec` و `proc_open` بسته‌اند. جای این کار، کرون است.

## تست بازیابی؛ بک‌آپ تست‌نشده بک‌آپ نیست#

این بخش را کسی انجام نمی‌دهد و دقیقاً به همین دلیل مهم‌ترین بخش مقاله است. تا وقتی یک فایل را برنگردانده باشید، نمی‌دانید بک‌آپ دارید یا فقط یک پوشه فایل `.gz` دارید.

اول یک نگاه سرسری به محتوا:

Bashکپی`gunzip -c ~/backups/daily/cpuser_shop-2026-09-05-0305.sql.gz | head -n 40`

باید هدر `mysqldump`، نسخه‌ی سرور، و اولین `CREATE TABLE` را ببینید. اگر خالی بود یا وسطش پیام خطا داشت، همان‌جا معلوم می‌شود.

بعد ریستور واقعی در یک دیتابیس موقت. روی سی‌پنل معمولاً کاربر شما اجازه‌ی `CREATE DATABASE` از خط فرمان ندارد؛ دیتابیس را از بخش MySQL Databases پنل بسازید (مثلاً `cpuser_rtest`)، همان کاربر را با دسترسی کامل به آن وصل کنید، و بعد:

Bashکپی`gunzip -c ~/backups/daily/cpuser_shop-2026-09-05-0305.sql.gz \
  | mysql --defaults-extra-file=&quot;$HOME/.my.cnf&quot; cpuser_rtest`

اگر بی‌صدا تمام شد، خبر خوبی است. حالا سه چیز را بررسی کنید:

SQLکپی`SELECT COUNT(*) FROM users;
SELECT MAX(created_at) FROM orders;
SELECT TABLE_NAME, TABLE_COLLATION
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = &#039;cpuser_rtest&#039;;`

اولی می‌گوید داده آمده. دومی می‌گوید داده تا کِی تازه است — اگر جدیدترین رکورد مال دو هفته پیش است، یعنی کرون شما مدتی است کار نمی‌کند. سومی می‌گوید کاراکترست جدول‌ها سالم مانده؛ اگر جایی `latin1` دیدید، متن فارسی‌تان در خطر است و باید سوییچ `--default-character-set` و تنظیمات دیتابیس مقصد را بازبینی کنید.

آخرش هم یک نگاه چشمی به داده‌ی فارسی بیندازید:

SQLکپی`SELECT id, name FROM products ORDER BY id DESC LIMIT 5;`

اگر به‌جای فارسی علامت سؤال یا کاراکترهای عجیب دیدید، مشکل در همان زنجیره‌ی کاراکترست است نه در بک‌آپ.

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

## فایل‌های storage و چیزهایی که نباید بک‌آپ گرفت#

دیتابیس نصف ماجراست. تصویر محصول، فایل‌های قابل دانلود و فاکتورها روی دیسک‌اند. برای فروشگاه فایل، از دست رفتن `storage/app` تقریباً به بدی از دست رفتن دیتابیس است.

بک‌آپ فایل‌ها را جدا و کم‌تکرارتر بگیرید، چون حجمش خیلی بیشتر است:

Bashکپی`tar -czf &quot;$HOME/backups/files/storage-$(date +%F).tar.gz&quot; \
  --exclude=&#039;storage/framework/cache/*&#039; \
  --exclude=&#039;storage/framework/sessions/*&#039; \
  --exclude=&#039;storage/framework/views/*&#039; \
  --exclude=&#039;storage/logs/*&#039; \
  -C &quot;$HOME/app&quot; storage`

توجه کنید که `storage/app/public` معمولاً با یک لینک از `public/storage` سرو می‌شود. خودِ لینک را بک‌آپ نگیرید؛ بعد از ریستور دوباره بسازیدش. روی سی‌پنل این کار خودش تله دارد که در [storage:link روی سی‌پنل](/blog/storage-link-روی-سی-پنل) نوشتیمش.

و حالا فهرست چیزهایی که **نباید** در بک‌آپ روزانه باشند، چون فقط حجم و زمان می‌خورند:

- `vendor/` — با `composer install` از روی `composer.lock` بازساخته می‌شود. فقط مطمئن شوید خود `composer.lock` در گیت هست. اگر روی سرور اصلاً `composer` ندارید، یک نسخه از `vendor` را در زمان هر انتشار جدا نگه دارید، نه هر شب.

- `node_modules/` — هیچ‌وقت.

- `storage/framework/cache`، `sessions`، `views` — همه بازساختنی. `sessions` را بک‌آپ نگیرید مگر اینکه واقعاً بخواهید کاربرها بعد از ریستور لاگین بمانند.

- `storage/logs` — سنگین و بی‌فایده در بک‌آپ. اگر لاگ لازم دارید، جدا آرشیوش کنید.

- `bootstrap/cache` — بازساختنی با `php artisan config:cache`.

- خود پوشه‌ی `backups` — این را جدی بگیرید؛ یک `tar` بدون exclude که پوشه‌ی بک‌آپ را هم بردارد، هر شب حجم را چند برابر می‌کند.

در مقابل، این‌ها باید باشند: `storage/app`، فایل `.env` (در جای امن و جدا)، و کد که در گیت است.

## جمع‌بندی#

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

اگر امروز فقط وقت یک کار را دارید، آخرین فایل بک‌آپتان را در یک دیتابیس موقت ریستور کنید و تعداد ردیف‌های جدول اصلی را بشمارید. جوابِ آن `COUNT` تنها چیزی است که واقعاً می‌دانید.