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