cron là bộ lập lịch kinh điển của Unix: chạy backup lúc 2h sáng, dọn log mỗi tuần, gọi một script định kỳ. Cú pháp năm trường của nó không khó. Nhưng có một câu than quen thuộc với mọi kỹ sư: "script chạy tay thì hoàn hảo, mà để cron chạy thì im lặng không làm gì cả". Nguyên nhân gần như luôn giống nhau, và nó không nằm ở code của bạn. Bài này (phần 11 loạt Unix) chạy một crond thật trong container để bắt tận tay cái bẫy đó và cách sửa.
Cú pháp: năm trường thời gian
Mỗi dòng crontab là năm trường thời gian rồi tới lệnh:
┌─ phút (0-59)
│ ┌─ giờ (0-23)
│ │ ┌─ ngày trong tháng (1-31)
│ │ │ ┌─ tháng (1-12)
│ │ │ │ ┌─ thứ trong tuần (0-6, 0=Chủ nhật)
* * * * * /duong/dan/job.sh # mỗi phút
0 2 * * * backup.sh # 2h sáng hằng ngày
*/15 * * * * check.sh # mỗi 15 phút
0 9 * * 1 bao-cao.sh # 9h sáng thứ Hai
* là "mọi giá trị", */N là "mỗi N", danh sách 1,15 và khoảng 1-5 cũng được. Quản lý bằng crontab -e (sửa), crontab -l (xem), crontab -r (xoá).

Hình 1: Crontab gồm năm trường thời gian (phút/giờ/ngày/tháng/thứ) + lệnh; bẫy #1 là môi trường tối giản (PATH ngắn); cách sửa là set PATH/đường dẫn tuyệt đối; bẫy #2 là không ghi log nên không thấy lỗi.
Đo thật: bẫy PATH, bắt tận tay
Điều khiến job cron thất bại thầm lặng: cron chạy với môi trường tối giản. Nó không nạp ~/.bashrc, ~/.profile hay các biến bạn quen có trong terminal. Đặc biệt, PATH của cron rất ngắn — thường chỉ /usr/bin:/bin. Bất kỳ lệnh nào cài ở nơi khác (như go ở /usr/local/go/bin) sẽ không tìm thấy. Mình chạy crond thật để chứng minh:

Hình 2: Chạy thật — PATH shell thường có /usr/local/go/bin, môi trường cron chỉ /usr/bin:/bin; crond fire lúc 10:24:01 báo go: command not found; sau khi thêm PATH=... vào crontab, job chạy lúc 10:25:01 cho go version go1.23.12 linux/arm64.
Đây là bằng chứng end-to-end, không phải mô phỏng: crond thật thi hành job đúng đầu mỗi phút (:01). Lần đầu (không set PATH), job chạy với PATH=/usr/bin:/bin và go — dù cài sẵn — báo command not found. Sau khi thêm một dòng PATH=... vào crontab, job phút sau chạy trơn tru và in đúng phiên bản Go. Khác biệt duy nhất giữa thất bại và thành công là dòng PATH — code không đổi một ký tự.
Hai cách sửa
- Set
PATHở đầu crontab: thêmPATH=/usr/local/go/bin:/usr/local/bin:/usr/bin:/bintrước các dòng job. Đơn giản, áp cho mọi job trong crontab đó. - Dùng đường dẫn tuyệt đối trong script: gọi
/usr/local/go/bin/gothay vìgo, vàsourcemôi trường cần thiết ở đầu script. Bền hơn khi script được gọi từ nhiều nơi.
Bẫy thứ hai: không có log = mù
Cron gửi output (stdout/stderr) của job qua mail nội bộ của hệ thống — mà gần như không ai đọc. Nên khi job lỗi, bạn không thấy gì. Luôn chuyển hướng ra file log:
* * * * * /work/job.sh >> /var/log/job.log 2>&1
2>&1 gộp cả stderr vào file (đúng bài học thứ tự redirect ở phần pipe/fd). Không có dòng này, một job hỏng sẽ hỏng âm thầm hàng tháng trời — chính là kịch bản "sao backup không chạy?" kinh điển.
Đánh đổi cần cân nhắc
cron không "bù" lần chạy bị lỡ. Nếu máy tắt đúng lúc job đến hạn, cron bỏ qua lần đó — không chạy bù khi máy bật lại. Với máy tính cá nhân/laptop hay tắt, dùng anacron (chạy bù các job ngày/tuần/tháng bị lỡ) hoặc systemd timer với Persistent=true. cron hợp nhất cho server luôn bật.
Múi giờ và định dạng % là bẫy nhỏ. cron chạy theo múi giờ của hệ thống (hoặc CRON_TZ) — đặt sai là job chạy lệch giờ, đúng như bẫy TZ khi triển khai. Và ký tự % trong dòng crontab có nghĩa đặc biệt (xuống dòng cho stdin), phải thoát \% — hay gặp khi dùng date +%Y%m%d trực tiếp trong crontab; bọc trong script để tránh.
systemd timer là lựa chọn hiện đại cho nhiều tình huống. Trên hệ thống dùng systemd, systemd timer cho log tích hợp (journalctl), phụ thuộc dịch vụ, chạy bù, và quản lý tập trung hơn cron. cron vẫn đơn giản và có mặt khắp nơi; chọn theo môi trường và nhu cầu giám sát.
Ba ý mang về
- cron chạy với môi trường tối giản: đo thật crond fire lúc
10:24:01vớiPATH=/usr/bin:/binkhiếngo: command not founddùgođã cài — đây là lý do #1 của "chạy tay được, cron thì không". Sửa bằng setPATHtrong crontab hoặc dùng đường dẫn tuyệt đối (đo thật job chạy đúng ở phút kế). - Luôn ghi log job cron: cron gửi output qua mail nội bộ mà không ai đọc — thêm
>> /var/log/job.log 2>&1để lỗi không hỏng âm thầm. - Biết giới hạn của cron: cú pháp năm trường (phút/giờ/ngày/tháng/thứ); cron không chạy bù lần lỡ (dùng anacron/systemd timer cho máy hay tắt), chạy theo múi giờ hệ thống, và
%cần thoát trong crontab.
Nguồn
- man7.org — crontab(5) (định dạng): https://man7.org/linux/man-pages/man5/crontab.5.html
- man7.org — crontab(1) (lệnh): https://man7.org/linux/man-pages/man1/crontab.1.html
- crontab.guru — công cụ giải thích biểu thức cron: https://crontab.guru/
Phần sau khép lại loạt với bài tổng hợp: vì sao script shell hỏng, và dùng shellcheck để bắt trước hàng loạt lỗi (quoting, biến chưa gán, exit code) mà chúng ta đã gặp suốt loạt bài.