SSD Nodes Learn Hosting plans →
Hướng dẫn Matt ConnorBởi Matt Connor · Cập nhật ngày 2026-09-04

5 lý do cron job không chạy và cách kiểm tra

Cron job không chạy thường do PATH tối giản, dấu % chưa escape, sai crontab, output bị gửi qua mail hoặc script cần login shell. Kiểm tra theo 5 bước.

Vì sao cron job của bạn không chạy

Một cron job “không bao giờ chạy” gần như chắc chắn đã từng chạy. Nó chạy trong một environment không phải shell của bạn, fail ngay trong giây đầu tiên, và message được gửi đến nơi bạn chưa kiểm tra. Năm nguyên nhân giải thích gần như mọi trường hợp: search path, dấu phần trăm, file crontab không đúng, output được gửi qua mail và script yêu cầu login session.

cron là một daemon (background service) đọc các file crontab và khởi chạy command theo lịch. Nó không đọc .bashrc, không mở terminal, không khởi động login shell và không cho bạn biết khi command fail. Mọi nguyên nhân bên dưới đều bắt nguồn từ 4 thực tế này.

Hãy kiểm tra lần lượt theo thứ tự và bắt đầu bằng câu hỏi nền tảng sau: cron có thực sự kích hoạt job không? “cron chưa bao giờ khởi chạy job” và “job đã khởi chạy rồi bị dừng” là 2 vấn đề khác nhau, không liên quan với nhau. Vì vậy, hãy trả lời câu hỏi này trước.

Cron có thực sự chạy không?

Daemon có tên unit khác nhau trên từng họ distribution. Hãy kiểm tra cả hai tên, sau đó đọc log.

systemctl status cron
systemctl status crond
journalctl -u cron --since "2 hours ago"
journalctl -u crond --since "2 hours ago"

Debian và Ubuntu gọi unit này là cron. Fedora, Rocky và Alma gọi là crond. Trên mỗi máy chỉ tồn tại một trong hai tên đó, vì vậy một trong hai lệnh báo unknown unit là bình thường và không phải lỗi.

Hãy đọc các entry do chính hệ thống của bạn ghi lại. Đừng tìm một dòng được chép nguyên từ tài liệu hướng dẫn, vì cách ghi khác nhau giữa các triển khai cron và các cấu hình logging. Bạn chỉ cần kiểm tra 2 việc: có entry tại phút mà schedule của bạn chỉ định hay không, và entry đó có ghi đúng command của bạn hay không. Entry ghi đúng command nghĩa là cron đã hoàn tất phần việc của nó, còn lỗi nằm bên trong command. Không có entry nào nghĩa là cron chưa từng nhận schedule của bạn. Đây là nguyên nhân 3 bên dưới.

Một số image gửi message của cron qua rsyslog vào file thay vì journal. Hãy tìm trong /var/log một file có tên liên quan đến cron hoặc syslog, sau đó đọc phần cuối file.

ls -l /var/log
sudo tail -n 50 /var/log/syslog

Nếu không tồn tại cả unit lẫn log, có thể cron chưa được cài đặt. Các cloud image tối giản và container thường không có cron.

dpkg -l cron
rpm -q cronie
sudo apt install cron
sudo dnf install cronie
sudo systemctl enable --now cron

Nguyên nhân 1: cron không có PATH của bạn

Shell tương tác của bạn tạo PATH từ /etc/profile, ~/.profile, ~/.bashrc và mọi tệp mà các tệp đó source. Không phần nào trong số này chạy cho cron job. cron khởi động command với environment ngắn gọn riêng, nên không tìm thấy program nằm ngoài các thư mục hệ thống tiêu chuẩn. Mọi thứ nằm dưới /usr/local/bin, /opt, một language version manager, Python virtual environment hoặc Go workspace đều có thể gây ra lỗi này. Job fail ngay ở dòng đầu tiên. Shell ghi ra lỗi dạng "not found"; câu chữ chính xác phụ thuộc vào shell đã chạy command.

Tìm đường dẫn thực của mọi command mà job sử dụng.

command -v docker
command -v node
readlink -f "$(command -v node)"

Sau đó, ghi các đường dẫn tuyệt đối đó vào job hoặc đặt PATH một lần ở đầu crontab.

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
0 3 * * * /usr/local/bin/mytool run

Lấy danh sách đó trên chính máy của bạn bằng echo "$PATH", rồi loại bỏ mọi mục chỉ tồn tại trong phiên tương tác. Có một quy tắc quan trọng: cron không expand biến trong các dòng assignment này. PATH=$PATH:/usr/local/bin lưu nguyên văn text $PATH:/usr/local/bin, nên job kết thúc với một search path không chứa thư mục nào có thể sử dụng. Hãy ghi đầy đủ toàn bộ danh sách.

Version manager cần nhiều hơn một path. nvm, pyenv, rbenv và asdf cài một shell function hoặc một shims directory từ .bashrc của bạn, nhưng cron không bao giờ đọc tệp đó. Hãy gọi binary theo version bằng absolute path hoặc source init script của manager ở dòng đầu tiên trong script của bạn.

Nguyên nhân 2: dấu phần trăm kết thúc command của bạn

Trong trường command của crontab, % không phải là ký tự thông thường. % đầu tiên không được escape sẽ kết thúc command. Mọi nội dung sau đó được truyền cho command dưới dạng standard input, và mỗi % tiếp theo trở thành một dòng mới. Đây là tính năng có thật của cron để truyền input ngắn cho một chương trình. Đây cũng là lý do filename có ngày tháng là dạng crontab thường bị lỗi.

Ghi 0 3 * * * /usr/bin/tar -czf /srv/backups/site-$(date +%F).tar.gz /srv/site thì tar sẽ không bao giờ nhận được ngày đã format. cron cắt dòng tại % đầu tiên, nên shell nhận một command substitution chưa hoàn chỉnh, còn phần còn lại của dòng được truyền dưới dạng standard input. Hãy escape mọi dấu phần trăm bằng dấu gạch chéo ngược.

0 3 * * * /usr/bin/tar -czf /srv/backups/site-$(date +\%F).tar.gz /srv/site

Có 2 lớp đọc dòng đó, theo đúng thứ tự. \% là một cron rule, được cron áp dụng trước khi khởi chạy bất cứ thứ gì. $(date +\%F) là command substitution, được shell mà cron khởi chạy áp dụng sau đó. Mấu chốt là biết ký tự nào thuộc lớp nào.

Cách an toàn hơn là không đặt logic trực tiếp trong crontab. Hãy đặt logic trong một script, nơi dấu phần trăm không có ý nghĩa đặc biệt.

#!/bin/bash
set -euo pipefail
stamp="$(date +%F)"
tar -czf "/srv/backups/site-${stamp}.tar.gz" /srv/site

Khi đó, dòng crontab chỉ chứa một path và một redirect, không có gì khác. Crontab có thể đọc nhanh để hiểu ngay cũng là crontab dễ debug.

Nguyên nhân 3: bạn đã sửa crontab nào?

Không chỉ có một crontab. Có nhiều file, thuộc các owner khác nhau và có số lượng field khác nhau. Nếu ghi job vào nhầm file, job đó sẽ không được nhìn thấy.

  • crontab -e sửa crontab của user chạy command. sudo crontab -e sửa crontab của root. Hai người cùng debug một máy thường lại đọc hai file khác nhau.
  • sudo crontab -l -u deploy liệt kê crontab của user khác. Đây là cách xác nhận account cần chạy job thực sự đã cài những gì.
  • /etc/crontab và mọi file trong /etc/cron.d có thêm một field giữa lịch chạy và command: user mà job sẽ chạy dưới quyền đó. Nếu dán một dòng crontab của user có 5 field vào /etc/cron.d, từ đầu tiên của command sẽ bị đọc thành username.
  • File trong /etc/cron.d phải có tên chỉ gồm chữ cái, chữ số, dấu gạch dưới và dấu gạch ngang. File tên backup.sh hoặc site.conf sẽ bị bỏ qua chỉ vì tên không hợp lệ. Đổi tên file thành backup rồi kiểm tra log lại.
  • File trong /etc/cron.d phải thuộc sở hữu của root và không được cho group hoặc user khác quyền ghi. ls -l /etc/cron.d hiển thị cả hai thông tin này cùng lúc.
  • Script đặt vào /etc/cron.daily và các thư mục tương tự phải tuân theo cùng quy tắc đặt tên, đồng thời phải có execute bit. Thiếu execute bit sẽ khiến script bị bỏ qua mà không có thông báo rõ ràng.
  • /etc/cron.allow và /etc/cron.deny quyết định user nào được phép cài crontab. Nếu một trong hai file này tồn tại trên máy, hãy đọc nó trước khi giả định user của bạn được phép cài crontab.

Cài user crontab bằng command crontab thay vì tự sửa spool file, vì crontab sẽ parse file trước khi cài. Sau khi lưu, hãy đọc nội dung command in ra. Nếu command từ chối file, phiên bản trước vẫn tiếp tục hoạt động và thay đổi của bạn không có hiệu lực. Hiện tượng này trông giống hệt cron phớt lờ bạn.

Owner cũng quyết định permission. Job trong crontab của root tạo ra các file thuộc sở hữu của root, nên application đọc các file đó có thể không ghi được vào chúng. Job trong crontab của user thông thường không thể đọc thư mục chỉ root mới có quyền truy cập. Hãy chọn owner phù hợp với công việc: tác vụ maintenance của application nên chạy dưới account riêng của application. Đây là lý do của việc thay wp-cron của WordPress bằng system cron job. Mode của các file do job tạo ra lấy từ umask mà job kế thừa. Giá trị này không phải umask của shell của bạn, vì vậy cách umask thiết lập file permission rất đáng đọc nếu output của job bị tạo ở trạng thái không thể đọc.

Nguyên nhân 4: output được gửi vào hộp thư không ai đọc

cron thu thập mọi thứ mà một job ghi vào standard output và standard error. Nếu job ghi ra bất kỳ nội dung nào, cron chuyển nội dung đó cho hệ thống mail cục bộ, gửi đến chủ sở hữu của crontab hoặc đến địa chỉ mà MAILTO chỉ định. Trên một VPS tối giản thường không cài MTA (mail transfer agent), nên không có gì chuyển tiếp được thư. Lỗi của bạn chỉ tồn tại trong chốc lát rồi biến mất. Đó là toàn bộ lý do một job bị lỗi nhưng trông như không có gì xảy ra.

Hãy ghi output vào một file mà bạn kiểm soát.

0 3 * * * /usr/local/sbin/backup-site.sh >> /var/log/backup-site.log 2>&1

>> nối standard output vào file. 2>&1 trỏ standard error đến nơi mà standard output đang trỏ tới, nên phải đặt sau redirect. Nếu viết ngược lại, như 2>&1 >> file, standard error vẫn giữ đích ban đầu, và lỗi bạn đang tìm chính là phần không bao giờ được ghi vào file.

journal là một đích phù hợp khác. logger ghi vào syslog với một tag do bạn chọn.

0 3 * * * /usr/local/sbin/backup-site.sh 2>&1 | logger -t backup-site

Đọc lại bằng journalctl -t backup-site. Cách này giữ output của job ngay cạnh các entry cron, nên dễ theo dõi timeline. Nếu bạn cũng cần ghi lại người nào đã chạy lệnh nào trên máy chủ, đó là một hệ thống riêng; kiểm tra lệnh của người dùng trên máy chủ sẽ trình bày phần này.

MAILTO="" ở đầu crontab sẽ tắt mail cho các job bên dưới nó. Đặt MAILTO thành một địa chỉ thực chỉ có tác dụng nếu có MTA hoạt động, vì vậy hãy kiểm tra rằng mail thực sự rời khỏi máy chủ trước khi phụ thuộc vào cách này.

Một quy tắc khi debug: không bao giờ thêm > /dev/null 2>&1. Đây là dòng phổ biến nhất trong mọi crontab và nó loại bỏ bằng chứng duy nhất bạn có. Sau này, khi job đã hoạt động, bạn có thể thêm lại nếu muốn.

Nguyên nhân 5: script giả định môi trường mà cron không cung cấp

Khi đã tìm thấy command và thu được output, phần còn lại là mọi thứ mà session của bạn tự động cung cấp.

  • Shell có thể không phải là bash. Kiểm tra bằng ls -l /bin/sh. Trên Debian và Ubuntu, nó trỏ đến dash, nên phép kiểm tra bằng dấu ngoặc vuông kép, array và source sẽ lỗi cú pháp. Thêm dòng #!/bin/bash vào script rồi gọi script, hoặc đặt SHELL ở đầu crontab.
  • Working directory không phải thư mục bạn đang đứng. Dùng absolute path ở mọi nơi, hoặc dùng cd để chuyển đến directory trong dòng đầu tiên của script. Relative path là lý do phổ biến nhất khiến một job “chạy được khi tôi chạy thủ công”.
  • Locale không giống session của bạn. Bất kỳ command nào format ngày tháng hoặc số, hoặc sort text, đều có thể tạo output khác dưới một LANG khác. Nếu bước sau parse output đó, hãy set locale trong script thay vì phỏng đoán.
  • Không có TTY (terminal). Một command yêu cầu xác nhận, mở editor hoặc vẽ progress bar có thể bị treo hoặc thoát. Thêm flag non-interactive mà tool cung cấp.
  • Không có SSH agent. SSH_AUTH_SOCK không có trong environment của cron, nên command ssh hoặc rsync từng chạy được vì agent của bạn đã được load sẽ không thể authenticate. Cấp cho job một key riêng và để user chạy job sở hữu key đó.
  • Không có user session bus, nên systemctl --user từ cron job sẽ fail cho đến khi XDG_RUNTIME_DIR được set. System unit là lựa chọn phù hợp hơn.

Trên Fedora, Rocky và Alma còn có một khả năng khác. SELinux giới hạn cron job, nên job sẽ bị từ chối khi truy cập path có label không đúng dự kiến, ngay cả khi file permission trông có vẻ chính xác. Kiểm tra các denial bằng sudo ausearch -m avc -ts recent, rồi đọc kiến thức cơ bản về SELinux cho server trước khi tắt bất kỳ cơ chế nào.

Probe một phút cho bạn thấy môi trường của cron

Đừng đoán môi trường của cron chứa gì. Hãy đọc trực tiếp. Tạo một script ghi lại mọi thứ, lên lịch chạy mỗi phút, chờ rồi đọc file.

cat > /home/deploy/cron-probe.sh <<'EOF'
#!/bin/bash
echo "=== probe ==="
date -Is
pwd
id
echo "SHELL=$SHELL"
echo "LANG=$LANG"
command -v node || echo "node is not on this PATH"
env | sort
EOF
chmod +x /home/deploy/cron-probe.sh

Thêm một dòng vào crontab của user chạy job thực tế, với đường dẫn tuyệt đối ở cả hai phía.

* * * * * /home/deploy/cron-probe.sh >> /home/deploy/cron-probe.log 2>&1

Chờ một phút rồi đọc /home/deploy/cron-probe.log và so sánh với kết quả của cùng các lệnh khi chạy trong shell của bạn. Dòng PATH, thư mục làm việc và locale thường tự giải thích nguyên nhân lỗi. Lưu ý 2 chi tiết trong thiết lập này: các dấu phần trăm nằm bên trong script, nơi quy tắc của cron không áp dụng, và đường dẫn log là đường dẫn mà user chạy job có quyền ghi.

Xóa dòng crontab đó ngay sau khi có câu trả lời. Một job chạy mỗi phút và append vào file sẽ nhanh chóng làm đầy disk nhỏ, và nó sẽ làm việc đó một cách âm thầm.

Lịch này có đúng như bạn muốn không?

Một dòng crontab của user bắt đầu bằng 5 trường: phút, giờ, ngày trong tháng, tháng, ngày trong tuần. Hai trường trong số đó có cách kết hợp dễ gây nhầm lẫn.

Khi cả ngày trong tháng và ngày trong tuần đều bị giới hạn, nghĩa là cả hai đều không phải *, cron chạy job khi một trong hai trường khớp. 0 0 13 * 5 không có nghĩa là “thứ Sáu ngày 13”. Nó chạy lúc nửa đêm vào ngày 13 của mỗi tháng và lúc nửa đêm vào mỗi thứ Sáu. Để chạy vào một ngày cụ thể, hãy để một trong hai trường là * và kiểm tra trường còn lại bên trong script.

cron sử dụng timezone của hệ thống. Nhiều image VPS được cấu hình sẵn theo UTC (giờ quốc tế phối hợp), nên một job bạn đặt chạy lúc 03:00 sẽ chạy lúc 03:00 UTC, có thể là giữa buổi chiều của bạn. timedatectl in ra timezone mà máy của bạn thực sự đang dùng. Hãy kiểm tra giá trị này thay vì mặc định cho rằng nó giống laptop của bạn.

Có thêm 2 lỗi dễ gặp về lịch cần biết. @reboot chạy khi chính cron khởi động. Thời điểm này không đồng nghĩa với lúc network đã sẵn sàng, nên một job cần DNS hoặc host từ xa có thể fail khi boot nhưng lại thành công trong mọi lần chạy thủ công sau đó. Ngoài ra, không có gì ngăn một job chạy lâu khởi động lại khi bản trước vẫn đang chạy. Hãy bọc job bằng lock.

*/5 * * * * /usr/bin/flock -n /tmp/backup-site.lock /usr/local/sbin/backup-site.sh >> /var/log/backup-site.log 2>&1

flock -n thoát ngay khi lock đã được giữ, nên lần chạy trùng sẽ dừng thay vì tiếp tục chồng lên lần chạy đầu tiên.

Khi systemd timer là công cụ phù hợp hơn

cron chỉ làm tốt một việc: chạy lệnh này vào thời điểm này. Ở các khía cạnh khác, cron khá hạn chế. Timer cung cấp journal mà không cần redirect, cho phép truy vấn exit status sau đó, hỗ trợ sắp xếp thứ tự với network-online.target và thêm độ trễ ngẫu nhiên để một trăm server không cùng khởi chạy trong một giây. Khi job cần những tính năng này, một systemd service và timer trên VPS sẽ ít tốn công hơn so với việc phải bảo vệ một dòng crontab. Phần service buộc bạn trả lời một câu hỏi mà cron không đặt ra: unit xác định công việc đã thực sự bắt đầu bằng cách nào. Vì vậy, trước tiên hãy đọc ý nghĩa của Type= đối với unit simple, forking và notify, vì một script tự daemonize với type mặc định sẽ khiến unit hiển thị là active dù không còn tiến trình nào chạy phía sau. Cơ chế retry cũng thuộc về phần này, vì các chính sách restart của systemd quyết định điều gì xảy ra sau một lần fail, còn cron hoàn toàn không có cơ chế xử lý câu hỏi đó.

Hãy tiếp tục dùng cron cho các job nhỏ. Chuyển mọi job có dependency hoặc retry policy sang timer. Cả hai có thể chạy trên cùng một server, nên bạn không cần hoàn tất việc chuyển đổi này trong một lần.

FAQ

Vì sao cron job chạy thủ công nhưng lại lỗi khi chạy từ cron?

Vì môi trường của shell và cron khác nhau. Login shell đọc /etc/profile và ~/.bashrc, từ đó thiết lập PATH, locale và các biến của agent. cron khởi chạy lệnh mà không có những thiết lập đó, từ một working directory khác và đôi khi bằng một shell khác. Hãy dùng đường dẫn tuyệt đối cho mọi lệnh, thiết lập các giá trị cần thiết ở đầu crontab hoặc bên trong script, rồi tạo một probe job chạy sau một phút. Job này chạy env | sort, pwd và id rồi ghi kết quả vào file log để bạn đọc môi trường thực tế của cron thay vì đoán.

Làm thế nào để kiểm tra cron có thực sự chạy job của tôi không?

Đọc log của daemon. Dùng journalctl -u cron trên Debian và Ubuntu, hoặc journalctl -u crond trên Fedora, Rocky và Alma. Một số image chuyển các message qua rsyslog vào file nằm dưới /var/log. Tìm entry tại phút mà schedule của bạn chỉ định và kiểm tra entry đó có ghi đúng command hay không. Không có entry nghĩa là cron chưa nhận được schedule, vì vậy hãy xác nhận bạn đã sửa đúng crontab. Có entry nhưng không có kết quả nghĩa là command đã khởi chạy rồi thoát, vì vậy hãy redirect output của nó.

Vì sao date +%Y bị lỗi bên trong crontab?

cron xử lý % là ký tự đặc biệt trong command field. % đầu tiên không được escape sẽ kết thúc command. Mọi nội dung sau đó được truyền cho command dưới dạng standard input, còn mỗi % tiếp theo sẽ trở thành một dòng mới. Vì vậy, filename có định dạng ngày không đến được program mà bạn viết để nhận nó. Hãy escape từng ký tự percent thành \%, hoặc chuyển command vào một script rồi gọi script đó từ cron. Bên trong script, ký tự percent không có ý nghĩa đặc biệt.

Output của cron job đi đâu?

Output được gửi đến hệ thống mail cục bộ, với địa chỉ người sở hữu crontab hoặc địa chỉ mà MAILTO chỉ định. Phần lớn VPS image không cài mail transfer agent, vì vậy message bị loại bỏ và job có vẻ như không tạo output. Hãy redirect output vào file bằng >> /path/to/log 2>&1. Giữ đúng thứ tự này để standard error đi theo standard output. Hoặc pipe output qua logger -t myjob rồi đọc lại bằng journalctl -t myjob. Không dùng > /dev/null 2>&1 trong khi bạn vẫn đang debug.

Nên dùng cron hay systemd timer?

Dùng cron cho command đơn giản chạy vào thời điểm cố định, đặc biệt nếu bạn có thể cần chuyển command đó sang máy không chạy systemd. Dùng timer khi bạn muốn output nằm trong journal mà không cần redirect, muốn truy vấn exit status, muốn job chạy sau khi network sẵn sàng, muốn thêm randomized start delay hoặc muốn có retry policy sau khi job fail. Cả hai đều có thể chạy trên cùng một server, nên bạn có thể chuyển từng job một khi có lý do phù hợp.