วิธีเขียน systemd Unit File: คู่มือฉบับนักพัฒนา
เรียนรู้การทำงานของ systemd .service, .timer และ .socket unit file — และสร้าง unit พร้อมใช้งานด้วย Systemd Unit Generator โดยไม่ต้องจำ syntax
Table of Contents
ถ้าคุณเคย deploy application ขึ้น Linux server คุณต้องเจอ systemd มาแล้วแน่นอน เพราะมันคือ process แรกที่ถูก start บน Linux distribution สมัยใหม่แทบทุกตัว และทำหน้าที่ launch, supervise และ restart ทุกอย่างที่เหลือบนเครื่อง วิธีบอก systemd ให้รัน app ของคุณคือการเขียน unit file — ไฟล์ config ขนาดเล็กรูปแบบ INI ที่มีนามสกุล .service, .timer หรือ .socket
ปัญหาคือ unit file มี directive เยอะมาก ตั้งแต่ Type=, Restart=, OnCalendar=, การจัดการ dependency ไปจนถึง socket activation ทำให้ง่ายที่จะลืม directive หรือก๊อปตัวอย่างเก่าๆ จาก blog ดังๆ ที่อาจล้าสมัยแล้ว เราจึงสร้าง Systemd Unit Generator — เครื่องมือฟรีที่รันใน browser แปลง form ง่ายๆ ให้กลายเป็น unit file พร้อม production ที่เอาไปวางที่ /etc/systemd/system/ ได้เลย
ในคู่มือนี้ คุณจะได้เรียนรู้โครงสร้างของ unit file ว่า directive สำคัญแต่ละตัวทำอะไร และวิธีใช้ generator สร้าง .service, .timer และ .socket โดยไม่ต้องจำ syntax แม้แต่บรรทัดเดียว
ทำไม่ต้องใช้ Systemd Unit Generator
- ไม่ต้องจำ syntax — systemd มี directive เป็นร้อยตัว แต่ generator เอามาเฉพาะตัวที่ใช้จริงในงาน deploy ประจำวัน แต่ละตัวมี field กำกับชัดเจน คุณแค่กรอก form แทันที่จะต้องเปิด man systemd.service
- ครบทั้ง 3 unit type ในที่เดียว — สลับสร้าง .service, .timer และ .socket ได้โดยไม่ต้องออกจากหน้าเว็บ field ของ timer อย่าง OnCalendar, OnBootSec, AccuracySec และ Persistent จะโผล่เฉพาะตอนที่เกี่ยวข้องเท่านั้น
- ถูกต้องตั้งแต่โครงสร้าง — value ถูก escape และ section ถูกเรียงลำดับถูกต้อง ExecStart ที่พิมพ์ผิดหรือตัวอักษรแปลกปลอมจะไม่ทำให้ unit file เพี้ยนแบบเงียบๆ
- ค่า default ที่ปลอดภัย — generator เริ่มจากค่า production ที่ดี เช่น Type=simple, Restart=on-failure, After=network.target, WantedBy=multi-user.target ซึ่งคุณปรับแต่งต่อ field ได้
- รันใน browser ล้วนๆ — คำสั่ง service, path หรือ environment variable ของคุณไม่ถูกส่งขึ้น server ไหนเลย unit file ถูก generate ฝั่ง client และ export ในเครื่องคุณ
- เรียนรู้ไปพร้อมกัน — เพราะทุก field map กับ directive หนึ่งตัว การใช้ generator ก็คือวิธีฝึกจำว่าแต่ละบรรทัดของ unit file ทำอะไรจริงๆ
ฟีเจอร์เด่น
| ฟีเจอร์ | ทำอะไร |
|---|---|
| Unit types | สร้างไฟล์ .service, .timer หรือ .socket จาก form เดียวกัน |
| Service types | simple, exec, forking, oneshot, dbus, notify, notify-reload, idle |
| Restart policies | no, on-success, on-failure, on-abnormal, on-abort, always |
| Lifecycle commands | ExecStart, ExecStartPre, ExecStop, ExecReload |
| ควบคุม dependency | After=, Wants= และ Requires= สำหรับ ordering และการผูก unit เข้าด้วยกัน |
| ความปลอดภัยตอนรัน | กำหนด User=, Group=, WorkingDirectory= และ Environment= ได้หลายบรรทัด |
| ตั้งเวลา timer | OnCalendar, OnBootSec, AccuracySec และ Persistent สำหรับ catch-up |
| Socket activation | ListenStream= สำหรับ startup แบบ on-demand สไตล์ inetd |
- ทุก field เป็น optional ยกเว้นที่ systemd ต้องการจริง งานเล็กใช้เวลาไม่กี่วินาที งานที่ต้อง harden ก็ใช้แค่ไม่กี่นาที
- ผลลัพธ์คือไฟล์เดียวพร้อมใช้ — ก๊อปเลย หรือ download ไปวางที่ /etc/systemd/system/your-unit.service ได้ทันที
วิธีใช้งาน
- เปิด Systemd Unit Generator แล้วเลือก unit type: service, timer หรือ socket
- กรอกข้อมูลพื้นฐาน: description, คำสั่ง ExecStart และ user กับ working directory ที่ service ควรรัน
- ปรับพฤติกรรม: เลือก Type=, เลือก restart policy, ใส่ environment variables และตั้ง dependency เช่น After=network.target
- สำหรับ timer ตั้งเวลาด้วย OnCalendar (หรือ OnBootSec ถ้าอิงจากเวลา boot) และเปิด Persistent เพื่อให้ run ที่ค้างไว้ถูกชดเชยหลัง reboot — สำหรับ socket ให้ตั้งค่า ListenStream
- ตรวจ unit ที่ generate แล้วใน output panel แล้วก๊อปหรือ download ไฟล์ไปที่ /etc/systemd/system/ และรัน sudo systemctl daemon-reload
เข้าใจ systemd Unit File
สาม section ของ service unit
ไฟล์ .service ถูกจัดระเบียบเป็น section ตัวอย่างที่เล็กแต่สมจริงหน้าตาแบบนี้:
[Unit] Description=My Application Service After=network.target [Service] Type=simple User=myapp Group=myapp WorkingDirectory=/opt/myapp Environment=NODE_ENV=production Environment=PORT=3000 ExecStart=/usr/bin/node /opt/myapp/server.js Restart=on-failure [Install] WantedBy=multi-user.target
- [Unit] เก็บ metadata และการจัดลำดับ Description= คือข้อความที่เห็นใน systemctl status ส่วน After=, Wants= และ Requires= คุม ordering และการผูกพันกับ unit อื่น Requires= เป็นแบบเข้ม — ถ้า unit ที่ระบุล่ม unit ของคุณจะลงไปด้วย
- [Service] กำหนดว่า process รันอย่างไร: type, user, environment และคำสั่งสำหรับ start, stop และ reload
- [Install] ตัดสินว่าเกิดอะไรขึ้นเมื่อคุณ enable unit — WantedBy=multi-user.target คือตัวเลือกมาตรฐานสำหรับ boot ปกติ มันสร้าง symlink ที่ทำให้ service ของคุณ start ตอนบูตเครื่อง
การเลือก Type=
Type= บอก systemd ว่า main process ส่งสัญญาณว่า "พร้อม" อย่างไร:
- simple — ค่า default; process ที่ ExecStart รันคือ main process เหมาะกับ app ส่วนใหญ่ที่รันอยู่ใน foreground
- exec — เหมือน simple แต่ systemd รอให้ binary ถูก execute จริงก่อนจะถือว่า unit เริ่มแล้ว
- forking — สำหรับ daemon แบบดั้งเดิมที่ fork ไปทำงานเบื้องหลัง ใช้คู่กับ PIDFile= ถ้า daemon เขียนไฟล์นั้น
- oneshot — รันครั้งเดียวแล้วออก เหมาะกับ setup script และงาน backup ควรจับคู่กับ .timer แทันที่จะ enable ตรงๆ
- notify — service เรียก sd_notify() เมื่อพร้อมจริง ต้องให้ process รองรับ (app ที่รู้จัก systemd, server สมัยใหม่บางตัว)
- dbus — สัญญาณว่าพร้อมโดยการยึดชื่อ D-Bus
- idle — ดองการรันไว้จนกว่า job อื่นจะถูกจัดสรรหมด เป็นการจัดลำดับเชิงหน้าตามากกว่า
Restart policy อธิบาย
Restart= คุมว่า systemd จะทำอะไรเมื่อ process ออก: no ไม่ restart เด็ดขาด, on-failure restart เมื่อ exit code ไม่เป็นศูนย์, on-abnormal ครอบคลุม signal และ timeout, on-abort เฉพาะ signal ที่ไม่สะอาด, on-success restart เฉพาะเมื่อ exit code เป็น 0 และ always restart ไม่ว่าอย่างไร สำหรับ app ที่รันยาวนาน on-failure คู่กับ rate limiting คือทางเลือกที่ practical — ฟื้นจาก crash ได้โดยไม่บัง error ด้าน config ที่หลุดสายตา
Timer: OnCalendar และเพื่อน
unit แบบ .timer จะ trigger .service ตามตารางเวลา service คู่หูมักเป็น Type=oneshot:
[Unit] Description=Run database backup daily [Timer] OnCalendar=*-*-* 03:00:00 Persistent=true [Install] WantedBy=timers.target
OnCalendar ใช้รูปแบบปฏิทินของ systemd เอง: *-*-* 03:00:00 แปลว่าทุกวันเวลา 03:00 น. เขียน Mon..Fri 09:00 ได้สำหรับเช้าวันจันทร์ถึงศุกร์ หรือ Sun *-*-* 02:00:00 สำหรับงานรายสัปดาห์วันอาทิตย์ OnBootSec=5min ตั้งเวลาแบบอิงจากตอน boot แทน AccuracySec จับกลุ่มเวลายิงเพื่อประหยัดการปลุกเครื่อง (default 1 นาที) และ Persistent=true คือฟีเจอร์เด็ด — ถ้าเครื่องดับอยู่ตอนถึงเวลา timer จะยิงทันทีที่เครื่อง boot กลับมา
Socket activation
unit แบบ .socket จะ listen บน port หรือ socket และ start .service ที่ผูกอยู่ด้วยเฉพาะเมื่อมี connection แรกเข้ามา:
[Unit] Description=Socket for myapp [Socket] ListenStream=8080 [Install] WantedBy=sockets.target
service ของคุณจะได้รับ socket ที่ listen อยู่แล้วแทันที่จะต้อง bind เอง ประโยชน์คือ: ไม่ใช้ทรัพยากรตอน idle เลย, restart ได้เนียนๆ โดยไม่ทำ connection หล่น และ systemd เป็นคนจัดสรร port ให้
Use Case ในทางปฏิบัติ
รัน Node.js app บน production
เคสคลาสสิก: web app ที่ต้องอยู่รอดจากทั้ง reboot และ crash สร้าง .service ด้วย Type=simple, ExecStart=/usr/bin/node /opt/myapp/server.js, user myapp เฉพาะทาง, NODE_ENV=production ใน environment และ Restart=on-failure แล้ว enable ครั้งเดียวด้วย systemctl enable --now myapp แล้วปล่อยให้ systemd supervise มันตลอดไป
Backup database ตามตารางโดยไม่ต้องใช้ cron
เอา pg_dump ไปไว้ใน service แบบ Type=oneshot แล้วจับคู่กับ .timer ที่ใช้ OnCalendar=*-*-* 03:00:00 กับ Persistent=true เทียบกับ crontab entry คุณได้ journal log ทุกครั้งที่รัน, failure story ที่สะอาด และการชดเชยที่นการันตีหลังเซิร์ฟเวอร์ดับค้างคืน
API ภายในแบบ socket-activated
สำหรับ service ภายในที่ใช้นานๆ ครั้ง สร้าง .socket ด้วย ListenStream=8080 service จะอยู่ในสถานะ stopped จนกว่าจะมี request แรกมาที่ port นั้น systemd ค่อย start ตาม demand บนเครื่องที่มี service เยอะๆ socket activation ช่วยให้ memory usage ของตัวที่ idle ใกล้ศูนย์
Migration แบบ one-shot ตอน boot
สร้าง service Type=oneshot ที่มี ExecStartPre รอ database (After=network.target postgresql.service) และ ExecStart รัน migration script แล้ว enable ไว้ ทุก instance ใหม่จะรัน migration ครั้งเดียวตอน boot ก่อนที่ app service ของคุณจะ start
Best Practices
- รันด้วย user เฉพาะทาง — อย่ารัน service ด้วย root เด็ดขาด สร้าง system user แล้วตั้ง User=/Group= เพื่อให้ process ที่ถูก breach เข้าถึงได้น้อยที่สุด
- ตั้ง Restart=on-failure พร้อม rate limiting — StartLimitIntervalSec ค่า default ของ systemd กัน restart storm เก็บมันไว้ และเลือก on-failure มากกว่า always เพื่อให้ error ด้าน config โผล่มาแทันที่จะวนลูป
- อย่าลืม daemon-reload หลังแก้ไฟล์ — รัน sudo systemctl daemon-reload หลังแก้ unit file ทุกครั้ง แล้วค่อย restart unit การลืมขั้นตอนนี้คือสาเหตุอันดับหนึ่งของ "แก้แล้วไม่เปลี่ยน"
- อ่าน log ด้วย journalctl — journalctl -u myapp -f ดู log ของ service แบบ real-time ไม่ต้องมี log file ไม่ต้องตั้ง logrotate ทุกอย่างลง journal อัตโนมัติ
- ใช้ WantedBy=multi-user.target — เป็นการ start unit ตอน boot ปกติโดยไม่ดึง graphical target มาด้วย ส่วน timer ควรใช้ WantedBy=timers.target แทน
- เลือก systemd timer มากกว่า cron — timer ให้ logging, dependency, การ catch-up ด้วย Persistent และ RandomizedDelaySec ซึ่ง crontab เพียวๆ ไม่มี
เริ่มสร้าง Unit File แรกของคุณวันนี้
ตอนนี้คุณรู้โครงสร้างของ unit file แล้ว: section, type, restart policy และวิธีที่ timer กับ socket ต่อยอดโมเดลนี้ วิธีเร็วที่สุดที่จะเปลี่ยนความรู้นั้นให้เป็น service ที่รันอยู่จริงคือลอง Systemd Unit Generator — กรอก form ดู unit file สร้างตัวเอง แล้ว deploy ขึ้น server ได้ในไม่ถึงนาที ไม่ต้องเปิด man page สักหน้า
เครื่องมือที่เกี่ยวข้อง:
- Cron Expression Builder — สร้างและถอดรหัส cron expression แบบ visual สำหรับตอนที่คุณต้องใช้ crontab แบบคลาสสกจิริงๆ
- Cron Parser — วาง cron expression ใดก็ได้แล้วดูคำอธิบายง่ายๆ ว่ามันยิงเมื่อไหร่
- SSH Config Generator — สร้าง ~/.ssh/config ที่เรียบร้อยพร้อม alias และ jump host สำหรับ server ที่คุณ deploy
ขอให้สนุกกับการ deploy!