Cron job chạy local OK, lên server không chạy — 6 lý do tôi đã gặp 100 lần
PATH thiếu, $HOME khác, log câm như tờ, time zone server lệch. Đây là checklist 6 bước tôi luôn chạy khi cron job 'chạy mà không chạy'.
Tình huống: bạn viết một script Bash, chạy thử ở terminal — ngon. Đưa vào crontab — câm. Không output, không log, không error. Chỉ là… không chạy.
Hoặc tệ hơn: cron có chạy, nhưng output sai, hoặc thiếu, hoặc một nửa thành công nửa fail mà không ai biết.
Tôi đã debug kịch bản này ít nhất 100 lần trong 8 năm freelance/ops. Đây là checklist tôi luôn đi qua — theo thứ tự, từ phổ biến nhất đến hiếm gặp.
Lý do #1: PATH trong cron rỗng tuếch
Đây là cái bẫy số 1, dính 90% người mới.
Khi bạn login bằng SSH, shell load ~/.bashrc, ~/.profile, /etc/profile → PATH của bạn đầy đủ: /usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:.... Lệnh node, python3, composer, aws, docker đều gọi được trực tiếp.
Cron không load những file đó. Mặc định PATH trong cron chỉ là:
/usr/bin:/bin
Đó là lý do node script.js chạy ngon ở terminal nhưng cron báo “command not found” — node của bạn nằm ở /usr/local/bin/node hoặc /opt/homebrew/bin/node.
Cách fix #1: dùng full path trong crontab:
* * * * * /usr/local/bin/node /home/deploy/app/script.js
Cách fix #2 (sạch hơn): khai PATH ngay đầu crontab:
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
* * * * * node /home/deploy/app/script.js
Kiểm tra which node ở terminal đang user nào, copy đường dẫn đó vào.
Lý do #2: $HOME không phải nơi bạn nghĩ
Cron chạy với user owner của crontab đó. Nếu bạn crontab -e khi đang là root, cron chạy với $HOME=/root. Script bạn dùng ~/config/secrets.json → cron đi tìm /root/config/secrets.json → file không có.
Test nhanh:
* * * * * echo "HOME=$HOME, USER=$USER, PWD=$PWD" >> /tmp/cron-debug.log
Đợi 1 phút, mở /tmp/cron-debug.log. Đối chiếu với echo $HOME ở SSH session — nếu khác, đây là thủ phạm.
Fix: dùng absolute path mọi nơi trong script. Hoặc đổi user chạy cron (dùng sudo crontab -u deploy -e thay vì root).
Lý do #3: Output đi vào hư không
Cron mặc định email output cho user. Trên hầu hết VPS không setup mail server → email rơi vào /var/spool/mail/<user> (hoặc bị bỏ luôn). Bạn không bao giờ thấy.
Quy tắc tôi luôn áp dụng: mọi cron line phải redirect cả stdout và stderr ra file.
* * * * * /home/deploy/app/script.sh >> /var/log/myapp/cron.log 2>&1
>> file append stdout, 2>&1 redirect stderr về cùng chỗ với stdout. Đặt 2>&1 sau >> file — đảo ngược thứ tự sẽ không hoạt động đúng (stderr vẫn lạc).
Sau khi setup, tail -f /var/log/myapp/cron.log là cửa sổ debug duy nhất bạn cần.
Lý do #4: Time zone server khác bạn
Server bạn thuê thường mặc định UTC. Ở Việt Nam, UTC = local - 7. Bạn đặt 0 8 * * * (8h sáng) — cron chạy lúc 15h local. Khách phàn nàn “báo cáo sáng đến chiều mới có”.
Check time zone:
timedatectl
# hoặc cũ hơn
cat /etc/timezone
date
Hai cách xử lý:
-
Đổi server về tz Việt Nam (nếu server chỉ phục vụ user VN):
sudo timedatectl set-timezone Asia/Ho_Chi_Minh -
Giữ server UTC, viết cron theo UTC (production thật, có user nhiều múi giờ):
0 1 * * *để chạy 8h sáng VN.
Cách 2 sạch hơn về dài hạn, nhưng đòi hỏi mọi người trong team phải nhớ “server là UTC”. Tôi khuyên team nhỏ nên đi cách 1, team lớn / SaaS đi cách 2.
Lý do #5: User không có quyền
Crontab có 2 loại:
- User crontab:
crontab -e— chạy với quyền user đó. - System crontab:
/etc/crontab,/etc/cron.d/*,/etc/cron.daily/*— có cột thêm “user” trước command.
Bẫy hay gặp: bạn copy command từ system crontab về user crontab nhưng quên rằng nó cần root để write vào /var/log hoặc bind port < 1024 hoặc đọc /etc/shadow.
Test bằng cách sudo -u <user> /path/to/script.sh — chạy y như cron sẽ chạy. Nếu fail thì là quyền.
Fix: hoặc move sang root crontab, hoặc đổi script không cần quyền cao (vd /var/log/myapp/ chmod cho user deploy ghi).
Lý do #6: Job overlap — chạy đè lên nhau
Cron * * * * * chạy mỗi phút. Nếu lần chạy này mất 90 giây, lần kế tiếp đã start lúc giây 60 — hai instance cùng chạy. Với job đụng database thì hỏng dữ liệu, với job sync file thì lock conflict.
Fix bằng flock:
* * * * * /usr/bin/flock -n /tmp/myjob.lock /home/deploy/app/script.sh >> /var/log/myapp/cron.log 2>&1
flock -n thử khoá file /tmp/myjob.lock. Đang khoá → exit ngay (không chờ). Nên nếu instance trước chưa xong, instance mới bỏ qua. Đơn giản, không phụ thuộc package nào.
Checklist debug 60 giây
Khi cron im re, tôi đi qua đúng thứ tự này:
# 1. Cron có chạy không?
sudo systemctl status cron # Debian/Ubuntu
sudo systemctl status crond # CentOS/RHEL
# 2. Crontab có gì?
crontab -l
# 3. Cron log có gì?
sudo tail -50 /var/log/syslog | grep CRON # Ubuntu
sudo tail -50 /var/log/cron # CentOS
# 4. Setup probe để xem env
echo '* * * * * env > /tmp/cron-env.log' | crontab -
sleep 70 && cat /tmp/cron-env.log
# 5. Compare với env login:
env | sort > /tmp/login-env.log
diff /tmp/cron-env.log /tmp/login-env.log
Bước 4–5 là vũ khí cuối — diff giữa env trong cron và env login chỉ ra chính xác thiếu/khác biến nào. 95% case tôi giải bằng 2 bước này.
Template cron tôi đang dùng
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=""
# m h dom mon dow command
# Backup database mỗi đêm 2h sáng VN (= 19h UTC hôm trước)
0 19 * * * /usr/bin/flock -n /tmp/db-backup.lock /home/deploy/app/scripts/backup.sh >> /var/log/myapp/backup.log 2>&1
# Báo cáo sáng 8h VN (= 1h UTC)
0 1 * * * /home/deploy/app/scripts/morning-report.sh >> /var/log/myapp/report.log 2>&1
3 dòng đầu (SHELL, PATH, MAILTO) là non-negotiable với tôi. MAILTO="" tắt email báo lỗi (vì redirect ra file rồi). SHELL=/bin/bash để dùng được syntax bash trong command (mặc định cron là /bin/sh).
Lời cuối
Cron là một trong những công cụ Unix lâu đời nhất, đơn giản, và đầy bẫy ngầm. Hầu hết bug không phải ở script bạn viết — mà ở environment xung quanh: PATH, HOME, USER, TZ, output redirect, locking.
Nắm 6 lý do trên là bạn xử được 95% case. 5% còn lại thường là SELinux/AppArmor block — nhưng đó là chuyện cho một bài khác.