用 systemd 服务在 VPS 上运行程序
systemd 服务让您的程序持续运行:开机自启、崩溃后重启、日志写入 journal。本文讲如何编写服务、添加定时器并加固它。
什么是 systemd 服务,以及为什么您需要它
systemd 服务是一个小的文本文件,它告诉服务器如何运行一个程序:开机时启动它、崩溃时重启它、把它的输出送到系统日志。这就是它的全部工作。您在 SSH 会话里手动启动的程序,在您退出登录或服务器重启的那一刻就会消失。而包装成 systemd 服务的程序会持续运行,因为拥有它的是服务器本身,而不是您的 shell。
systemd 是 Ubuntu、Debian、Fedora 以及大多数现代 Linux 服务器上的 init 系统(初始化系统)。它是第一个启动的进程,也是监管其他所有进程的那一个。当您编写一个服务文件时,就是把您的程序交给这个监管者。本指南会展示能工作的最小单元(unit)、每个单元都有的三个区段、如何启用它并读取它的日志、如何用定时器按计划运行它,以及如何把它锁定到用尽可能少的权限运行。
能工作的最小服务
服务文件放在 /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这就是一个完整、可用的单元。ExecStart 是要运行的命令。WantedBy=multi-user.target 表示当服务器进入正常的多用户运行状态后就启动它,这正是让它开机自启的原因。其余的一切都只是细化。
三个区段,以及各自的用途
每个单元文件都用方括号划分为若干区段。一个服务用到三个。
[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 来运行程序,这是关乎安全最重要的一行。Restart=on-failure 和 RestartSec=5 会在下面单独用一节来讲,因为它们正是大多数人之所以要写服务的原因。
[Install] 决定当您启用(enable)服务时会发生什么:
[Install]
WantedBy=multi-user.target当您运行 systemctl enable 时,正是 WantedBy=multi-user.target 把服务挂接到开机流程中。没有 [Install] 区段,服务仍可以手动启动,但在重启后不会自行启动。
启用它并观察它
在编写或编辑任何单元文件之后,先重新加载 systemd 让它读取改动,然后一步完成启用并启动服务:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp.servicedaemon-reload 是人们常忘的一步:systemd 会缓存单元文件,所以在您重新加载之前,编辑不会生效。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 只查询这一个单元:
sudo journalctl -u myapp.service -f-f 会随着新行的到来持续跟踪,就像 tail -f 一样。您的程序写入标准输出或标准错误的任何内容都会落到这里,而您无需做任何日志配置。
失败时重启,您来这里的原因
服务的主要回报,就是当您的程序死掉时 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 以及默认拒绝的防火墙搭配使用。
与其把这一切都手动敲一遍、还记错某条指令,不如生成一个完整、加固好的单元,再把它复制出来:
定时器:现代版的 cron
systemd 定时器(timer)按计划运行一个服务,它是 cron 任务的现代替代品。一个定时器由两个文件组成:一个负责干活的 .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" 来测试任何日历表达式,它会确认表达式能被解析,并打印出接下来几次触发的时间。Persistent=true 会在服务器于凌晨 3 点处于关机状态而错过任务时,一旦服务器恢复就立即补跑,这是 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 排定的计划和一个 oneshot 服务配成一对,在大多数服务器任务上取代 cron。
我的 systemd 服务文件该放在哪里?
把您自己的单元放在 /etc/systemd/system/,名字以 .service 结尾。那个目录是给管理员添加的单元用的,并且它的优先级高于软件包放在 /lib/systemd/system/ 里的单元。在那里创建或编辑文件之后,运行 sudo systemctl daemon-reload,让 systemd 接纳这个改动。
我该怎样让服务在崩溃后重启?
在 [Service] 区段加上 Restart=on-failure 和 RestartSec=5,然后运行 sudo systemctl daemon-reload 并重启服务。当程序以非零退出码退出或因崩溃信号死亡时,systemd 会重新启动它,两次尝试之间等待五秒。用 sudo systemctl kill -s SIGKILL myapp.service 来测试(默认信号 SIGTERM 算作一次正常停止,不会触发 on-failure),并观察 systemctl status 在几秒内显示一个新的 PID。
我该怎样以非 root 用户运行 systemd 服务?
用 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 会显示程序自己的错误信息,它通常会直接点出问题所在。