วิธีรันโปรแกรมเป็น systemd service บน Linux VPS
เรียนรู้วิธีเขียน unit file เพื่อรันโปรแกรมบน VPS ให้ทำงานอัตโนมัติเมื่อบูตเครื่อง พร้อมวิธีตั้งค่า restart เมื่อโปรแกรมค้าง การจัดการ log และการจำกัดสิทธิ์เพื่อความปลอดภัย
systemd service คืออะไรและทำไมคุณจึงควรใช้งาน
systemd service คือไฟล์ข้อความขนาดเล็กที่ทำหน้าที่บอกเซิร์ฟเวอร์ของคุณว่าต้องรันโปรแกรมอย่างไร เช่น การเริ่มโปรแกรมตอนบูตเครื่อง การรีสตาร์ทโปรแกรมหากเกิดการขัดข้อง และการส่งข้อมูลเอาต์พุตไปยัง log ของระบบ นี่คือหน้าที่ทั้งหมดของมัน โปรแกรมที่คุณเริ่มด้วยตนเองในเซสชัน SSH จะหยุดทำงานทันทีที่คุณออกจากระบบหรือเซิร์ฟเวอร์รีบูต แต่โปรแกรมที่ถูกห่อหุ้มด้วย systemd service จะทำงานต่อไปได้เนื่องจากเซิร์ฟเวอร์เป็นเจ้าของกระบวนการนั้นแทนที่จะเป็นเชลล์ของคุณ
systemd คือระบบ init ที่ใช้ใน Ubuntu, Debian, Fedora และเซิร์ฟเวอร์ Linux สมัยใหม่ส่วนใหญ่ มันเป็นกระบวนการแรกที่เริ่มทำงานและเป็นผู้ดูแลกระบวนการอื่นๆ ทั้งหมด ซึ่งในอดีตไม่ได้เป็นเช่นนี้เสมอไป และ วิธีการที่ systemd เข้ามาแทนที่ init script แบบเดิม เป็นเรื่องที่น่าอ่านเมื่อคุณเข้าใจแล้วว่า unit file ทำงานอย่างไร เมื่อคุณเขียนไฟล์ service คุณกำลังส่งมอบโปรแกรมของคุณให้กับผู้ดูแลระบบนี้ คู่มือนี้จะแสดงให้เห็นถึง unit ที่เล็กที่สุดที่สามารถทำงานได้, ส่วนประกอบ 3 ส่วนที่ทุก unit ต้องมี, วิธีการเปิดใช้งานและอ่าน log, วิธีการรันโปรแกรมตามกำหนดเวลาด้วย timer และวิธีการจำกัดสิทธิ์เพื่อให้โปรแกรมทำงานด้วยสิทธิ์ที่น้อยที่สุดเท่าที่จะเป็นไปได้
บริการที่เล็กที่สุดที่ทำงานได้
ไฟล์ service จะถูกเก็บไว้ใน /etc/systemd/system/ โดยต้องลงท้ายด้วย .service และต้องการเพียงไม่กี่บรรทัดเท่านั้น ให้สร้างไฟล์สำหรับโปรแกรมที่ /usr/local/bin/myapp ดังนี้:
sudo nano /etc/systemd/system/myapp.service[Unit]
Description=My application
[Service]
ExecStart=/usr/local/bin/myapp
[Install]
WantedBy=multi-user.targetนี่คือ unit ที่สมบูรณ์และทำงานได้ ExecStart คือคำสั่งที่จะให้รัน WantedBy=multi-user.target หมายถึงให้เริ่มบริการนี้เมื่อเซิร์ฟเวอร์เข้าสู่สถานะการทำงานแบบหลายผู้ใช้ตามปกติ ซึ่งเป็นสิ่งที่ทำให้บริการเริ่มทำงานเมื่อบูตเครื่อง ส่วนที่เหลือทั้งหมดเป็นการปรับแต่งเพิ่มเติม
ส่วนประกอบทั้งสามส่วนและหน้าที่ของแต่ละส่วน
ไฟล์ unit แต่ละไฟล์จะถูกแบ่งออกเป็นส่วนต่างๆ โดยใช้เครื่องหมายวงเล็บก้ามปู บริการหนึ่งจะใช้ส่วนประกอบหลัก 3 ส่วน
[Unit] อธิบายรายละเอียดของบริการและความสัมพันธ์ของบริการนั้น บรรทัดที่คุณจะได้ใช้งานบ่อยที่สุดมี 2 บรรทัด:
[Unit]
Description=My application
After=network-online.target
Wants=network-online.targetDescription คือชื่อเรียกที่มนุษย์อ่านเข้าใจซึ่งคุณจะเห็นใน systemctl status ส่วน After=network-online.target เป็นการบอก systemd ว่าอย่าเพิ่งเริ่มโปรแกรมของคุณจนกว่าเครือข่ายจะพร้อมใช้งาน ซึ่งมีความสำคัญสำหรับทุกสิ่งที่ต้องผูกพอร์ต (bind port) หรือสร้างการเชื่อมต่อขาออก
[Service] คือวิธีการทำงานของโปรแกรม นี่คือส่วนที่คุณจะตั้งค่าส่วนใหญ่ไว้:
[Service]
ExecStart=/usr/local/bin/myapp --config /etc/myapp/config.toml
WorkingDirectory=/opt/myapp
User=myapp
Restart=on-failure
RestartSec=5
Environment=LOG_LEVEL=infoUser=myapp คือการรันโปรแกรมในฐานะบัญชีผู้ใช้ที่ไม่มีสิทธิ์พิเศษแทนการใช้ root ซึ่งถือเป็นบรรทัดที่สำคัญที่สุดเพียงบรรทัดเดียวเพื่อความปลอดภัย ในที่นี้ไม่มีบรรทัด Type= ดังนั้น systemd จึงใช้ค่าเริ่มต้นเป็น simple และถือว่ากระบวนการ ExecStart จะทำงานอยู่เบื้องหน้าเสมอ หากโปรแกรมมีการ fork ตัวเองไปทำงานเบื้องหลัง คุณจำเป็นต้องตั้งค่า Type= ให้เหมาะสมกับวิธีการเริ่มทำงาน มิฉะนั้น unit จะรายงานสถานะว่า active ทั้งที่ daemon จริงได้หยุดทำงานไปแล้ว ส่วน Restart=on-failure และ RestartSec=5 จะมีส่วนแยกต่างหากด้านล่าง เนื่องจากเป็นเหตุผลหลักที่คนส่วนใหญ่ต้องเขียน service ขึ้นมาใช้งาน
[Install] คือสิ่งที่เกิดขึ้นเมื่อคุณเปิดใช้งานบริการ:
[Install]
WantedBy=multi-user.targetWantedBy=multi-user.target คือสิ่งที่เชื่อมโยงบริการเข้ากับกระบวนการบูตเมื่อคุณรันคำสั่ง systemctl enable หากไม่มีส่วน [Install] บริการจะสามารถเริ่มทำงานด้วยตนเองได้ แต่จะไม่เริ่มทำงานโดยอัตโนมัติหลังจากรีบูตเครื่อง
การเปิดใช้งานและการตรวจสอบ
หลังจากเขียนหรือแก้ไขไฟล์ unit ใดๆ ให้ reload systemd เพื่อให้ระบบอ่านการเปลี่ยนแปลง จากนั้นเปิดใช้งานและเริ่ม service ในขั้นตอนเดียว:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp.servicedaemon-reload คือขั้นตอนที่ผู้ใช้มักลืม เนื่องจาก systemd จะทำแคชไฟล์ unit ไว้ การแก้ไขจะไม่มีผลจนกว่าคุณจะ reload ส่วน enable --now จะทำหน้าที่ทั้งเปิดใช้งาน service สำหรับการบูตเครื่องและเริ่มทำงานทันที ตรวจสอบสถานะด้วย:
sudo systemctl status myapp.service* myapp.service - My application
Loaded: loaded (/etc/systemd/system/myapp.service; enabled)
Active: active (running) since Wed 2026-07-15 22:40:11 UTC; 3s ago
Main PID: 4123 (myapp)Active: active (running) และ enabled คือสถานะที่คุณต้องการ หากต้องการอ่าน output ของโปรแกรม ให้เรียกดู journal เฉพาะ unit นี้:
sudo journalctl -u myapp.service -f-f จะแสดงบรรทัดใหม่ที่เข้ามาแบบเรียลไทม์ เหมือนกับการใช้ tail -f ข้อมูลใดก็ตามที่โปรแกรมของคุณเขียนไปยัง standard output หรือ standard error จะปรากฏที่นี่โดยที่คุณไม่ต้องตั้งค่าการบันทึก log เพิ่มเติม
การเริ่มการทำงานใหม่เมื่อเกิดความล้มเหลว ซึ่งเป็นเหตุผลที่คุณมาที่นี่
ผลประโยชน์หลักของการทำ service คือ systemd จะเริ่มโปรแกรมของคุณใหม่เมื่อโปรแกรมหยุดทำงาน โดยใช้เพียงสองบรรทัดนี้:
[Service]
Restart=on-failure
RestartSec=5Restart=on-failure จะเริ่มโปรแกรมใหม่เมื่อโปรแกรมออกจากระบบด้วยรหัสที่ไม่ใช่ศูนย์ หรือหยุดทำงานจากสัญญาณ crash เช่น SIGKILL หรือ SIGSEGV หากเป็นการออกจากระบบโดยปกติ หรือการหยุดด้วยสัญญาณ SIGTERM, SIGINT, SIGHUP หรือ SIGPIPE จะไม่ทำให้เกิดการเริ่มใหม่ RestartSec=5 จะรอเป็นเวลาห้าวินาทีระหว่างการพยายามแต่ละครั้ง เพื่อป้องกันไม่ให้โปรแกรมที่ crash ทันทีทำงานวนซ้ำอย่างรวดเร็ว คุณสามารถตรวจสอบได้โดยการสั่ง kill process แล้วดูว่า systemd นำโปรแกรมกลับมาทำงานใหม่หรือไม่ ให้ใช้ SIGKILL เนื่องจาก SIGTERM ซึ่งเป็นค่าเริ่มต้นจะถือเป็นการหยุดทำงานปกติ ดังนั้น on-failure จะไม่ทำให้ service เริ่มทำงานใหม่:
sudo systemctl kill -s SIGKILL myapp.service
sudo systemctl status myapp.serviceภายในห้าวินาที สถานะจะแสดง Main PID ใหม่ และ active (running) อีกครั้ง นี่คือฟีเจอร์ทั้งหมด และเป็นเหตุผลว่าทำไมการใช้ service จึงดีกว่าการปล่อยให้โปรแกรมทำงานใน tmux หรือ screen
เรียกใช้งานด้วยผู้ใช้ที่ไม่มีสิทธิ์และเพิ่มความปลอดภัย
บริการที่ทำงานด้วยสิทธิ์ root สามารถทำทุกอย่างบนเซิร์ฟเวอร์ของคุณได้หากโปรแกรมถูกโจมตี ให้เรียกใช้งานด้วยผู้ใช้เฉพาะของบริการนั้น และกำหนดคำสั่งให้ systemd เพื่อจำกัดขอบเขตการทำงาน ขั้นแรกให้สร้างบัญชีผู้ใช้ระบบที่ล็อกอินไม่ได้และไม่มีไดเรกทอรี home:
sudo useradd --system --no-create-home --shell /usr/sbin/nologin myappจากนั้นตั้งค่า User=myapp และเพิ่มบรรทัดการเพิ่มความปลอดภัยลงใน [Service]:
[Service]
User=myapp
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=trueแต่ละบรรทัดจะตัดสิทธิ์การเข้าถึงในส่วนที่โปรแกรมไม่จำเป็นต้องใช้ NoNewPrivileges=true ป้องกันไม่ให้กระบวนการได้รับสิทธิ์ใหม่ แม้จะผ่านไฟล์ binary ที่เป็น setuid ก็ตาม PrivateTmp=true สร้าง /tmp ส่วนตัวที่กระบวนการอื่นมองไม่เห็น ProtectSystem=strict ทำให้ระบบไฟล์ทั้งหมดเป็นแบบอ่านอย่างเดียว ยกเว้นบาง path ที่คุณระบุด้วย ReadWritePaths= ส่วน ProtectHome=true จะซ่อน /home จากบริการนั้นโดยสิ้นเชิง นี่คือแนวคิดเรื่องการให้สิทธิ์น้อยที่สุด (least-privilege) เช่นเดียวกับการวางบริการไว้หลังไฟร์วอลล์ คือให้สิทธิ์เฉพาะสิ่งที่จำเป็นเท่านั้น หากคุณได้อ่านคู่มือเรื่อง การปิดช่องโหว่ไฟร์วอลล์ IPv6 บน VPS นี่คือส่วนของการจัดการบนโฮสต์ที่มีแนวคิดเดียวกัน สำหรับบริการที่เชื่อมต่อกับอินเทอร์เน็ต ให้ใช้การเพิ่มความปลอดภัยนี้ควบคู่ไปกับ Fail2ban หน้า SSH และไฟร์วอลล์ที่มีนโยบายปฏิเสธการเชื่อมต่อเป็นค่าเริ่มต้น
แทนที่จะพิมพ์คำสั่งทั้งหมดด้วยตนเองและอาจจำคำสั่งผิด ให้สร้าง unit ที่ผ่านการเพิ่มความปลอดภัยแล้วและคัดลอกไปใช้งาน:
Timers: ทางเลือกใหม่แทน cron
systemd timer ใช้สำหรับรัน service ตามกำหนดเวลา ซึ่งเป็นสิ่งที่เข้ามาแทนที่ cron job ในปัจจุบัน timer ประกอบด้วยไฟล์ 2 ส่วน ได้แก่ .service ซึ่งทำหน้าที่ประมวลผลงาน และ .timer ซึ่งกำหนดเวลาการทำงาน สมมติว่าคุณต้องการสำรองข้อมูลทุกวันตอนตี 3 ตัว service จะทำหน้าที่รันงานหนึ่งครั้งแล้วจบการทำงานไป:
# /etc/systemd/system/backup.service
[Unit]
Description=Nightly backup
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.shType=oneshot เป็นการบอก systemd ว่าโปรแกรมนี้รันแล้วจบการทำงาน ไม่ได้ทำงานค้างไว้ในหน่วยความจำ ส่วน timer จะทำหน้าที่กำหนดตารางเวลา:
# /etc/systemd/system/backup.timer
[Unit]
Description=Run the nightly backup
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
[Install]
WantedBy=timers.targetOnCalendar=*-*-* 03:00:00 หมายถึงเวลาตี 3 ของทุกวัน คุณสามารถทดสอบนิพจน์เวลา (calendar expression) ได้ด้วย systemd-analyze calendar "*-*-* 03:00:00" ซึ่งจะช่วยยืนยันว่าระบบอ่านค่าได้ถูกต้องและแสดงเวลาถัดไปที่จะเริ่มทำงาน Persistent=true จะรันงานที่พลาดไปทันทีเมื่อเซิร์ฟเวอร์กลับมาออนไลน์ หากเซิร์ฟเวอร์ปิดอยู่ตอนตี 3 ซึ่งเป็นสิ่งที่ cron ทำไม่ได้ โปรดสังเกตว่าการเปิดใช้งาน timer ต้องทำผ่าน timers.target ไม่ใช่ multi-user.target โดยให้เปิดใช้งานที่ตัว timer ไม่ใช่ตัว service:
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
systemctl list-timerslist-timers จะแสดง timer ทุกรายการพร้อมเวลาที่รันครั้งล่าสุดและครั้งถัดไป ทำให้คุณเห็นภาพรวมได้ทันทีว่างานของคุณจะรันอีกครั้งเมื่อใด ตัว generator ด้านบนจะสร้างไฟล์ .service และ .timer ที่จับคู่กันให้โดยอัตโนมัติเมื่อคุณเปิดโหมด timer เมื่อเทียบกับ cron แล้ว timer ให้ประโยชน์มากกว่าในแง่ของการบันทึก log ลงใน journal ได้จริง, สามารถใช้คำสั่ง hardening เดียวกับ service อื่นๆ ได้ และมีฟีเจอร์ชดเชยงานที่พลาดไปผ่าน Persistent=true แม้ว่า cron จะยังคงใช้งานได้ดีสำหรับงานง่ายๆ แต่ timer เป็นเครื่องมือที่ดีกว่าเมื่อเป็นงานที่มีความสำคัญ
FAQ
systemd service และ cron job แตกต่างกันอย่างไร
service มีไว้สำหรับโปรแกรมที่ทำงานต่อเนื่อง โดยจะเริ่มทำงานตอนบูตเครื่อง เริ่มใหม่หากเกิดข้อผิดพลาด และบันทึก log ลงใน journal ส่วน cron job มีไว้สำหรับรันคำสั่งสั้นๆ ตามกำหนดเวลาแล้วจบการทำงาน หากคุณต้องการการตั้งเวลาแต่ยังต้องการ log, การรักษาความปลอดภัย และการรันงานย้อนหลังเมื่อพลาดช่วงเวลาที่กำหนด ให้ใช้ systemd timer ซึ่งเป็นการจับคู่ตารางเวลา .timer เข้ากับ service oneshot และสามารถใช้แทน cron ในงานส่วนใหญ่บนเซิร์ฟเวอร์ได้
ฉันควรวางไฟล์ systemd service ไว้ที่ไหน
ให้วาง unit ของคุณไว้ใน /etc/systemd/system/ โดยตั้งชื่อไฟล์ให้ลงท้ายด้วย .service ไดเรกทอรีนี้มีไว้สำหรับ unit ที่ผู้ดูแลระบบเพิ่มเข้าไปเอง และจะมีลำดับความสำคัญสูงกว่า unit ที่มากับแพ็กเกจใน /lib/systemd/system/ หลังจากสร้างหรือแก้ไขไฟล์แล้ว ให้รัน sudo systemctl daemon-reload เพื่อให้ systemd รับทราบการเปลี่ยนแปลง
ฉันจะทำให้ service เริ่มทำงานใหม่ได้อย่างไรหากเกิดการ crash
ให้เพิ่ม Restart=on-failure และ RestartSec=5 ลงในส่วน [Service] จากนั้นรัน sudo systemctl daemon-reload และรีสตาร์ท service นั้น systemd จะเริ่มโปรแกรมใหม่เมื่อโปรแกรมจบการทำงานด้วย exit code ที่ไม่ใช่ศูนย์ หรือหยุดทำงานจากสัญญาณ crash โดยจะเว้นระยะห่าง 5 วินาทีก่อนลองใหม่ ทดสอบด้วย sudo systemctl kill -s SIGKILL myapp.service โดย SIGTERM ซึ่งเป็นสัญญาณเริ่มต้นจะถือเป็นการหยุดที่สะอาดและไม่กระตุ้น on-failure จากนั้นให้สังเกต systemctl status ว่ามีการแสดง PID ใหม่ภายในไม่กี่วินาที
ฉันจะรัน systemd service ด้วยสิทธิ์ผู้ใช้ที่ไม่ใช่ root ได้อย่างไร
สร้างบัญชีผู้ใช้สำหรับระบบด้วย sudo useradd --system --no-create-home --shell /usr/sbin/nologin myapp จากนั้นเพิ่ม User=myapp ลงในส่วน [Service] และเพิ่ม NoNewPrivileges=true, PrivateTmp=true และ ProtectSystem=strict เพื่อให้กระบวนการทำงานด้วยสิทธิ์ที่จำกัดเท่าที่จำเป็น การรันด้วยผู้ใช้ที่ไม่มีสิทธิ์สูงถือเป็นการเปลี่ยนแปลงที่สำคัญที่สุดที่คุณสามารถทำได้เพื่อความปลอดภัยของ service
ทำไม service ของฉันถึงเริ่มทำงานไม่ได้
ให้รัน systemctl status myapp.service เพื่อดูสรุปสถานะ และ journalctl -u myapp.service เพื่อดูผลลัพธ์ทั้งหมด สาเหตุที่พบบ่อยที่สุดคือการระบุ path ผิดใน ExecStart, การขาด WorkingDirectory, ข้อผิดพลาดด้านสิทธิ์เนื่องจาก User= ไม่สามารถอ่านไฟล์ได้ หรือลืมรัน sudo systemctl daemon-reload หลังจากแก้ไขไฟล์ journal จะแสดงข้อความผิดพลาดของโปรแกรมโดยตรง ซึ่งมักจะระบุปัญหาที่เกิดขึ้นได้ทันที