آموزش کرون جاب در لینوکس: ساخت، مشاهده و نمونه‌های کاربردی crontab

یک اسکریپت هر شب روی سرور اجرا می‌شود و شما یادتان نمی‌آید آن را ساخته باشید؛ یا برعکس، جاب پشتیبان‌گیری را در کرون گذاشته‌اید و هیچ‌وقت اجرا نمی‌شود. در هر دو حالت ریشه ماجرا معمولاً یک چیز است: دستور crontab -l فقط یکی از محل‌های نگهداری زمان‌بندی را نشان می‌دهد. کرون‌جاب‌های یک سرور لینوکسی در چند مکان جدا از هم ذخیره می‌شوند و تا همه‌شان را نبینید تصویر کاملی از کارهای زمان‌بندی‌شده ندارید.

هر آموزش کرون جاب که فقط crontab -l را یاد بدهد نصف ماجرا را گفته است. محل‌های زمان‌بندی روی یک سرور معمولی این‌ها هستند:

  • crontab شخصی هر کاربر (از جمله root) در /var/spool/cron/ — و روی دبیان و اوبونتو در /var/spool/cron/crontabs/
  • فایل سیستمی /etc/crontab
  • فایل‌های بسته‌های نرم‌افزاری در /etc/cron.d/
  • اسکریپت‌های پوشه‌های /etc/cron.hourly/، /etc/cron.daily/، /etc/cron.weekly/ و /etc/cron.monthly/
  • و روی توزیع‌های امروزی، تایمرهای systemd که کم‌کم جای بخشی از کرون را گرفته‌اند

نحو پنج‌فیلدی crontab، خط‌به‌خط

هر خط کرون‌جاب در crontab یک کاربر از پنج فیلد زمانی و بعد از آن دستوری که باید اجرا شود ساخته می‌شود. توضیح همین فیلدها در بالای فایل /etc/crontab هم آمده است — دقت کنید که نمونه‌ی آخرِ این توضیح یک فیلد user-name هم دارد، چون /etc/crontab یک فایل سیستمی است و نه crontab شخصی یک کاربر؛ کمی پایین‌تر مفصل به این تفاوت می‌پردازیم:

# .---------------- minute (0 - 59)
# | .------------- hour (0 - 23)
# | | .---------- day of month (1 - 31)
# | | | .------- month (1 - 12) OR jan,feb,mar,apr ...
# | | | | .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# | | | | |
# * * * * * user-name command to be executed

پنج فیلد زمانی به‌ترتیب این‌ها هستند: دقیقه (۰ تا ۵۹)، ساعت (۰ تا ۲۳)، روز ماه (۱ تا ۳۱)، ماه (۱ تا ۱۲ یا نام سه‌حرفی انگلیسی) و روز هفته (۰ تا ۶ که در آن یکشنبه صفر است — ۷ هم یکشنبه است — یا نام سه‌حرفی انگلیسی). در crontab شخصی که با crontab -e باز می‌شود، بلافاصله بعد از این پنج فیلد خودِ دستور می‌آید و فیلد user-name وجود ندارد.

در هر فیلد می‌توانید از این نشانه‌ها استفاده کنید:

  • * یعنی «هر مقداری». مثلاً * * * * * یعنی هر دقیقه.
  • , برای فهرست کردن چند مقدار: 0 8,20 * * * یعنی ساعت ۸ صبح و ۸ شب.
  • - برای بازه: 0 9-17 * * 1-5 یعنی هر ساعت از ۹ تا ۱۷، فقط در روزهای دوشنبه تا جمعه (چون ۰ یکشنبه است، ۱ تا ۵ می‌شود دوشنبه تا جمعه).
  • / برای گام: */5 * * * * یعنی هر پنج دقیقه و 0 */6 * * * یعنی هر شش ساعت یک‌بار.

پرتکرارترین اشتباه در نوشتن این خط، جابه‌جا گرفتن دقیقه و ساعت است. در 0 15 * * * عدد ۱۵ ساعت است نه دقیقه؛ یعنی هر روز رأس ساعت ۱۵:۰۰. اگر می‌خواهید هر روز ساعت ۳ بامداد اجرا شود بنویسید 0 3 * * *.

یک نکته که کمتر جایی گفته می‌شود: اگر هم فیلد روز ماه و هم فیلد روز هفته را مقداردهی کنید (یعنی هیچ‌کدام * نباشند)، کرون رابطه‌شان را «یا» در نظر می‌گیرد نه «و». مثلاً 0 0 13 * 5 هم روز سیزدهم هر ماه اجرا می‌شود و هم هر جمعه — نه فقط جمعه‌ی سیزدهم.

به‌جای پنج فیلد می‌توانید از کلیدواژه‌های میان‌بر هم استفاده کنید که خواناترند: @hourly، @daily (هم‌ارز @midnight@weekly، @monthly، @yearly و @reboot که فقط یک‌بار پس از هر بالا آمدن سرور اجرا می‌شود.

ساخت، ویرایش و حذف کرون‌جاب با دستور crontab

برای ویرایش زمان‌بندی‌های کاربری که با آن وارد شده‌اید:

crontab -e

اگر ویرایشگری که باز می‌شود vi است و با آن راحت نیستید، می‌توانید ویرایشگر را برای همان اجرا عوض کنید:

EDITOR=nano crontab -e

در nano با Ctrl+O ذخیره و با Ctrl+X خارج می‌شوید؛ در vi با :wq. بعد از ذخیره لازم نیست سرویس کرون را ریستارت کنید؛ کرون خودش تغییر فایل را تشخیص می‌دهد و جاب جدید را برمی‌دارد.

برای دیدن کرون‌جاب‌های کاربر جاری (که روی SSH معمولاً root است):

crontab -l

برای دیدن کرون‌جاب‌های یک کاربر مشخص (این کار نیاز به دسترسی root دارد):

crontab -u username -l

مثلاً برای کاربر testuser:

crontab -u testuser -l

و برای حذف کامل crontab یک کاربر:

crontab -r

مراقب این دستور باشید. crontab -r بدون هیچ پرسش تأییدی همه‌ی زمان‌بندی‌های آن کاربر را پاک می‌کند و راه برگشتی ندارد — و از آن بدتر، crontab -r فقط یک حرف با crontab -e فاصله دارد. پیش از هر دستکاری یک نسخه پشتیبان بگیرید:

crontab -l > /root/crontab-backup.txt

روی cronie (پیش‌فرض AlmaLinux، Rocky و CentOS) می‌توانید سوئیچ -i را هم اضافه کنید تا قبل از حذف تأیید بگیرد:

crontab -r -i

اگر می‌خواهید تعیین کنید کدام کاربران اجازه‌ی استفاده از دستور crontab را دارند، از فایل‌های /etc/cron.allow و /etc/cron.deny استفاده کنید. اگر cron.allow وجود داشته باشد فقط کاربران داخل آن مجازند.

نمایش تمام کرون‌جاب‌های سرور

crontab -l فقط زمان‌بندی‌های یک کاربر را نشان می‌دهد. برای گرفتن فهرست کرون‌جاب‌های همه‌ی کاربران سرور می‌توانید روی کاربران /etc/passwd حلقه بزنید:

for u in $(cut -f1 -d: /etc/passwd); do echo "=== $u"; crontab -u "$u" -l 2>/dev/null; done

همین فایل‌ها را می‌توانید مستقیماً هم ببینید؛ روی AlmaLinux، Rocky و CentOS در /var/spool/cron/ و روی دبیان و اوبونتو در /var/spool/cron/crontabs/ ذخیره می‌شوند. این فایل‌ها را دستی ویرایش نکنید و همیشه از crontab -e استفاده کنید، چون این دستور پیش از ذخیره نحو خط‌ها را بررسی می‌کند.

بعد سراغ زمان‌بندی‌های سیستمی بروید. محتوای فایل اصلی:

less /etc/crontab

و فایل‌هایی که بسته‌های نصب‌شده در پوشه‌ی اختصاصی خودشان می‌گذارند:

ls -la /etc/cron.d/

سپس اسکریپت‌های دوره‌ای. برای دیدن همه‌ی کرون‌جاب‌های روزانه:

ls -la /etc/cron.daily/

و برای خواندن محتوای یکی از آن‌ها:

less /etc/cron.daily/filename

سه پوشه‌ی دیگر هم دقیقاً به همین شکل بررسی می‌شوند:

ls -la /etc/cron.hourly/
ls -la /etc/cron.weekly/
ls -la /etc/cron.monthly/

در نسخه‌های قدیمی‌تر معمولاً 0anacron در cron.hourly و logrotate در cron.daily دیده می‌شد. روی توزیع‌های جدید لزوماً همین‌طور نیست؛ مثلاً چرخش لاگ‌ها در بسیاری از نسخه‌های امروزی به‌جای /etc/cron.daily/logrotate با یک تایمر systemd انجام می‌شود. پس فهرست پوشه را بخوانید و روی نام‌های قدیمی حساب نکنید.

و بخش آخر که در راهنماهای قدیمی وجود ندارد — تایمرهای systemd:

systemctl list-timers --all

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

تفاوت /etc/crontab و /etc/cron.d با پوشه‌های cron.daily و مانند آن

این تفاوت جایی است که خیلی‌ها گیر می‌کنند و کرون‌جابشان بی‌صدا اجرا نمی‌شود.

فایل‌های /etc/crontab و /etc/cron.d — شش فیلدی

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

SHELL=/bin/bash
PATH=/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=root
HOME=/

# m h dom mon dow  user  command
30 2 * * *         root  /usr/local/bin/backup.sh

اگر همین خط را بدون کلمه‌ی root بنویسید، کرون /usr/local/bin/backup.sh را نام کاربر فرض می‌کند و جاب هرگز اجرا نمی‌شود. برعکس، اگر در crontab -e یک نام کاربر اضافه کنید، آن نام جزئی از دستور در نظر گرفته می‌شود و دستور شکست می‌خورد. یعنی:

  • crontab کاربر (با crontab -e): پنج فیلد + دستور
  • /etc/crontab و /etc/cron.d: پنج فیلد + نام کاربر + دستور

برای جاب‌های سفارشی خودتان بهتر است به‌جای دست زدن به /etc/crontab، یک فایل جدا در /etc/cron.d/ بسازید؛ این‌طور به‌روزرسانی بسته‌ها فایل شما را بازنویسی نمی‌کند. دو شرط دارد: فایل باید مالکش root و پرمیژنش 644 باشد، و نامش نباید نقطه داشته باشد. فایلی به نام mybackup.cron در بسیاری از توزیع‌ها به‌کلی نادیده گرفته می‌شود؛ همان فایل با نام mybackup کار می‌کند.

پوشه‌های cron.hourly، cron.daily، cron.weekly و cron.monthly

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

nano /etc/cron.daily/cleanup-tmp
chmod +x /etc/cron.daily/cleanup-tmp

سه شرطی که معمولاً فراموش می‌شود و باعث می‌شود اسکریپت هرگز اجرا نشود:

  • فایل باید بیت اجرا داشته باشد (chmod +x).
  • نام فایل نباید نقطه داشته باشد. cleanup-tmp.sh در بسیاری از توزیع‌ها اجرا نمی‌شود؛ cleanup-tmp اجرا می‌شود.
  • خط اول اسکریپت باید shebang داشته باشد، مثلاً #!/bin/bash.

نکته‌ی دیگر اینکه روی بیشتر سرورها اجرای این پوشه‌ها به anacron سپرده شده است. anacron تضمین می‌کند که اگر سرور در ساعت مقرر خاموش یا مشغول بوده، جاب بعداً جبران شود — اما در عوض ساعت اجرا دقیق نیست و ممکن است با تأخیر انجام شود. برای کاری که باید حتماً رأس ساعت مشخصی اجرا شود، از crontab استفاده کنید نه از این پوشه‌ها.

خروجی، ایمیل و PATH — سه چیزی که بیشتر کرون‌جاب‌ها را خراب می‌کند

هدایت خروجی

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

0 3 * * * /usr/local/bin/myscript.sh >/dev/null 2>&1

ترتیب مهم است. >/dev/null 2>&1 یعنی «خروجی را دور بریز، بعد خطاها را هم به همان‌جا بفرست». اگر برعکس بنویسید (2>&1 >/dev/null) خطاها همچنان روی ترمینال/ایمیل می‌روند.

در عمل دور ریختن کامل خروجی معمولاً بدترین کار است، چون وقتی جاب خراب شود هیچ ردی از خود باقی نمی‌گذارد. بهتر است به‌جای /dev/null در یک فایل لاگ بنویسید:

0 3 * * * /usr/local/bin/myscript.sh >> /var/log/myscript.log 2>&1

متغیر MAILTO

اگر می‌خواهید خروجی جاب‌ها به‌جای کاربر محلی به یک آدرس مشخص برود، در بالای crontab بنویسید:

MAILTO=admin@example.com

و اگر اصلاً ایمیلی نمی‌خواهید:

MAILTO=""

توجه کنید که ارسال ایمیل به یک MTA فعال روی سرور (مثل Postfix) وابسته است؛ اگر سرویس ایمیل روی سرور راه‌اندازی نشده باشد، خروجی جاب عملاً جایی نمی‌رود.

متغیر PATH

شایع‌ترین دلیل «کرون‌جاب من در ترمینال کار می‌کند ولی در کرون نه» همین است. کرون محیط شل تعاملی شما را بارگذاری نمی‌کند؛ نه .bashrc خوانده می‌شود و نه .bash_profile. متغیر PATH داخل کرون بسیار کوتاه‌تر از چیزی است که در SSH می‌بینید. دو راه‌حل دارید. یا مسیر کامل هر دستور را بنویسید:

command -v php

خروجی این دستور مسیر کامل مفسر PHP روی همان سرور است؛ همان را عیناً در خط کرون بنویسید. راه دوم این است که در بالای crontab خودتان PATH را تعریف کنید:

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

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

0 2 * * * tar -czf /backup/site-$(date +%Y%m%d).tar.gz /home/user/public_html

مثال ۱: ریستارت زمان‌بندی‌شده وب‌سرور

فرض کنید آپاچی روی سرور بعد از چند ساعت کند می‌شود و ریستارت موقتاً حلش می‌کند. برای زمان‌بندی این کار با root وارد SSH شوید، crontab -e بزنید و این خط را اضافه کنید:

0 15 * * * /usr/bin/systemctl restart httpd >/dev/null 2>&1

یعنی هر روز رأس ساعت ۱۵:۰۰ سرویس httpd ریستارت شود و خروجی و خطاها دور ریخته شوند. فایل را ذخیره کنید و خارج شوید؛ کرون خودش خط جدید را برمی‌دارد.

چند نکته پیش از اجرا:

  • روی CentOS 7 به بعد و همچنین AlmaLinux و Rocky، مسیر /etc/init.d/httpd دیگر استاندارد نیست و دستور service httpd restart هم فقط یک لایه‌ی سازگاری است که به systemd پاس داده می‌شود. شکل درست امروزی systemctl restart httpd است.
  • اگر آپاچی شما روی اوبونتو یا دبیان است، نام سرویس httpd نیست بلکه apache2 است: /usr/bin/systemctl restart apache2.
  • اگر هدفتان صرفاً اعمال تغییرات کانفیگ است، systemctl reload httpd بهتر از restart است چون اتصال‌های در حال سرویس‌دهی را قطع نمی‌کند.
  • پیش از گذاشتن در کرون، همان دستور را یک‌بار دستی اجرا کنید تا مطمئن شوید کار می‌کند.

و مهم‌ترین نکته: ریستارت شبانه‌ی httpd یک راه‌حل نیست، پاک کردن صورت‌مسئله است. آپاچی‌ای که هر روز نیاز به ریستارت دارد یک مشکل واقعی دارد — معمولاً نشتی حافظه در یکی از ماژول‌ها یا اسکریپت‌های PHP، یا تنظیم نامناسب تعداد پروسه‌های همزمان (مثلاً MaxRequestWorkers) نسبت به رم سرور. ریستارت زمان‌بندی‌شده فقط علائم را پنهان می‌کند و در ضمن هر روز یک قطعی کوتاه به سایت‌های شما تحمیل می‌کند. آن را به‌عنوان یک مُسکن موقت بگذارید و در همان فاصله لاگ خطای آپاچی و مصرف حافظه را بررسی کنید تا علت اصلی را پیدا کنید. وقتی علت رفع شد، خط کرون را پاک کنید.

مثال ۲: قفل کردن خودکار پرمیژن فایل‌های حساس

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

find /home/*/public_html/configuration.php -print -exec chmod 400 {} ;
find /home/*/public_html/config.php -print -exec chmod 400 {} ;
find /home/*/public_html/wp-config.php -print -exec chmod 400 {} ;
find /home/*/public_html/*/configuration.php -print -exec chmod 400 {} ;
find /home/*/public_html/*/config.php -print -exec chmod 400 {} ;
find /home/*/public_html/*/wp-config.php -print -exec chmod 400 {} ;
find /home -type d -perm 777 -print -exec chmod 755 {} ;

سه فایل اول به‌ترتیب کانفیگ جوملا، کانفیگ عمومی بسیاری از اسکریپت‌ها و کانفیگ وردپرس هستند؛ سه خط بعدی همان‌ها را یک سطح عمیق‌تر (سایت‌های نصب‌شده در زیرپوشه) پیدا می‌کنند. خط آخر هر دایرکتوری‌ای را که دقیقاً روی 777 است به 755 برمی‌گرداند. سوئیچ -print باعث می‌شود مسیر هر فایلی که تغییر کرده در خروجی بیاید که برای لاگ‌گیری مفید است.

همین کار را می‌توانید فشرده‌تر و سریع‌تر هم بنویسید. علامت + به‌جای ; باعث می‌شود به‌جای اجرای یک chmod برای هر فایل، همه‌ی مسیرها یکجا به chmod داده شوند:

find /home -maxdepth 4 -type f -path '*/public_html/*' ( -name wp-config.php -o -name config.php -o -name configuration.php ) -exec chmod 400 {} +

شرط -path '*/public_html/*' را حذف نکنید. بدون آن، دستور هر فایلی به این نام‌ها را تا عمق چهار زیر /home پیدا می‌کند — از جمله چیزهایی مثل /home/user/backup/config.php که در فهرست هفت‌خطی بالا اصلاً هدف نبودند و ممکن است بستنشان چیزی را خراب کند.

برای زمان‌بندی، این دستورها را در یک اسکریپت بگذارید و اسکریپت را در کرون صدا بزنید — نه اینکه هفت خط را جداگانه در crontab بنویسید:

30 4 * * * /usr/local/bin/fix-permissions.sh >> /var/log/fix-permissions.log 2>&1

چند هشدار که قبل از اجرا باید بخوانید:

  • در بسیاری از نمونه‌هایی که در وب می‌بینید این خط‌ها با {} ; نوشته شده‌اند که کار نمی‌کند. سمی‌کالن باید از دید شل پنهان بماند، یعنی {} ; یا {} ';'. اگر بک‌اسلش را جا بیندازید، find با خطای «missing argument to -exec» متوقف می‌شود.
  • اگر روی یک سرور هیچ کاربری آن فایل را نداشته باشد، الگوی /home/*/public_html/config.php بازنشده باقی می‌ماند و find خطای «No such file or directory» می‌دهد و با کد خروجی ۱ تمام می‌شود. این خطا بی‌ضرر است ولی لاگ را شلوغ می‌کند؛ اگر آزارتان داد آخر همان خط 2>/dev/null بگذارید.
  • پرمیژن 400 یعنی فقط مالک فایل می‌تواند آن را بخواند. این روی سرورهایی درست کار می‌کند که PHP با کاربر خود سایت اجرا می‌شود (PHP-FPM با pool جداگانه برای هر کاربر یا suEXEC — حالت رایج روی cPanel و DirectAdmin). اگر روی سرور شما PHP به‌صورت mod_php و با کاربر وب‌سرور اجرا می‌شود، وب‌سرور دیگر نمی‌تواند wp-config.php را بخواند و سایت با خطا بالا می‌آید. در آن حالت باید از گروه مشترک و پرمیژن 440 استفاده کنید.
  • خط آخر روی کل /home راه می‌رود. اول آن را بدون بخش -exec اجرا کنید تا فقط ببینید چه دایرکتوری‌هایی قرار است تغییر کنند: find /home -type d -perm 777 -print. بعضی برنامه‌ها روی سرورهایی که PHP با کاربر وب‌سرور اجرا می‌شود، به پوشه‌ی نوشتنی برای آپلود نیاز دارند و بستن نابه‌جای آن‌ها آپلود فایل را از کار می‌اندازد.

اگر با مفهوم اعداد پرمیژن و تفاوت 400، 644، 755 و 777 راحت نیستید، پیش از اجرای این جاب مقاله‌ی پرمیژن چیست و چگونه تنظیم می‌شود؟ را بخوانید.

بررسی اینکه کرون‌جاب واقعاً اجرا شده است

اول مطمئن شوید خود سرویس کرون بالا و فعال است:

systemctl status crond

روی دبیان و اوبونتو نام سرویس cron است. اگر اصلاً نصب نبود:

# AlmaLinux / Rocky / CentOS 8 و بالاتر
dnf install cronie
systemctl enable --now crond

# CentOS 7 (منسوخ؛ دستور yum فقط روی همین نسخه‌های قدیمی)
yum install cronie

# دبیان / اوبونتو
apt install cron
systemctl enable --now cron

بعد لاگ اجرای جاب‌ها را ببینید:

journalctl -u crond --since today

روی سرورهای قدیمی‌تر همین اطلاعات در /var/log/cron (خانواده‌ی ردهت) یا /var/log/syslog (دبیان و اوبونتو) هم هست:

grep CRON /var/log/syslog

اگر در لاگ می‌بینید که کرون جاب را اجرا کرده ولی نتیجه‌ای نگرفته‌اید، مشکل تقریباً همیشه یکی از این سه چیز است: مسیر کامل دستور نوشته نشده (بحث PATH)، اسکریپت بیت اجرا ندارد، یا کاربری که جاب با آن اجرا می‌شود به فایل مقصد دسترسی ندارد. لاگ را به فایل بفرستید تا خطای واقعی را ببینید.

اگر سرورتان کنترل‌پنل cPanel یا DirectAdmin دارد، بخش Cron Jobs همان crontab کاربر را ویرایش می‌کند؛ یعنی هر چیزی که آنجا بسازید با crontab -u username -l از SSH هم دیده می‌شود و برعکس. لازم نیست نگران باشید که دو زمان‌بندی موازی درست کرده‌اید.