کرون جاب (Cron Job) سرور را از دستگاهی که باید مدام بالای سرش باشید، به کارمندی تبدیل میکند که ۳:۳۰ بامداد بکاپ میگیرد، هر ۵ دقیقه مصرف را ثبت میکند و اول هر ماه گزارش میفرستد — بیآنکه شما بیدار باشید. در این آموزش crontab را از پایه میبینید: نحو پنجفیلدی با جدول مثالهای واقعی، رشتههای @daily و @reboot، و مهمتر از همه اینکه چرا دستوری که در ترمینال بینقص کار میکند، در cron بیصدا شکست میخورد.
کرون جاب چیست و چه کارهایی را باید به آن سپرد؟
cron یک سرویس (daemon) قدیمی و بهشدت پایدار در لینوکس است که هر دقیقه جدولهای زمانبندی را نگاه میکند و هر خطی را که وقتش رسیده باشد اجرا میکند. به هر یک از این خطها — یک زمانبندی بهعلاوهی یک فرمان — «کرون جاب» میگویند، و مجموعهشان در فایلی بهنام crontab (کوتاهشدهی cron table) نگهداری میشود.
کارهایی که بهطور کلاسیک به cron سپرده میشوند یک ویژگی مشترک دارند: دورهایاند و شروع و پایانِ مشخص دارند:
- بکاپ شبانه از فایلها و دیتابیس، در ساعتی که سرور خلوت است.
- پاکسازی دورهای: فایلهای موقت، لاگهای قدیمی، کشهای منقضی — قبل از اینکه دیسک پر شود.
- گزارش و پایش: ثبت مصرف منابع هر چند دقیقه، گزارش هفتگی، چک سلامت سرویسها.
- نگهداری: تمدید گواهی SSL، همگامسازی فایلها، بهروزرسانی فهرستها.
و یک کاربرد رایج که عمداً جدایش میکنیم: «زنده نگهداشتن» ربات تلگرام یا هر پروسهی دائمی — در انتهای مقاله میبینید چرا این یکی کار cron نیست. اگر هم هنوز با ترمینال دستبهعصا هستید، اول نگاهی به دستورات پرکاربرد لینوکس بیندازید.
cron تقریباً روی هر توزیعی از پیش نصب است؛ اگر روی ایمیج مینیمال نبود:
sudo apt install cron && sudo systemctl enable --now cron # Debian / Ubuntu
sudo dnf install cronie && sudo systemctl enable --now crond # AlmaLinux / Rocky
آموزش crontab: ویرایش، مشاهده و پشتیبانگیری از جدول
هر کاربر لینوکس crontab شخصی خودش را دارد و کارهای داخل آن با دسترسی همان کاربر اجرا میشوند؛ پس کاری که root لازم ندارد را در crontab کاربر عادی بگذارید.
crontab -e # edit your crontab
crontab -l # list current jobs
crontab -l > ~/cron-backup.txt # save a plain-text backup
sudo crontab -u www-data -l # view another user's table (root only)
بار اول که crontab -e را میزنید، ویرایشگر را از شما میپرسد؛ اگر بهاشتباه vi را انتخاب کردید، در اوبونتو با دستور select-editor قابل تغییر است. بعد از ذخیره، تغییرات بلافاصله اعمال میشوند — نه ریاستارتی لازم است، نه reload.
crontab -r کل جدول را بدون حتی یک سؤال حذف میکند. حرف r روی صفحهکلید دقیقاً کنار e است و یک خطای تایپی یعنی همهی زمانبندیها ناپدید شدهاند — بدون تأیید و بدون راه برگشت. عادت کنید بعد از هر تغییر مهم، خروجی crontab -l را در فایلی ذخیره کنید.نحو پنجفیلدی cron — یکبار برای همیشه
هر خط crontab از پنج فیلد زمان و سپس فرمان تشکیل میشود. فیلدها با فاصله جدا میشوند و ترتیبشان هرگز عوض نمیشود:
┌───────────── minute (0-59)
│ ┌─────────── hour (0-23)
│ │ ┌───────── day of month (1-31)
│ │ │ ┌─────── month (1-12)
│ │ │ │ ┌───── day of week (0-7, 0 or 7 = Sunday)
│ │ │ │ │
* * * * * command-to-run
چهار عملگر همهی حالتها را میسازند: ستاره یعنی «هر مقدار»، کاما فهرست میسازد (1,15)، خط تیره بازه میسازد (9-17) و اسلش گام تعریف میکند (*/5 یعنی هر ۵ واحد). با همین چهار عملگر، جدول زیر رایجترین زمانبندیهای دنیای واقعی را پوشش میدهد:
| عبارت cron | چه زمانی اجرا میشود | کاربرد نمونه |
|---|---|---|
*/5 * * * * | هر ۵ دقیقه | اسنپشات مصرف، چک سلامت |
30 3 * * * | هر شب ساعت ۳:۳۰ بامداد | بکاپ شبانه |
0 0 1 * * | نیمهشبِ اول هر ماه | گزارش ماهانه، آرشیو لاگ |
0 9 * * 1 | هر دوشنبه ساعت ۹ صبح | گزارش هفتگی |
0 4 * * 5 | هر جمعه ساعت ۴ بامداد | کارهای سنگین آخر هفته |
0 */6 * * * | هر ۶ ساعت، سرِ ساعت | همگامسازی دورهای |
*/10 9-17 * * * | هر ۱۰ دقیقه، فقط ۹ تا ۱۷ | کارهای ساعات کاری |
یک تلهی قدیمی: اگر هم «روز ماه» و هم «روز هفته» را محدود کنید، cron بین آنها OR میگذارد، نه AND. عبارت 0 3 13 * 5 هم سیزدهمِ هر ماه اجرا میشود و هم تمامِ جمعهها — نه فقط جمعهی سیزدهم. برای شرطهای ترکیبی، منطق تاریخ را داخل اسکریپت ببرید.
رشتههای ویژه: @daily و @reboot و بقیه
برای زمانبندیهای خیلی رایج، میانبرهای خواناتری هم وجود دارد:
@reboot /usr/local/bin/warmup.sh # once, right after boot
@hourly /usr/local/bin/sync.sh # minute 0 of every hour
@daily /usr/local/bin/cleanup.sh # every day at 00:00
@weekly /usr/local/bin/report.sh # Sunday at 00:00
@monthly /usr/local/bin/archive.sh # 1st of month at 00:00
@reboot برای کارهایی مثل گرمکردن کش عالی است، اما «هنگام شروع سرویس cron» اجرا میشود، نه لزوماً وقتی شبکه آماده است. و @daily یعنی رأس نیمهشب؛ ده کار @daily یعنی ده اجرای همزمان و یک جهش بار — زمانها را عمداً پخش کنید: ۳:۱۰، ۳:۳۰، ۳:۵۰.
همین نحو را یکبار درست یاد بگیرید و کارتان با جدولها تمام است؛ بقیهی این مقاله دربارهی چیزی است که هیچ جدول مرجعی به شما نمیگوید: چرا کارِ درستِ زمانبندیشده، اجرا نمیشود.
چرا کرون جاب بیصدا شکست میخورد؟
پرتکرارترین جملهی دنیای cron این است: «دستور در ترمینال کار میکند، ولی در cron نه.» دلیلش تقریباً همیشه یکی است: cron فرمان شما را در محیطی بسیار حداقلیتر از شلِ شما اجرا میکند و خطایش را هم جایی نشان نمیدهد.
محیط حداقلی و مسئلهی PATH
وقتی وارد سرور میشوید، bash فایلهای پروفایل (.bashrc و .profile) را میخواند و یک PATH بلندبالا میسازد. cron هیچکدام را نمیخواند: PATH آن معمولاً فقط /usr/bin:/bin است و شل پیشفرضش /bin/sh است، نه bash. نتیجه: فرمانی مثل certbot که در ترمینال پیدا میشود، در cron خطای «command not found» میگیرد — و شما آن خطا را هرگز نمیبینید.
# put these at the top of `crontab -e` once
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# or find the absolute path once, and always use it
which certbot
# /usr/bin/certbot
قانون طلایی: در crontab همیشه مسیر مطلق بنویسید — هم برای فرمان، هم برای فایلها؛ همین یک عادت، نیمی از مشکلات cron را از ریشه حذف میکند.
علامت ٪ معنای ویژه دارد
در خط crontab، کاراکتر درصد به خط جدید تبدیل میشود و هرچه بعد از اولین درصد بیاید، ورودی استانداردِ فرمان میشود. قربانی همیشگی این قاعده: date.
# BROKEN: cron cuts this line at the first %
0 2 * * * tar -czf /backup/www-$(date +%F).tar.gz -C /var/www .
# CORRECT: escape % with a backslash inside a crontab line
0 2 * * * tar -czf /backup/www-$(date +\%F).tar.gz -C /var/www .
نه ترمینالی هست، نه سؤالوجوابی
cron فرمان را بدون TTY تعاملی اجرا میکند: sudoای که رمز بخواهد، sshای که تأیید yes/no بخواهد و هر ابزارِ منتظرِ پاسخ، یا میمیرد یا آویزان میماند — همهچیز باید غیرتعاملی باشد (سوییچ -y، حالت batch، احراز هویت کلیدمحور). متغیر HOME از /etc/passwd ست میشود، اما دایرکتوری کاری هم همان HOME است نه پوشهی پروژه؛ پس مسیرهای نسبی میشکنند — در ابتدای فرمان cd /opt/app بگذارید یا مسیرها را در اسکریپت مطلق کنید.
grep CRON /var/log/syslog (در خانوادهی RHEL: /var/log/cron). این لاگ فقط میگوید cron فرمان را «صدا زده»، نه اینکه فرمان موفق بوده — برای دیدن خطاها به بخش بعد نیاز دارید.خروجی را نگه دارید: ریدایرکت، MAILTO و قفل flock
رفتار پیشفرض cron این است که هر خروجی (stdout و stderr) را با ایمیلِ محلی برای صاحب crontab بفرستد. اما روی اغلب سرورهای مجازی هیچ MTA (مثل Postfix) نصب نیست؛ آن ایمیل هیچجا نمیرود و پیام خطا عملاً دود میشود. راهحل ساده و همیشگی، ریدایرکت به فایل لاگ است:
30 3 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
>> یعنی «به انتهای فایل اضافه کن» و 2>&1 یعنی «خطاها هم به همانجا». اگر شب بد پیش رفت، صبح مدرک دارید؛ فقط فایلهای لاگِ رشدکننده را به logrotate بسپارید.
اگر سرورتان MTA یا relay ایمیل دارد، گزارش را با ایمیل هم میتوانید بگیرید: متغیر MAILTO را بالای crontab ست کنید؛ مقدار خالی (MAILTO="") ارسال را کلاً خاموش میکند:
MAILTO=admin@example.com
0 4 * * 1 /usr/local/bin/weekly-report.sh
flock: نگذارید اجراها روی هم سوار شوند
کاری که هر ۵ دقیقه اجرا میشود ولی گاهی ۷ دقیقه طول میکشد، کمکم چند نسخهی همزمان از خودش را روی سرور جمع میکند: دو rsync موازی، دو dump همزمان از یک دیتابیس، و باری که هر اجرا برای اجراهای بعدی سنگینترش میکند. راهحل استاندارد flock است؛ سوییچ -n میگوید اگر قفل هنوز دست نسخهی قبلی است، این اجرا بیسروصدا صرفنظر شود:
*/5 * * * * flock -n /var/lock/bwsnap.lock /usr/local/bin/bwsnap.sh >> /var/log/bwsnap.log 2>&1
crontab کاربر، /etc/crontab و cron.d — فیلد ششمی که همه را غافلگیر میکند
کرون جابها سه خانهی اصلی دارند و فرقشان دقیقاً یک فیلد است. crontab شخصی پنجفیلدی است و با دسترسی صاحبش اجرا میشود؛ اما /etc/crontab و فایلهای /etc/cron.d/ سیستمیاند و یک فیلد اضافه دارند: نام کاربری که فرمان با آن اجرا میشود، بین زمانبندی و فرمان:
# /etc/crontab and /etc/cron.d/* use SIX fields: user before command
30 3 * * * root /usr/local/bin/backup.sh
# a personal crontab (crontab -e) uses FIVE fields, no user column
30 3 * * * /usr/local/bin/backup.sh
گافِ کلاسیک دو طرف دارد. خطی را از /etc/crontab به crontab شخصی کپی میکنید و cron سعی میکند فرمانی بهنام root اجرا کند («root: command not found»)؛ یا برعکس، خط پنجفیلدی در cron.d میگذارید و cron اولین کلمهی فرمان را نام کاربر فرض میکند — کاربری که وجود ندارد و کار هرگز اجرا نمیشود. نکتهی ریزتر: نام فایل در /etc/cron.d نباید نقطه داشته باشد؛ فایلی بهاسم backup.sh آنجا بیصدا نادیده گرفته میشود.
راه سوم پوشههای /etc/cron.daily و cron.weekly و cron.monthly است: اسکریپت اجرایی را داخلشان بگذارید تا run-parts اجرایش کند — اغلب با کمک anacron، که اجرای عقبافتادهی سرورِ خاموش را بعد از روشنشدن جبران میکند.
منطقهی زمانی: ۳:۳۰ شما، ۳:۳۰ سرور نیست
cron با ساعت محلی سیستم کار میکند و اغلب ایمیجهای ابری روی UTC تحویل داده میشوند. تهران UTC+3:30 است؛ یعنی بکاپِ «۳:۳۰ بامدادِ» شما در واقع ساعت ۷ صبح — درست ابتدای ساعت شلوغی — اجرا میشود. جابهجایی مرز دورهها هم داستان خودش را دارد: همانطور که در راهنمای پهنای باند دیدید، همین ۳٫۵ ساعت اختلاف، چند گیگابایت مصرفِ روز آخر ماه را به ماهِ صورتحسابِ بعدی میبرد.
timedatectl | grep "Time zone"
# Time zone: Etc/UTC (UTC, +0000)
sudo timedatectl set-timezone Asia/Tehran
sudo systemctl restart cron # cron reads the timezone at startup
ریاستارت آخر مهم است: cron منطقهی زمانی را هنگام شروع میخواند و تا ریاستارت نشود، با ساعت قبلی ادامه میدهد.
تست همین حالا، اطمینان از فردا
اجرای فرمان در محیطِ شبیهِ cron
بهجای اینکه تا نیمهشب صبر کنید، همان فرمان را همین حالا در محیطی حداقلی اجرا کنید. env -i همهی متغیرهای شل را دور میریزد و فقط چیزی میماند که خودتان بدهید — دقیقاً مثل cron:
env -i HOME="$HOME" PATH=/usr/bin:/bin SHELL=/bin/sh /bin/sh -c '/usr/local/bin/backup.sh'
اگر اینجا کار کرد، در cron هم کار میکند؛ اگر شکست، همان خطایی را جلوی چشمتان میبینید که cron بیصدا قورت میداد.
* * * * * (هر دقیقه) بگذارید و لاگ را با tail -f تماشا کنید؛ بعد از یکی دو اجرای موفق، زمانبندی واقعی را برگردانید. این دو دقیقه صبر، از هر حدسزدنی سریعتر است.مانیتورینگ: سکوت یعنی خبر بد
فایل لاگ فقط به دردِ کسی میخورد که آن را بخواند — و هیچکس هر روز لاگ بکاپ را نمیخواند. برای کارهای حیاتی منطق را برعکس کنید: اسکریپت در پایانِ اجرای موفق یک URL را صدا میزند و اگر این تماس در موعدش نیاید، سیستم پایش هشدار میدهد. به این الگو dead man's switch میگویند و سرویسهایی مثل Healthchecks برای همین ساخته شدهاند:
# success ping: silence means trouble
30 3 * * * /usr/local/bin/backup.sh && curl -fsS -m 10 --retry 3 https://hc-ping.com/your-uuid-here >/dev/null
الگوی مکمل، هشدار فقط هنگام شکست است — مثلاً با پیام تلگرام. متغیرها را بالای crontab تعریف کنید تا خط اصلی خوانا بماند:
TOKEN=123456:AAErealtokenhere
CHAT=11111111
30 3 * * * /usr/local/bin/backup.sh || curl -s -X POST "https://api.telegram.org/bot$TOKEN/sendMessage" -d chat_id="$CHAT" -d text="backup FAILED on $(hostname)"
تفاوتشان مهم است: هشدارِ شکست فقط شکستِ خودِ فرمان را میگیرد، اما الگوی ping حتی خاموشبودن سرور یا ازکارافتادن خود cron را هم لو میدهد. ساخت کامل هشدار تلگرامی و پایش رم و CPU و ترافیک در آموزش مانیتورینگ سرور لینوکس آمده است.
کرون جاب یا تایمر systemd؟ مقایسهی صادقانه
هر لینوکس مدرنی systemd دارد و systemd زمانبند خودش را: واحدهای timer. هیچکدام «بهتر مطلق» نیست؛ ابزارها برای دو جور کار ساخته شدهاند:
| معیار | cron | تایمر systemd |
|---|---|---|
| راهاندازی | یک خط در crontab | دو فایل (service + timer) و چند دستور |
| جبران اجرای ازدسترفته (سرور خاموش) | ندارد (مگر با anacron) | Persistent=true — در بوت بعدی اجرا میشود |
| لاگ | خودتان باید ریدایرکت کنید | خودکار در journalctl |
| جلوگیری از اجرای همزمان | دستی با flock | ذاتی — تا سرویس فعال است دوباره شروع نمیشود |
| پخشکردن بار | ندارد | RandomizedDelaySec |
| زنده نگهداشتن پروسهی دائمی | کار cron نیست | Restart=always |
جمعبندی بیتعارف: برای کار دورهای ساده cron کاملاً کافی است و یک خط بیشتر نمیخواهد. سراغ تایمر systemd وقتی بروید که جبران اجرای ازدسترفته حیاتی است، لاگ ساختیافته میخواهید یا کار به سرویس دیگری وابسته است. و یک قانون بیاستثنا: پروسهی دائمی را systemd زنده نگه میدارد، نه cron — نمونهاش همین پایین.
سه نمونهی واقعی، آمادهی کپی
۱) بکاپ شبانهی ساعت ۳:۳۰ با tar
# crontab -e
30 3 * * * flock -n /var/lock/backup.lock /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
#!/bin/bash
# /usr/local/bin/backup.sh -- make it executable: chmod +x
set -euo pipefail
STAMP=$(date +%F) # inside a script, % needs no escaping
mkdir -p /backup
tar -czf /backup/www-$STAMP.tar.gz -C /var/www .
find /backup -name 'www-*.tar.gz' -mtime +7 -delete
به تفاوت ظریف دقت کنید: در خط crontab درصد به بکاسلش نیاز داشت، داخل اسکریپت نه — یکی از دلایلی که توصیه میکنیم منطق همیشه در اسکریپت باشد و crontab فقط زمانبندش. نسخهی حرفهایتر — بکاپ افزایشی، رمزگذاری و انتقال به مقصد بیرونی — در آموزش بکاپگیری از سرور لینوکس آمده است.
۲) اسنپشات ۵ دقیقهای پهنای باند
*/5 * * * * /usr/local/bin/bwsnap.sh eth0 >/dev/null 2>&1
اسکریپت bwsnap.sh همان شمارندهخوانِ مقاومبهریبوت است که در راهنمای پهنای باند سرور مجازی خطبهخط ساختیم. چون شمارندههای کرنل تجمعیاند، فاصلهی ۵ دقیقهای هیچ دادهای را از دست نمیدهد؛ فاصلهی کوتاهتر فقط CPU مصرف میکند، دقت اضافه نمیکند.
۳) زنده نگهداشتن ربات تلگرام — کاری که به cron ندهید
الگوی رایج و اشتباه: * * * * * pgrep -f bot.py || python3 /opt/bot/bot.py. یعنی بعد از هر کرش تا ۵۹ ثانیه قطعی، و اگر الگوی pgrep نادقیق باشد، دو نسخهی موازی ربات و پاسخهای تکراری. ابزار درست، سرویس systemd با ریاستارت خودکار است:
# /etc/systemd/system/telegram-bot.service
[Unit]
Description=Telegram bot
After=network-online.target
Wants=network-online.target
[Service]
User=botuser
ExecStart=/usr/bin/python3 /opt/bot/bot.py
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now telegram-bot
حالا ربات چند ثانیه بعد از هر کرش برمیگردد و بعد از ریبوت هم خودش بالا میآید. راهاندازی کامل ربات از صفر — توکن، وبهوک و نکات امنیتی — در راهنمای راهاندازی ربات تلگرام روی سرور مجازی.
سؤالات پرتکرار
cron job چیست و چه فرقی با crontab دارد؟
cron سرویس زمانبند لینوکس است که هر دقیقه جدولها را بررسی میکند؛ کرون جاب یک خط از آن جدول است — زمانبندی پنجفیلدی بهعلاوهی یک فرمان — و crontab هم نام جدول است و هم نام دستوری که آن را میبینید و ویرایش میکنید (crontab -l و crontab -e).
چرا دستورم در ترمینال کار میکند ولی کرون جابش اجرا نمیشود؟
چون cron پروفایل شل شما را نمیخواند: PATH آن معمولاً فقط /usr/bin:/bin است، شل پیشفرضش sh است نه bash، ترمینال تعاملی ندارد و علامت درصد را هم خط جدید حساب میکند. مسیرها را مطلق بنویسید، درصد را با بکاسلش escape کنید و فرمان را یک بار با env -i در محیط حداقلی تست کنید تا خطای واقعی را ببینید.
از کجا مطمئن شوم کرون جاب شبانه واقعاً اجرا شده است؟
سه لایه بسازید: ثبت اجرا در syslog (grep CRON /var/log/syslog)، ریدایرکت خروجی به فایل لاگ با >> و 2>&1، و برای کارهای حیاتی الگوی dead man's switch — اسکریپت در پایان موفق یک URL را صدا میزند و اگر این تماس نیاید هشدار میگیرید. لایهی سوم حتی خاموشبودن سرور یا ازکارافتادن خود cron را هم لو میدهد.
کرون جاب بهتر است یا تایمر systemd؟
برای کارهای دورهای ساده cron کافی و بسیار سادهتر است. تایمر systemd وقتی میارزد که جبران اجرای ازدسترفته، لاگ خودکار در journalctl یا وابستگی به سرویسهای دیگر لازم داشته باشید. زنده نگهداشتن پروسهی دائمی — مثل ربات تلگرام — هم اصلاً کار cron نیست؛ آن را به سرویس systemd با Restart=always بسپارید.
قدم بعدی
از همین امروز شروع کنید: برای هر کرون جاب فعلیتان فایل لاگ بگذارید، مسیرها را مطلق کنید، منطقهی زمانی سرور را چک کنید و برای بکاپ شبانه یک ping سلامت بسازید. اگر هم سرور آزمایشی ندارید، یک سرور ابری ساعتی بسازید: در حدود ۶۰ ثانیه تحویل میگیرید، چند کرون جاب واقعی میچینید، خراب میکنید و درست میکنید — و فقط هزینهی همان ساعتهایی را میدهید که سرور روشن بوده.