วิธีแก้ปัญหา docker container ไม่ start cron อัตโนมัติ

ปัญหา

เวลาสร้าง docker container เพื่อ run งานแบบอัตโนมัติ เราก็จำเป็นต้องใช้ cron แต่ว่า เจ้า docker container images เนี่ย จะเป็นรุ่นตัดทุกอย่างที่ไม่จำเป็นออกหมด เอาไว้ให้ start – run – stop แต่ถึงกระนั้น เราก็ยังอยากได้การทำงานแบบ cron อยู่ดี

Reproduce

สร้าง docker container จาก ubuntu:20.04

docker run -d -it  --name kxtest ubuntu:20.04

ตรวจสอบว่ามี crontab หรือไม่ ก็ไม่มี

docker exec -it kxtest bash

root@93c827f2f9fc:/# crontab -l
bash: crontab: command not found
root@93c827f2f9fc:/# 

/var/log/syslog มีไม๊ ก็ไม่มี

root@93c827f2f9fc:/# ls -l /var/log/syslog
ls: cannot access '/var/log/syslog': No such file or directory

systemctl มีไม๊ ก็ไม่มี

root@93c827f2f9fc:/# systemctl
bash: systemctl: command not found

แก้ไข

ติดตั้ง 3 อย่าง cron, systemctl, rsyslog แล้ว ขอแถม vim ด้วย

apt update ; apt install -y cron systemctl rsyslog vim

ติดตั้งแล้ว ลองตรวจสอบ ก็พบว่า crontab มาแล้ว, systemctl มาแล้ว ส่วน /var/log/syslog ยังไม่ได้สร้าง

root@93c827f2f9fc:/# crontab -l
no crontab for root
root@93c827f2f9fc:/# systemctl      
apt-daily-upgrade.service	loaded inactive dead	Daily apt upgrade and clean activities
apt-daily-upgrade.timer	loaded inactive dead	Daily apt upgrade and clean activities
apt-daily.service	loaded inactive dead	Daily apt download activities
apt-daily.timer	loaded inactive dead	Daily apt download activities
cron.service	loaded inactive dead	Regular background program processing daemon
dmesg.service	loaded inactive dead	Save initial kernel messages after boot
e2scrub@.service	loaded inactive dead	Online ext4 Metadata Check for 
e2scrub_all.service	loaded inactive dead	Online ext4 Metadata Check for All Filesystems
e2scrub_all.timer	loaded inactive dead	Periodic ext4 Online Metadata Check for All Filesystems
e2scrub_fail@.service	loaded inactive dead	Online ext4 Metadata Check Failure Reporting for 
e2scrub_reap.service	loaded inactive dead	Remove Stale Online ext4 Metadata Check Snapshots
fstrim.service	loaded inactive dead	Discard unused blocks on filesystems from /etc/fstab
fstrim.timer	loaded inactive dead	Discard unused blocks once a week
hwclock.sh.service	loaded inactive dead	
logrotate.service	loaded inactive dead	Rotate log files
logrotate.timer	loaded inactive dead	Daily rotation of log files
motd-news.service	loaded inactive dead	Message of the Day
motd-news.timer	loaded inactive dead	Message of the Day
procps.service	loaded inactive dead	Loads kernel parameters that are specified in /etc/sysctl.conf
rsyslog.service	loaded inactive dead	System Logging Service

20 loaded units listed.
To show all installed unit files use 'systemctl list-unit-files'.
root@93c827f2f9fc:/# ls -l /var/log/syslog
ls: cannot access '/var/log/syslog': No such file or directory

ลอง start cron และ rsyslog ดู ก็พบว่า /var/log/syslog มาแล้ว

root@93c827f2f9fc:/# systemctl start cron
root@93c827f2f9fc:/# systemctl status cron
cron.service - Regular background program processing daemon
    Loaded: loaded (/usr/lib/systemd/system/cron.service, disabled)
    Active: active (running)
root@93c827f2f9fc:/# systemctl start rsyslog
rsyslog.service - System Logging Service
    Loaded: loaded (/usr/lib/systemd/system/rsyslog.service, disabled)
    Active: active (running)
root@93c827f2f9fc:/# ls -l /var/log/syslog
-rw-r----- 1 syslog adm 517 Oct 10 01:36 /var/log/syslog

ทดสอบสร้าง cron

root@93c827f2f9fc:/# crontab -e

ทำความเข้าใจก่อน crontab เอาไว้สร้าง cron ส่วนตัวของแต่ละ user

แต่ที่ /etc/cron.d/ นั้น เอาไว้สร้างสำหรับ system

2 วิธีการสร้าง cron นี้แตกต่างกันที่ crontab ไม่สามารถระบุผู้ใช้ได้ แต่ใน /etc/cron.d/ นั้น สามารถระบุ user ที่จะ run ได้

ต่อไป จะสร้าง cron ง่าย ๆ ให้ echo เวลา ทุก 1 นาทีลงไฟล์

root@93c827f2f9fc:~# echo 'echo $(date "+%Y%m%d-%H%M%S") >> test.log' > ~/testcron.sh ; chmod +x ~/testcron.sh
root@93c827f2f9fc:~# crontab -e

แล้วใส่ บรรทัดนี้ลงไป

* * * * * /usr/bin/bash ~/testcron.sh

ดูใน /var/log/syslog แล้ว ก็พบว่า ทำงานได้

Oct 10 01:56:39 93c827f2f9fc crontab[1631]: (root) BEGIN EDIT (root)
Oct 10 01:56:42 93c827f2f9fc crontab[1631]: (root) END EDIT (root)
Oct 10 01:57:01 93c827f2f9fc CRON[1638]: (root) CMD (/usr/bin/bash ~/testcron.sh)

ดูผลใน -/test.log

root@93c827f2f9fc:~# cat ~/test.log 
20211010-015701
20211010-015801
20211010-015901

เป็นอันว่า OK ต่อไป ดูว่า ถ้า restart docker แล้ว cron จะทำงานไม๊ ?

ทดสอบ restart docker container

ออกจาก bash ของ docker แล้วใช้คำสั่ง

docker restart kxtest

รอสัก 3 นาที แล้วใช้คำสั่ง

docker exec -it kxtest bash -c "date ; cat ~/test.log"

ผลที่ได้คือ อ้าว cron ไม่ทำงานต่อ

Sun Oct 10 02:02:33 UTC 2021
20211010-015701
20211010-015801
20211010-015901

มาดูเหตุกันก่อน

เราต้องไปดู รากเหง้าของ ubuntu:20.04 image

https://hub.docker.com/_/ubuntu

Docker ทำงานโดยจะทำเพียงคำสั่งเดียว การสร้าง docker image จะสร้างจาก Dockerfile ซึ่ง ubuntu:20.04 มี Dockerfile ดังนี้

FROM scratch
ADD ubuntu-focal-oci-amd64-root.tar.gz /
CMD ["bash"]

แสดงว่า docker container ที่ใช้ ubuntu:20.04 (ตัวอื่น ๆ ก็ต้องตามไปดูกัน) ก็จะเรียก Bash เป็นคำสั่งสุดท้าย ก่อนจะยืนเป็น Service ให้ใช้งาน

ดังนั้น … เราก็ต้องไปทำให้ start cron service ใน .bashrc กัน

ประเด็นคือ มัน start ด้วย user ไหน ? ลองใช้คำสั่งนี้

docker exec -it kxtest bash -c "id"

uid=0(root) gid=0(root) groups=0(root)

ก็แสดงว่า สำหรับ image ตัวนี้ run ด้วย root นั่นเอง

วิธีแก้ไข ให้ cron ทำงานทุกครั้งที่ restart docker

เข้าไปใน docker container ด้วยคำสั่ง

docker exec -it kxtest bash

จากนั้น แก้ไขไฟล์ ~/.bashrc เพิ่มบรรทัดนี้ลงไปที่ท้ายไฟล์

systemctl start cron.service

save แล้วออกมาจาก container

แล้ว docker restart

docker restart kxtest

ดูผลลัพธ์

ดูว่า cron ทำงานเลยไม๊

docker exec -it kxtest bash -c "date ; systemctl status cron"
Sun Oct 10 03:01:17 UTC 2021
cron.service - Regular background program processing daemon
    Loaded: loaded (/usr/lib/systemd/system/cron.service, enabled)
    Active: active (running)

ดูว่า ผลการทำงานเป็นอย่างไร

docker exec -it kxtest bash -c "date ; cat ~/test.log"

Sun Oct 10 03:02:05 UTC 2021
20211010-015701
20211010-015801
20211010-015901
20211010-030101
20211010-030201

เย้ ทำงานแล้ว

UPDATE

สำหรับ กรณีใช้ docker image อย่าง JupyterHub หรือ มันก็คือ Jupyter Notebook แบบที่ทำงานได้พร้อมกันหลายคน (แยก Login) ผมลองแล้ว สามารถใช้วิธีนี้ครับ

อ้างอิงจาก Dockerfile ที่ https://hub.docker.com/r/jupyterhub/jupyterhub/dockerfile

จะเห็นได้ว่า CMD ตัวสุดท้ายคือ [“jupyterhub”]

เราสามารถไปสั่งให้ start cron ได้โดย

docker exec -it myjupyterhub bash
vi $(which jupyterhub)

แล้วแก้ไข Code เพิ่มนิดเดียว จากของเดิม

import re
import sys
from jupyterhub.app import main
if __name__ == '__main__':
    sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
    sys.exit(main())

เป็น

import re
import sys
from jupyterhub.app import main
if __name__ == '__main__':
    sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
    import subprocess
    subprocess.run(["service", "cron", "start"])
    sys.exit(main())

จากนั้น restart ได้เลยครับ

หวังว่าจะเป็นประโยชน์ครับ