EST. 2026 — บันทึกงานเซิร์ฟเวอร์จริง SYSADMIN · SECURITY · AI · DIRECTADMIN เล่มที่ 01
THE SYSADMIN ALMANAC
หน้าหลัก / Linux / cron ไม่รันเลยสักรอบ เพราะ redirect log ไปโฟลเดอร์ที่ไม่มี
Linux

cron ไม่รันเลยสักรอบ เพราะ redirect log ไปโฟลเดอร์ที่ไม่มี

โดย Ton-Or ·15 พ.ย. 2026·อ่าน 10 นาที
cron ไม่รันเลยสักรอบ เพราะ redirect log ไปโฟลเดอร์ที่ไม่มี

บรรทัด cron ที่ลงท้ายด้วยการเก็บ log ดูเป็นนิสัยที่ดี แต่ถ้าปลายทางของ log เปิดไม่ได้ คำสั่งข้างหน้าจะไม่ถูกรันเลย และหลักฐานเดียวที่บอกสาเหตุก็ถูกส่งไปที่ที่ไม่มีใครอ่าน

สรุปใน 1 นาที

อาการ/ปัญหา
งาน cron ไม่รันเลยสักรอบ ไม่มี log ไม่มีเมล รันมือผ่านปกติ
ใครควรอ่าน
คนตั้ง crontab ที่ลงท้ายบรรทัดด้วยการ redirect log
ทางแก้แบบสั้น
ปลายทาง redirect ไปโฟลเดอร์ที่ไม่มี shell เลยไม่รันคำสั่ง แก้โดยให้งานสร้างที่เก็บ log ของตัวเองก่อน และใช้เช็กลิสต์ก่อนปล่อยบรรทัด cron
ข้ามไปขั้นตอน

พอดีผมตั้งคิวปล่อยบทความอัตโนมัติให้เว็บตัวหนึ่ง ให้ cron ปล่อยวันละสองรอบตอน 08:00 กับ 20:00 รอบแรกคือ 20:00 ของวันที่ตั้ง ถึงเวลาแล้วไม่มีอะไรเกิดขึ้นเลย บทความไม่ขึ้น ไฟล์ log ไม่ถูกสร้าง ไม่มีเมล ไม่มี error ใน log ของแอป แต่พอรันคำสั่งเดียวกันด้วยมือจาก shell ผ่านปกติ ผมเลยต้องปล่อยบทความแรกด้วยมือตอน 20:05 แล้วค่อยมานั่งหาว่าทำไม cron ถึงเงียบ

ต้นเหตุคือโฟลเดอร์ที่ผมสั่งให้เก็บ log ไม่มีอยู่จริง แค่นั้น ชื่อ user กับ path ในบทความนี้เป็นค่าสมมุติ ส่วนลำดับเหตุการณ์และวิธีพิสูจน์คือของที่ทำจริง

อาการ: เงียบทุกช่องทาง

เว็บเป็น Laravel บน shared host แบบ DirectAdmin มีบทความสถานะรอคิว และมีคำสั่ง artisan ที่ปล่อยบทความตัวแรกของคิว ผมใส่ crontab ของ user เว็บไว้แบบนี้

MAILTO=""
0 8,20 * * * /opt/php/bin/php /home/webuser/app/artisan articles:publish-next >> /home/webuser/private/publish-next.log 2>&1

path ของ php ต้องเขียนเต็ม เพราะ php ตัวที่ CLI เรียกเจอเป็นค่าเริ่มต้นบนเครื่องนี้เป็นรุ่นเก่ากว่าที่แอปใช้ ส่วน MAILTO="" ใส่ไว้เพราะไม่อยากได้เมลทุกรอบ

สิ่งที่ตรวจหลังรอบ 20:00 ผ่านไป

0 บรรทัดใน log ของแอป
ไม่มีไฟล์publish-next.log
0 ฉบับเมลจาก cron
ผ่านรันคำสั่งเดียวกันด้วยมือ
20:05ปล่อยบทความแรกด้วยมือ
70 วินาทีเวลาที่ใช้พิสูจน์ daemon

ความเงียบแบบนี้ต่างจากงานที่รันแล้วล้ม งานที่ล้มยังทิ้งอะไรไว้บ้าง แต่รอบนี้ไม่มีร่องรอยว่ามีอะไรพยายามรันเลย

สามข้อที่ผมเดาผิด

ข้อแรก ผมคิดว่า path ของ php ผิด หรือ cron ไปหยิบ php รุ่นเก่า เพราะรู้อยู่แล้วว่าเครื่องนี้มีหลายรุ่น แต่ในบรรทัด crontab เขียน path เต็มถูกต้องอยู่แล้ว และคำสั่งเดียวกันรันมือผ่าน

ข้อสอง ผมคิดว่า cron daemon ไม่ได้อ่าน crontab ของ user นี้ เครื่องนี้เคยย้ายจากตระกูล CentOS มาเป็น Debian ซึ่งของหลายอย่างเปลี่ยนที่

รายการ ตระกูล CentOS ตระกูล Debian
ที่เก็บ crontab ของ user /var/spool/cron/<user> /var/spool/cron/crontabs/<user>
ชื่อ service crond cron
log ของ cron /var/log/cron journalctl -u cron หรือ /var/log/syslog

ถ้ามีสคริปต์หรือเครื่องมือเก่าที่ยังเขียนไฟล์ลงที่เดิม งานก็จะไม่ถูกอ่าน เป็นข้อสงสัยที่สมเหตุสมผล แต่รอบนี้ไม่ใช่ ส่วนกรณี crontab ถูกเขียนลงผิด user ผมเขียนแยกไว้ที่ crontab ของ user เว็บ ไม่ใช่ crontab ที่ cron รันจริง รอบนี้ crontab ถูกไฟล์ ถูก user แต่บรรทัดตายก่อนถึงคำสั่ง

ข้อสาม ผมคิดว่าเป็นเรื่อง timezone คือ cron อาจนับ 20:00 คนละเวลากับที่ผมคิด ซึ่งถ้าใช่ งานก็ควรรันในชั่วโมงอื่นและทิ้ง log ไว้ แต่ไม่มี log เลย

ทั้งสามข้อมีจุดร่วมคือผมเดาโดยไม่มีข้อมูล เพราะช่องทางที่ควรให้ข้อมูลถูกผมปิดเองทั้งหมด

พิสูจน์ daemon ใน 70 วินาที

แทนที่จะเดาต่อ ผมแยกปัญหาเป็นสองท่อนก่อน คือ daemon กับ crontab ทำงานไหม และบรรทัดงานของผมมีปัญหาไหม วิธีที่เร็วที่สุดคือเพิ่มบรรทัดชั่วคราวที่รันทุกนาทีและเขียนลงที่ที่มีอยู่แน่นอน

# เพิ่มบรรทัด probe ต่อท้าย crontab เดิม โดยไม่ต้องเปิด editor
( crontab -l; echo '* * * * * date >> /home/webuser/cron-probe.log' ) | crontab -

# รอให้ข้ามขอบนาทีแน่ ๆ แล้วดูผล
sleep 70; cat /home/webuser/cron-probe.log

# เสร็จแล้วถอดบรรทัด probe ออก และลบไฟล์
crontab -l | grep -v 'cron-probe.log' | crontab -
rm -f /home/webuser/cron-probe.log

รอประมาณ 70 วินาทีมีบรรทัดเวลาโผล่ในไฟล์ แปลว่า daemon ทำงาน crontab ของ user นี้ถูกอ่าน และเวลาของเครื่องเดินปกติ ข้อเดาที่สองกับสามตกไปพร้อมกัน ปัญหาเหลืออยู่ที่บรรทัดงานของผมเอง

สิ่งเดียวที่ต่างกันระหว่างบรรทัด probe กับบรรทัดงาน นอกจากตัวคำสั่งซึ่งรันมือผ่านแล้ว คือปลายทางของ redirect

$ ls -ld /home/webuser/private
ls: cannot access '/home/webuser/private': No such file or directory
ทำไมบรรทัด probe ถึงได้ผลเร็ว

มันตัดตัวแปรออกทีละครึ่ง ถ้าไม่มีบรรทัดเวลาโผล่ ปัญหาอยู่ที่ daemon สิทธิ์ หรือไฟล์ crontab ถ้ามี ปัญหาอยู่ที่บรรทัดงาน ไม่ต้องรอถึงรอบจริงซึ่งรอบนี้คือ 12 ชั่วโมง อย่าลืมถอดบรรทัดออกหลังได้คำตอบ

กลไก: shell เปิด redirect ก่อนรันคำสั่ง

cron ไม่ได้รันคำสั่งของเราตรง ๆ มันส่งทั้งบรรทัดให้ sh -c และ sh จัดการ redirect ก่อนจะ exec คำสั่ง ถ้าเปิดไฟล์ปลายทางไม่ได้ sh จะแจ้ง error แล้วเลิก คำสั่งข้างหน้าไม่ถูกรันเลยแม้แต่นิดเดียว

ที่เกิดจริง: redirect ล้มก่อนถึงคำสั่งcron ถึงเวลา08:00 / 20:00sh -cรับทั้งบรรทัดเปิด >> logโฟลเดอร์ไม่มีไม่ exec คำสั่งphp ไม่ถูกเรียกstderr ถูกทิ้งMAILTO="" ว่างหลัง mkdir -p: ลำดับเดิม ผ่านทุกขั้นcron ถึงเวลา08:00 วันถัดไปsh -cรับทั้งบรรทัดเปิด >> logสร้างไฟล์ได้exec คำสั่งphp artisan รันผลลง logอ่านย้อนหลังได้ข้อความเดียวที่บอกสาเหตุ ออกทาง stderr ของ sh แล้วไม่มีใครได้อ่าน:sh: 1: cannot create /home/webuser/private/publish-next.log: Directory nonexistent
sh เปิดปลายทางของ redirect ก่อน exec เสมอ เมื่อขั้นนี้ล้ม คำสั่งไม่ถูกเรียก และข้อความ error ไปลงช่องทางที่ถูกปิดไว้

ลองพิสูจน์เองได้บนเครื่องไหนก็ได้ โดยไม่ต้องแตะ cron

$ sh -c 'echo hi >> /nonexistent/x.log 2>&1; echo rc=$?'
sh: 1: cannot create /nonexistent/x.log: Directory nonexistent
rc=2

echo hi ไม่ได้ทำงาน และ exit code ไม่เป็นศูนย์ ข้อความกับตัวเลขจะต่างกันไปตาม shell ที่อยู่หลัง /bin/sh ถ้าเป็น bash จะได้ No such file or directory แต่พฤติกรรมเหมือนกัน คือคำสั่งที่มี redirect เปิดไม่ได้จะไม่ถูกรัน

จุดที่ทำให้เงียบสนิทคือ 2>&1 ที่ผมใส่ไว้ท้ายบรรทัดไม่ได้ช่วยอะไร เพราะมันเป็น redirect ของคำสั่งที่ไม่เคยเกิด ข้อความ error ของ sh เองออกทาง stderr ที่ cron ถืออยู่ ซึ่งปกติ cron จะรวบรวมแล้วเมลหาเจ้าของ crontab แต่ผมตั้ง MAILTO="" ไว้ ข้อความเลยถูกทิ้ง

ช่องทางที่ถูกปิดทีละชั้นlog ของงานเปิดไม่ได้ตั้งแต่ต้น->log ของแอปแอปไม่เคยถูกเรียก->เมลจาก cronMAILTO ว่าง->เหลือ log ของ daemonที่ผมไม่ได้เปิดดู

ที่จริงยังเหลืออีกช่องทางหนึ่งที่ไม่ถูกปิด คือ log ของตัว cron daemon

# ต้องใช้สิทธิ์ root - cron บันทึกบรรทัด CMD ทุกครั้งที่ยิงงาน แม้คำสั่งจะล้มทันที
journalctl -u cron --since "20:00" --until "20:05"
grep CRON /var/log/syslog | grep webuser | tail -5

ถ้าเห็นบรรทัด CMD ตรงเวลา แปลว่า cron ยิงงานแล้ว และสิ่งที่ล้มอยู่หลังจากนั้น บน shared host ที่ user เว็บไม่มีสิทธิ์อ่าน log ระบบ บรรทัด probe เลยเป็นทางที่ใช้ได้เสมอ

แก้: ให้งานสร้างที่เก็บ log ของตัวเอง

ทางแก้เฉพาะหน้าคือสร้างโฟลเดอร์

mkdir -p /home/webuser/private && chmod 700 /home/webuser/private

รอบ 08:00 ของวันถัดไป cron ปล่อยบทความเองโดยผมไม่ต้องทำอะไร และไฟล์ log ถูกสร้างตามปกติ

แต่ทางแก้เฉพาะหน้าไม่กันรอบหน้า เพราะวันที่ย้ายเครื่องหรือกู้จาก backup โฟลเดอร์นี้อาจหายอีก ผมเลยมองทางแก้ถาวรไว้สองแบบ

แบบที่ 1

mkdir -p ใน deploy script

เพิ่มบรรทัดสร้างโฟลเดอร์ log ไว้ในขั้น deploy บรรทัด crontab คงเดิม ง่ายและพอสำหรับงานที่ deploy ผ่านสคริปต์ทุกครั้ง แต่ถ้ามีใครตั้งเครื่องใหม่ด้วยมือแล้วข้ามสคริปต์ ปัญหาเดิมกลับมา

แบบที่ 2

wrapper ที่ดูแลตัวเอง

crontab เรียกสคริปต์ตัวเดียวโดยไม่มี redirect ในบรรทัด สคริปต์สร้างโฟลเดอร์ เปิด log แล้วค่อย exec งานจริง บรรทัด crontab สั้นลง และเงื่อนไขทั้งหมดอยู่ในไฟล์ที่ติดไปกับโค้ด

wrapper ที่ผมใช้เป็นแม่แบบ

#!/bin/sh
# /home/webuser/bin/publish-next.sh - crontab เรียกไฟล์นี้อย่างเดียว ไม่มี redirect ในบรรทัด cron
LOG_DIR=/home/webuser/private
mkdir -p "$LOG_DIR" || exit 1

exec >> "$LOG_DIR/publish-next.log" 2>&1
echo "== $(date '+%F %T') start"
exec /opt/php/bin/php /home/webuser/app/artisan articles:publish-next
0 8,20 * * * /home/webuser/bin/publish-next.sh

exec >> file 2>&1 แบบไม่มีคำสั่งตามหลังคือการเปลี่ยนปลายทาง stdout กับ stderr ของตัวสคริปต์เอง ทุกบรรทัดถัดจากนั้นลง log รวมถึง error ของ php ถ้ามี

ก่อนปล่อยบรรทัดใหม่ลง crontab ผมทดสอบด้วยการรันบรรทัดนั้นในสภาพที่ใกล้กับ cron ที่สุด คือผ่าน sh -c และล้าง environment ออกให้เหลือเท่าที่ cron ให้ การรันจาก shell ที่ login อยู่ผ่านได้เพราะมี PATH และตัวแปรอื่นที่ cron ไม่มี

# จำลองสภาพของ cron: environment เกือบว่าง, shell เป็น /bin/sh, ไม่มี tty
env -i HOME=/home/webuser PATH=/usr/bin:/bin /bin/sh -c '/home/webuser/bin/publish-next.sh' < /dev/null; echo "rc=$?"

ถ้าบรรทัดงานมี redirect ไปที่ที่เปิดไม่ได้ วิธีนี้จะแสดงข้อความ error ของ sh ออกมาบนจอทันที ไม่ต้องรอถึงรอบจริง

เรื่อง MAILTO ส่วนตัวผมเปลี่ยนนิสัยเป็นไม่ตั้งว่างจนกว่างานจะรันนิ่งแล้วอย่างน้อยหนึ่งรอบจริง ช่วงแรกยอมรับเมลไปก่อน พองานนิ่งและมี log ที่เชื่อได้แล้วค่อยปิด

nohup กับงานเบื้องหลังพังแบบเดียวกัน

กลไกนี้ไม่ได้เป็นของ cron อย่างเดียว บรรทัดที่ใช้สั่งงานเบื้องหลังบนเซิร์ฟเวอร์ระยะไกลเจอแบบเดียวกัน

# ถ้า logs/ ยังไม่มี งานไม่เริ่ม และ shell คืน prompt ตามปกติ
nohup bash job.sh > logs/out.log 2>&1 &

# สร้างโฟลเดอร์ก่อนเสมอ
mkdir -p logs && nohup bash job.sh > logs/out.log 2>&1 &

แบบแรกอันตรายกว่า cron ด้วยซ้ำ เพราะเรามักสั่งแล้วปิด session ไปทำอย่างอื่น กลับมาอีกทีพบว่างานที่คิดว่ารันอยู่หลายชั่วโมงไม่เคยเริ่ม

อีกเรื่องที่ควรใส่คู่กับงาน cron ที่มีหลายขั้นคือกันรอบซ้อน ตัวอย่างจากอีกระบบที่ผมดูแล เป็นงานรายชั่วโมงสองขั้นต่อกัน ใช้เวลารอบละประมาณ 31 วินาที ผมครอบ flock ไว้ในบรรทัดเดียว

5 * * * * flock -n /home/webuser/private/hourly.lock -c '/home/webuser/bin/pull-data.sh && /home/webuser/bin/recompute.sh' >> /home/webuser/private/hourly.log 2>&1

-n ทำให้รอบใหม่เลิกทันทีถ้ารอบเก่ายังถือ lock อยู่ ไม่เข้าคิวรอ วันปกติ 31 วินาทีไม่มีทางชนรอบถัดไป แต่วันที่ต้นทางช้าหรือเครือข่ายค้าง รอบที่ซ้อนกันสองรอบเขียนข้อมูลชุดเดียวกันพร้อมกันจะสร้างปัญหาที่หายากกว่ามาก สังเกตว่าไฟล์ lock กับไฟล์ log อยู่ในโฟลเดอร์เดียวกัน ถ้าโฟลเดอร์นั้นไม่มี บรรทัดนี้ก็ตายเงียบแบบเดียวกับเรื่องข้างบน

บน shared host อย่าเก็บ log ที่ /tmp

host บางแบบไม่ให้ PHP ที่รันจากฝั่งเว็บเขียน /tmp และล้มเงียบเหมือนกัน ถ้างาน cron กับโค้ดฝั่งเว็บต้องใช้ไฟล์ cache หรือ log ร่วมกัน ให้เก็บไว้ใต้ home ของ user ในโฟลเดอร์ที่อยู่นอก docroot จะได้พฤติกรรมเดียวกันทั้งสองฝั่ง

เช็กลิสต์ก่อนปล่อยบรรทัด cron

  1. interpreter เขียน path เต็ม และเป็นรุ่นเดียวกับที่รันมือผ่าน ตรวจด้วยการรันบรรทัดเดียวกันผ่าน sh -c ไม่ใช่ผ่าน shell ที่ login อยู่
  2. โฟลเดอร์ปลายทางของทุก redirect มีอยู่จริง และมีบรรทัด mkdir -p อยู่ใน deploy script หรือใน wrapper
  3. user เจ้าของ crontab มีสิทธิ์เขียนโฟลเดอร์นั้น ไม่ใช่มีแค่ root
  4. MAILTO ยังไม่ตั้งว่างจนกว่างานจะรันจริงผ่านอย่างน้อยหนึ่งรอบ
  5. เครื่องหมาย % ในบรรทัด cron ต้องเขียนเป็น \% ไม่งั้น cron ตัดบรรทัดตรงนั้นและส่งส่วนที่เหลือเป็น stdin เจอบ่อยกับ date +%F
  6. บรรทัดสุดท้ายของ crontab ต้องจบด้วย newline ไม่งั้นบรรทัดนั้นอาจไม่ถูกอ่าน
  7. งานหลายขั้นหรืองานที่อาจช้า ครอบด้วย flock -n
  8. ใส่บรรทัด probe ทุกนาที รอ 70 วินาที ยืนยันว่า daemon อ่าน crontab แล้วถอดออก

บทเรียนของรอบนี้คือผมเสียเวลาเดาสามเรื่องที่ซับซ้อนกว่าความจริงทั้งหมด เพราะงานที่ไม่ทิ้งร่องรอยทำให้คิดไปถึงสาเหตุใหญ่ก่อนเสมอ ครั้งต่อไปที่ cron เงียบ ผมจะเริ่มจากบรรทัด probe แล้วดูปลายทางของ redirect ก่อนอย่างอื่น ใครจะเลือก wrapper หรือเลือกใส่ mkdir -p ใน deploy ก็ตามสะดวกครับ ขอแค่มีอย่างใดอย่างหนึ่ง ตรงไหนผมอธิบายตกหล่นหรือพิมพ์ผิด เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ

อยากให้เราทำให้ไหม?

ให้เราช่วยติดตั้ง ตั้งค่า ย้ายระบบ หรือแก้ปัญหาเซิร์ฟเวอร์ เพื่อให้คุณมีเวลาไปทำงานที่สำคัญกว่า

คุยเรื่องงานกับเรา

เคยเจองานแบบนี้ไหม? ติดตรงไหน?

มาแลกวิธีกันต่อในบอร์ด LinuxThai

→ คุยต่อในบอร์ด

ความคิดเห็น 0

ยังไม่มีความคิดเห็น — เป็นคนแรกเลย