VPS 上如何將程式設為 systemd 服務
學會撰寫最小 unit file,讓程式開機自動啟動、當機後重新啟動,並透過 journalctl 查看日誌;另涵蓋 timer 排程與權限限制。
systemd 服務是什麼,以及為什麼需要它
systemd 服務是小型文字檔,會告訴伺服器如何執行程式:在開機時啟動、當程式當機時重新啟動,並將輸出寫入系統日誌。這就是它的全部工作。在 SSH 工作階段中手動啟動的程式,會在登出或伺服器重新開機時立即終止。包裝在 systemd 服務中的程式則會持續執行,因為它由伺服器本身管理,而不是由您的 shell 管理。
systemd 是 Ubuntu、Debian、Fedora 及大多數現代 Linux 伺服器上的 init 系統。它是第一個啟動的程序,也負責監督其他所有程序。這並非一直如此;了解 unit file 的運作方式後,值得閱讀一次 systemd 如何取代先前的 init script。撰寫 service file 時,就是將程式交給這個監督程序。本指南會說明可運作的最小 unit、每個 unit 都具備的 3 個區段、如何啟用服務並讀取其日誌、如何使用 timer 依排程執行服務,以及如何限制權限,讓服務以盡可能低的權限執行。
能正常運作的最小服務
服務檔案位於 /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 表示伺服器進入正常的 multi-user operation 後啟動服務,因此服務會在開機時啟動。其他內容都屬於後續調整。
三個區段及其用途
每個 unit file 都會分成數個方括號區段。服務會使用其中三個區段。
[Unit] 說明服務及其相依關係。以下是最常使用的兩行:
[Unit]
Description=My application
After=network-online.target
Wants=network-online.targetDescription 是您在 systemctl status 中看到的人類可讀標籤。After=network-online.target 會告訴 systemd,網路啟動後才能啟動您的程式。對於會繫結埠或建立輸出連線的程式,這項設定很重要。
[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 會在下方各自說明,因為它們正是多數人撰寫服務的主要原因。
[Install] 定義啟用服務後的行為:
[Install]
WantedBy=multi-user.targetWantedBy=multi-user.target 是您執行 systemctl enable 時,將服務連結至開機流程的設定。沒有 [Install] 區段時,服務可以手動啟動,但重新開機後不會自行啟動。
啟用並監控
編輯或建立任何 unit file 後,重新載入 systemd,讓它讀取變更,然後一次完成啟用與啟動服務:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp.servicedaemon-reload 是最容易被忽略的步驟:systemd 會快取 unit file,因此重新載入前,編輯內容不會生效。enable --now 會同時讓服務在開機時啟用,並立即啟動服務。確認狀態:
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 才是預期結果。若要讀取程式輸出,請讓 journal 只顯示這個 unit:
sudo journalctl -u myapp.service -f-f 會在新行寫入時持續顯示,就像 tail -f。程式寫入 standard output 或 standard error 的內容都會出現在這裡,無須另外設定 logging。
失敗時重新啟動,這就是你需要它的原因
服務的主要價值,在於程式結束時,systemd 會重新啟動程式。只需加入兩行:
[Service]
Restart=on-failure
RestartSec=5Restart=on-failure 會在程式以非零狀態碼結束,或因 SIGKILL、SIGSEGV 等當機訊號終止時重新啟動程式。正常結束,或由 SIGTERM、SIGINT、SIGHUP 或 SIGPIPE 停止,都不會觸發重新啟動。RestartSec=5 會在每次嘗試之間等待五秒,因此立即當機的程式不會進入緊密迴圈。請終止程序,並監看 systemd 將其重新啟動,以確認此功能。請使用 SIGKILL:預設的 SIGTERM 會被視為正常停止,因此 on-failure 不會重新啟動服務:
sudo systemctl kill -s SIGKILL myapp.service
sudo systemctl status myapp.service五秒內,狀態會顯示新的 Main PID,並再次顯示 active (running)。這就是整項功能,也是服務優於讓程式持續執行於 tmux 或 screen 的原因。
以非特權使用者執行並強化服務
如果程式遭到利用,以 root 身分執行的服務可以對伺服器進行任何操作。讓服務使用專屬使用者執行,並為 systemd 加入幾項限制其權限的設定。先建立沒有登入權限且沒有家目錄的系統帳號:
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 可防止程序取得新的權限,即使透過 setuid 二進位檔也一樣。PrivateTmp=true 會提供專用的 /tmp,其他程序無法查看。ProtectSystem=strict 會讓整個檔案系統變成唯讀,只有透過 ReadWritePaths= 指定的少數路徑例外。ProtectHome=true 會完全隱藏 /home。這與將服務置於防火牆後方採用的是相同的最小權限原則:只提供服務所需的權限。如果你已閱讀 關閉 VPS 上的 IPv6 防火牆漏洞 指南,這就是相同概念在主機上的實作。對於面向網際網路的服務,請搭配 在 SSH 前方設定 Fail2ban 與預設拒絕的防火牆。
不要手動逐項輸入並冒險記錯設定,請產生完整且已強化的 unit,再將其複製出去:
計時器:新式 cron
systemd 計時器會依排程執行服務,是 cron 工作的現代替代方案。計時器由 2 個檔案組成:負責執行工作的 .service,以及指定執行時間的 .timer。假設您要每天凌晨 3 點執行備份。服務會執行一次工作,然後結束:
# /etc/systemd/system/backup.service
[Unit]
Description=Nightly backup
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.shType=oneshot 告訴 systemd,程式會執行、完成並結束,而不是持續常駐。計時器則負責排程:
# /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 點。您可以使用 systemd-analyze calendar "*-*-* 03:00:00" 測試任何日曆運算式;它會確認語法是否能解析,並列出下次執行的時間。若伺服器在凌晨 3 點關機,Persistent=true 會在伺服器恢復運作後立即執行錯過的工作;cron 無法做到這點。請注意,計時器是透過 timers.target 啟用,而不是 multi-user.target。請啟用計時器,而不是服務:
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
systemctl list-timerslist-timers 會顯示所有計時器,以及各自的下次與上次執行時間,讓您快速確認工作下次何時執行。啟用計時器模式時,上述產生器會為您建立配對的 .service 與 .timer。與 cron 設定行相比,計時器可在 journal 中提供完整日誌,支援與服務相同的強化指令,也具備 Persistent=true 提供的錯過執行追趕功能。簡單工作仍適合使用 cron;工作變得重要後,計時器是更合適的工具。
FAQ
systemd 服務與 cron 工作有何不同?
服務會讓長時間執行的程式持續運作:開機時啟動、失敗時重新啟動,並將日誌寫入 journal。cron 工作則依排程執行短指令,完成後結束。若需要排程功能,也需要 journal 日誌、強化設定,以及補執行錯過的工作,請使用 systemd timer。它會將 .timer 排程與 oneshot 服務配對,在大多數伺服器工作中取代 cron。
systemd 服務檔案應放在哪裡?
請將自訂 unit 放在 /etc/systemd/system/,檔名結尾為 .service。此目錄用於存放管理員新增的 unit,其優先權高於套件在 /lib/systemd/system/ 中提供的 unit。在該目錄建立或編輯檔案後,執行 sudo systemctl daemon-reload,讓 systemd 載入變更。
如何讓服務在當機後重新啟動?
在 [Service] 區段加入 Restart=on-failure 與 RestartSec=5,然後執行 sudo systemctl daemon-reload 並重新啟動服務。程式以非零代碼結束,或因當機訊號終止時,systemd 會重新啟動程式,每次嘗試之間等待 5 秒。使用 sudo systemctl kill -s SIGKILL myapp.service 測試。SIGTERM 是預設訊號,會被視為正常停止,不會觸發 on-failure;請監控 systemctl status,確認幾秒內出現新的 PID。
如何讓 systemd 服務以非 root 使用者執行?
使用 sudo useradd --system --no-create-home --shell /usr/sbin/nologin myapp 建立系統帳號,然後在 [Service] 區段加入 User=myapp。加入 NoNewPrivileges=true、PrivateTmp=true 與 ProtectSystem=strict,讓程序只具備所需的最低存取權限。以非特權使用者執行,是提升服務安全性的最重要單項變更。
為什麼我的服務啟動失敗?
執行 systemctl status myapp.service 查看摘要,並執行 journalctl -u myapp.service 查看完整輸出。最常見的原因包括 ExecStart 中的路徑錯誤、缺少 WorkingDirectory、User= 無法讀取檔案而造成的權限錯誤,或編輯後忘記執行 sudo systemctl daemon-reload。journal 會顯示程式本身的錯誤訊息,通常會直接指出問題。