Cách tạo systemd service chạy trên VPS
Hướng dẫn viết file unit để chạy chương trình tự động khi boot, tự restart khi crash và quản lý log qua journald trên VPS Linux cực kỳ đơn giản.
systemd service là gì, và tại sao bạn cần nó
Một systemd service là một file text nhỏ dùng để chỉ dẫn server cách chạy một chương trình: khởi động khi boot, tự khởi động lại nếu bị crash, và gửi output vào system log. Chỉ vậy thôi. Một chương trình bạn chạy thủ công trong phiên SSH sẽ chết ngay khi bạn logout hoặc server reboot. Một chương trình được bao bọc trong một systemd service sẽ tiếp tục chạy, vì server sẽ quản lý nó thay vì shell của bạn.
systemd là init system trên Ubuntu, Debian, Fedora, và hầu hết các server Linux hiện đại. Nó là process đầu tiên được khởi chạy và là thực thể giám sát mọi thứ khác. Khi bạn viết một service file, bạn đang giao chương trình của mình cho bộ giám sát đó. Hướng dẫn này sẽ trình bày một unit tối giản nhất, ba section mà mọi unit đều có, cách bật và đọc log, cách chạy theo lịch trình bằng timer, và cách thắt chặt bảo mật để nó chạy với đặc quyền thấp nhất có thể.
Unit tối giản nhất để hoạt động
Một service file nằm trong /etc/systemd/system/, kết thúc bằng đuôi .service, và chỉ cần vài dòng. Hãy tạo một file cho chương trình tại /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Đây là một unit hoàn chỉnh và hoạt động được. ExecStart là lệnh cần chạy. WantedBy=multi-user.target nghĩa là bắt đầu chạy khi server đạt đến trạng thái multi-user bình thường, đây là lý do nó tự chạy khi boot. Mọi thứ khác chỉ là tinh chỉnh.
Ba section, và công dụng của từng cái
Mỗi unit file được chia thành các section nằm trong dấu ngoặc vuông. Một service sử dụng ba section.
[Unit] mô tả service và các mối quan hệ của nó. Hai dòng bạn sẽ dùng nhiều nhất:
[Unit]
Description=My application
After=network-online.target
Wants=network-online.targetDescription là nhãn hiển thị cho người dùng trong systemctl status. After=network-online.target yêu cầu systemd không khởi động chương trình cho đến khi network đã sẵn sàng, điều này quan trọng với bất kỳ thứ gì cần bind port hoặc tạo kết nối outbound.
[Service] là cách chương trình chạy. Đây là nơi chứa hầu hết các thiết lập:
[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 chạy chương trình dưới quyền một account không có đặc quyền thay vì root, đây là dòng quan trọng nhất để đảm bảo an toàn. Restart=on-failure và RestartSec=5 có section riêng ở bên dưới, vì chúng là lý do chính khiến mọi người viết service.
[Install] là những gì xảy ra khi bạn enable service:
[Install]
WantedBy=multi-user.targetWantedBy=multi-user.target là thứ liên kết service vào quá trình boot khi bạn chạy systemctl enable. Nếu không có section [Install], service có thể được chạy thủ công nhưng sẽ không tự khởi động lại sau khi reboot.
Bật lên và theo dõi
Sau khi viết hoặc chỉnh sửa bất kỳ unit file nào, hãy reload systemd để nó đọc thay đổi, sau đó enable và start service trong một bước:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp.servicedaemon-reload là bước mà mọi người hay quên: systemd cache các unit file, nên việc chỉnh sửa sẽ không có tác dụng cho đến khi bạn reload. enable --now vừa enable service cho quá trình boot, vừa start nó ngay lập tức. Kiểm tra bằng cách:
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) và enabled là những gì bạn muốn thấy. Để đọc output của chương trình, hãy yêu cầu journal chỉ hiển thị unit này:
sudo journalctl -u myapp.service -f-f sẽ theo dõi các dòng mới khi chúng xuất hiện, giống như tail -f. Bất kỳ thứ gì chương trình ghi ra standard output hoặc standard error sẽ được đưa vào đây mà bạn không cần thiết lập logging gì thêm.
Tự động restart khi lỗi, lý do bạn tìm đến đây
Lợi ích chính của một service là systemd sẽ restart chương trình khi nó chết. Hai dòng sau sẽ thực hiện việc này:
[Service]
Restart=on-failure
RestartSec=5Restart=on-failure restart chương trình khi nó thoát với exit code khác 0 hoặc chết do crash signal như SIGKILL hoặc SIGSEGV. Một lần thoát sạch (clean exit), hoặc bị dừng bởi SIGTERM, SIGINT, SIGHUP, hoặc SIGPIPE, sẽ không kích hoạt nó. RestartSec=5 đợi 5 giây giữa các lần thử, để một chương trình bị crash liên tục không chạy vòng lặp vô tận. Hãy xác nhận bằng cách kill process và quan sát systemd khôi phục nó. Sử dụng SIGKILL: mặc định SIGTERM được coi là dừng sạch, nên on-failure sẽ không restart service:
sudo systemctl kill -s SIGKILL myapp.service
sudo systemctl status myapp.serviceTrong vòng 5 giây, status sẽ hiển thị Main PID và active (running) mới. Đó là toàn bộ tính năng này, và đó là lý do tại sao dùng service tốt hơn là để một chương trình chạy trong tmux hoặc screen.
Chạy với user không có đặc quyền và thắt chặt bảo mật
Một service chạy dưới quyền root có thể làm bất cứ điều gì với server nếu chương trình bị khai thác. Hãy chạy nó với user riêng, và cung cấp cho systemd một vài directive để giới hạn nó. Đầu tiên, tạo một system account không có login và không có home:
sudo useradd --system --no-create-home --shell /usr/sbin/nologin myappSau đó thiết lập User=myapp và thêm các dòng hardening vào [Service]:
[Service]
User=myapp
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=trueMỗi dòng sẽ loại bỏ một thứ mà chương trình không thực sự cần. NoNewPrivileges=true ngăn process không bao giờ lấy thêm đặc quyền mới, ngay cả thông qua một binary setuid. PrivateTmp=true cấp cho nó một /tmp riêng tư mà không process nào khác có thể thấy. ProtectSystem=strict biến toàn bộ filesystem thành read-only ngoại trừ một vài path bạn chỉ định bằng ReadWritePaths=. ProtectHome=true ẩn hoàn toàn /home khỏi nó. Đây chính là tư duy "least-privilege" (đặc quyền tối thiểu) giống như việc đặt một service sau firewall: chỉ cấp cho nó những gì nó cần. Nếu bạn đã đọc hướng dẫn về đóng lỗ hổng IPv6 firewall trên VPS, thì đây là phần thực hiện trên host của cùng một ý tưởng đó. Đối với một service đối mặt với internet, hãy kết hợp hardening này với Fail2ban trước SSH và một firewall mặc định là deny.
Thay vì gõ tay tất cả những thứ này và có thể nhớ sai directive, hãy generate một unit hoàn chỉnh đã được hardening và copy nó:
Timer: cron hiện đại
Một systemd timer chạy một service theo lịch trình, và nó là sự thay thế hiện đại cho cron job. Một timer gồm hai file: một .service thực hiện công việc, và một .timer nói rõ thời điểm chạy. Giả sử bạn muốn backup vào 3 giờ sáng mỗi ngày. Service sẽ thực hiện tác vụ một lần rồi thoát:
# /etc/systemd/system/backup.service
[Unit]
Description=Nightly backup
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.shType=oneshot báo cho systemd biết chương trình chạy xong rồi thoát, chứ không phải chạy ngầm liên tục. Timer sẽ lập lịch cho nó:
# /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 nghĩa là 3 giờ sáng mỗi ngày. Kiểm tra bất kỳ biểu thức lịch nào bằng systemd-analyze calendar "*-*-* 03:00:00", nó sẽ xác nhận việc parse và in ra các lần chạy tiếp theo. Persistent=true sẽ chạy một job bị lỡ ngay khi server hoạt động trở lại nếu nó bị tắt lúc 3 giờ sáng, điều mà cron không làm được. Lưu ý rằng timer được enable thông qua timers.target, chứ không phải multi-user.target. Hãy enable timer, đừng enable service:
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
systemctl list-timerslist-timers hiển thị mọi timer cùng với lần chạy tiếp theo và lần chạy cuối, giúp bạn biết ngay khi nào job tiếp theo sẽ chạy. Generator ở trên sẽ tự tạo cặp .service và .timer cho bạn khi bạn bật timer mode. So với một dòng cron, timer cung cấp log thực trong journal, các directive hardening giống như bất kỳ service nào, và khả năng chạy bù cho các job bị lỡ mà Persistent=true cung cấp. Cron vẫn ổn cho các tác vụ đơn giản; timer là công cụ tốt hơn khi tác vụ đó quan trọng.
FAQ
Sự khác biệt giữa systemd service và cron job là gì?
Một service giữ cho một chương trình chạy lâu dài luôn sống: nó khởi động khi boot, restart khi lỗi, và log vào journal. Một cron job chạy một lệnh ngắn theo lịch trình rồi thoát. Khi bạn cần lập lịch nhưng cũng muốn có journal logs, hardening, và khả năng chạy bù cho các lần bị lỡ, hãy dùng systemd timer, nó kết hợp lịch trình .timer với một service oneshot và thay thế cron cho hầu hết các tác vụ server.
Tôi nên đặt file systemd service ở đâu?
Hãy đặt các unit của riêng bạn vào /etc/systemd/system/, với tên kết thúc bằng .service. Directory đó dành cho các unit do administrator thêm vào, và nó có ưu tiên cao hơn các unit đi kèm trong các package tại /lib/systemd/system/. Sau khi tạo hoặc sửa file ở đó, hãy chạy sudo systemctl daemon-reload để systemd nhận thay đổi.
Làm thế nào để service tự restart nếu bị crash?
Thêm Restart=on-failure và RestartSec=5 vào section [Service], sau đó chạy sudo systemctl daemon-reload và restart service. systemd sẽ relaunch chương trình khi nó thoát với exit code khác 0 hoặc chết do crash signal, và đợi 5 giây giữa các lần thử. Kiểm tra bằng sudo systemctl kill -s SIGKILL myapp.service — SIGTERM, signal mặc định, được coi là dừng sạch và không kích hoạt on-failure — và quan sát systemctl status hiển thị một PID mới trong vòng vài giây.
Làm thế nào để chạy systemd service dưới quyền user không phải root?
Tạo một system account với sudo useradd --system --no-create-home --shell /usr/sbin/nologin myapp, sau đó thêm User=myapp vào section [Service]. Thêm NoNewPrivileges=true, PrivateTmp=true, và ProtectSystem=strict để process chạy với quyền truy cập thấp nhất có thể. Chạy dưới quyền user không có đặc quyền là thay đổi quan trọng nhất để tăng tính an toàn cho một service.
Tại sao service của tôi không khởi động được?
Chạy systemctl status myapp.service để xem tóm tắt và journalctl -u myapp.service để xem output đầy đủ. Các nguyên nhân phổ biến nhất là sai path trong ExecStart, thiếu WorkingDirectory, lỗi permission do User= không đọc được file, hoặc quên sudo systemctl daemon-reload sau khi chỉnh sửa. Journal sẽ hiển thị thông báo lỗi của chính chương trình, thường sẽ chỉ đích danh vấn đề.