告别粗暴的 nohup!手把手教你写标准的 Systemd 服务:实现 VPS 进程守护与开机自启
2026-08-14 · DevCraft Studio
新手别再用 nohup 和 screen 挂后台了,重启就丢、崩溃不拉起。这篇手把手教你写一份标准 .service 文件,进程崩溃自动重启、开机自启、还能一条命令查日志。
很多刚上手 VPS 的朋友,习惯用 nohup python app.py & 或者开个 screen / tmux 把程序挂后台,以为这样就万事大吉了。确实,它简单,敲一行命令程序就跑起来了,不用学任何新东西。但只要你经历过一次服务器重启、一次意外掉线、或者程序自己崩了一次,你就会发现这种"野路子"有多脆弱——服务直接没了,数据可能没保存,日志散落得到处都是,你甚至不知道它什么时候死的。这篇文章就是来帮你把"野路子"换成正经的现代做法:用 systemd 写一份标准的服务文件,让你的进程自动守护、崩溃自动拉起、开机自启,还能用一条命令看日志。
延伸阅读
更多相关攻略推荐:2026 黑五 VPS 优惠大全:全年最低价怎么抢、【购买指南 01】新手如何选购第一台 VPS:配置/线路/支付/退款、你是电信还是联通还是移动?按运营商选便宜 VPS 最省钱最不卡、【内存技术 01】DDR4 还是 DDR5?2026 年内存涨价背景、【VPS 硬件选型指南 (CPU 篇) 02】AMD EPYC vs。
为什么 nohup 和 screen 不靠谱
先说痛点,搞清楚我们为什么要换掉它。nohup 的本质是"忽略挂断信号,把进程丢到后台",screen 的本质是"开个虚拟终端让你断开连接后进程还在"。它们能跑,但都只是临时凑合,问题一大堆。
举个真实例子你就有体感了:你写了个 Telegram 机器人挂在新买的 VPS 上,用 nohup 跑着,前两周风平浪静。某天商家给宿主机做内核升级,你的 VPS 被重启了,机器人悄无声息地挂了。你自己没在盯着,直到群里有人问"机器人怎么不回消息了"才发现——而这种事永远发生在你最忙、最不想折腾的时候。systemd 存在的意义,就是把这种"人工盯梢"变成"系统自动看护",你睡你的,它看着进程。
第一,重启就丢。nohup 启动的进程是 shell 的子进程,系统一重启全没了,不会自动恢复;screen 会话也是,VPS 重启后你之前挂的窗口直接蒸发。你得每次手动上去重新拉,挂机类程序(比如挖矿脚本、机器人、网盘同步)一旦服务器维护重启,你就得半夜爬起来救火。
第二,没有"崩溃后自动拉起"。程序自己抛个异常崩了,它就真崩了,留在原地没人管。你想让它"挂了就立刻重启",nohup 根本做不到,screen 也做不到,你得写复杂的 while 死循环或者第三方程序去盯着。
第三,日志管理一团糟。nohup 默认把所有输出塞进当前目录的 nohup.out,文件越写越大,半年能吃掉几 GB 磁盘;screen 里想回看历史得拼命往上滚,痛苦得要命。哪天磁盘被日志写满了,整台机器都可能卡死。
第四,进程管理粗糙。想优雅地停止、重启、看状态,没有一个统一入口,只能 ps aux | grep 满屏找进程号再 kill,一不小心杀错就是事故。权限上也容易图省事直接用 root 跑,一旦程序有漏洞就是提权炸弹。
Systemd 服务文件长什么样
systemd 是现代 Linux 发行版(Ubuntu、Debian、CentOS、Rocky 等)默认的服务管理器。它的配置文件叫 unit 文件,放在 /etc/systemd/system/ 目录下,名字以 .service 结尾。一个标准的服务文件分成三段:[Unit]、[Service]、[Install],各管各的事。
[Unit] 段描述的是"这个服务是什么、什么时候启动"。最常用的两个指令是 Description=(给人看的描述)和 After=network.target(告诉 systemd 等网络起来之后再启动我的服务,避免程序一启动就去连数据库结果网络还没好而报错)。
[Service] 段是核心,描述"怎么运行这个程序"。包括以哪个用户运行(User=)、工作目录(WorkingDirectory=)、启动命令(ExecStart=)、崩溃了怎么办(Restart=)、环境变量(Environment= 或 EnvironmentFile=)等等。
[Install] 段控制"开机怎么启用",通常只需要一行 WantedBy=multi-user.target,意思是把它挂到多用户运行级别下,开机就跟着起来。
一份可以直接抄的完整示例
下面这份 unit 文件是我常用的"生产级模板",把上面提到的要点都涵盖了,你把路径和命令换成自己的就能用。假设我们要守护一个用虚拟环境运行的 Python 程序,放在 /opt/myapp。
[Unit]
Description=My Python App
After=network.target
[Service]
Type=simple
User=appuser
Group=appuser
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/venv/bin/python /opt/myapp/main.py
Restart=always
RestartSec=5s
Environment=PYTHONUNBUFFERED=1
EnvironmentFile=-/opt/myapp/.env
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp
[Install]
WantedBy=multi-user.target
写完之后,把它保存成 /etc/systemd/system/myapp.service,然后执行下面三条命令让 systemd 接管:
顺手验证一下开机自启有没有真的挂上:跑 systemctl is-enabled myapp,返回 enabled 就说明已经进了开机流程;如果返回 disabled,说明你漏了 enable 那一步,重启后服务不会自己起来——这是新手最容易踩、也最隐蔽的一个坑。
sudo systemctl daemon-reload
sudo systemctl enable myapp
sudo systemctl start myapp
daemon-reload 是告诉 systemd"我新加了/改了配置文件,重新读一下";enable 是设置开机自启;start 是立刻启动。之后用 systemctl status myapp 就能看到它活着没、最近日志、进程号,一目了然。
崩溃自动拉起:Restart 与 RestartSec
这是守护进程最爽的功能。Restart= 决定"什么情况下 systemd 帮你把它拉起来",常用取值有三个:
no:从不自动重启(默认,等于没守护)。on-failure:只有非正常退出才重启,比如程序崩溃、被信号杀死、退出码非 0;如果是你自己systemctl stop或者程序正常退出(退出码 0),它就不会重启。适合那些"正常退出就是真要停"的服务。always:不管怎么退出都重启,包括正常退出、被杀、崩溃。适合必须 7x24 在线、宁可重启也不愿意停的挂机类程序。
对 VPS 上跑的常驻进程,我一般直接上 Restart=always,最省心。再配合 RestartSec=5s,意思是崩溃后等 5 秒再拉起,避免程序一启动就崩、立即又崩,陷入疯狂重启把 CPU 打满的死循环。
如果你想更保守,还可以加 StartLimitBurst=5 和 StartLimitIntervalSec=300,意思是 5 分钟里最多重启 5 次,超过就放弃并报失败,防止有 bug 的程序把机器拖垮。不过对新手来说,Restart=always + RestartSec=5s 已经足够。
顺带说一句,崩溃时你完全不用慌着去翻日志猜发生了什么。systemd 会自己记录整个重启过程,你在 journalctl -u myapp 里通常会看到这样一行:"Main process exited, code=exited, status=1/FAILURE",紧接着下一行就是"Scheduled restart, restart counter is at 1",再过 5 秒程序就被自动拉起来了。整个过程全自动,你哪怕人不在电脑前,服务也在自己修复。
不要用 Root 跑:指定 User 和 Group
很多教程图省事让你直接 root 跑,这是坏习惯。一旦你的程序有远程代码执行之类的漏洞,攻击者就直接拿到了 root 权限,整台 VPS 沦陷。正确做法是建一个普通用户专门跑这个服务:
sudo useradd -r -s /bin/false appuser
sudo chown -R appuser:appuser /opt/myapp
-r 表示创建系统用户(没有登录密码、不能拿来 SSH 登录),-s /bin/false 让它连 shell 都没有,更安全。然后在 unit 文件里写 User=appuser 和 Group=appuser,程序就以这个低权限用户运行了。数据库密码、密钥文件记得把权限收好,chmod 600 只允许属主读取。
环境变量怎么给:Environment 与 EnvironmentFile
程序经常需要一些环境变量,比如数据库地址、API Key、运行模式。有两种给法。简单的值直接写在 unit 文件里:
Environment=NODE_ENV=production
Environment=PORT=3000
如果是敏感信息(密钥、密码),千万别写进 unit 文件——那玩意儿谁都能 cat 到。正确做法是放进一个独立的 .env 文件,再用 EnvironmentFile= 引用:
EnvironmentFile=-/opt/myapp/.env
注意路径前面的那个减号 -,它的意思是"这个文件即使不存在也不要报错",特别适合 .env 是可选的或者还没生成的场景。文件内容就是普通的一行一个键值对:
DATABASE_URL=mysql://user:pass@localhost:3306/mydb
SECRET_KEY=your-secret-key-here
记得把 .env 的权限收紧:sudo chmod 600 /opt/myapp/.env && sudo chown appuser:appuser /opt/myapp/.env,只有运行用户能读。
看日志:journalctl 常用命令
前面我们把 StandardOutput=journal 和 StandardError=journal 设上了,程序的 stdout/stderr 就全部进了 systemd 的日志系统(journald),不用再管 nohup.out。看日志用 journalctl,配 -u 服务名 只盯这一个服务:
sudo journalctl -u myapp # 看全部日志
sudo journalctl -u myapp -f # 实时跟随,像 tail -f
sudo journalctl -u myapp -n 50 # 只看最近 50 行
sudo journalctl -u myapp --since "1 hour ago" # 最近一小时
sudo journalctl -u myapp -p err # 只看错误级别及以上
sudo journalctl -u myapp -b # 只看本次开机以来的
sudo journalctl -u myapp -r # 倒序,最新的在最上面
这几个命令基本覆盖日常 90% 的排错需求。-f 调试时最常用,程序一打日志你就看见;-p err 帮你从一堆正常输出里快速捞出报错。
几个新手常踩的坑
- 改了 unit 文件不 daemon-reload:每次新增或修改 .service 文件,都必须先
systemctl daemon-reload,否则 systemd 还用着旧的缓存,改了跟没改一样。 - ExecStart 必须用绝对路径:不能写
python main.py,必须写/opt/myapp/venv/bin/python /opt/myapp/main.py,systemd 不会去翻你的 PATH。 - Type 选错:大部分前台运行的程序(Node、Python、Go 直接跑)用
Type=simple就行;只有那种自己 fork 到后台的传统守护进程(比如 nginx、Apache)才用Type=forking并配PIDFile=。选错会导致 systemd 以为启动失败。 - 权限不对:报 Permission denied 多半是文件属主不是你设的 User,或者目录没有读/执行权限,回去检查 chown。
- 想看实时日志没 -f:
journalctl -u myapp默认只显示历史,想跟着刷新的一定要加-f。
总结
把 nohup / screen 换成 systemd 服务,短期看是多学一个配置文件,长期看是给自己省了无数半夜救火的精力。核心就三件事:写一份带 Restart=always 和 RestartSec=5s 的 .service 文件实现崩溃自动拉起;用 User= 指定非 root 用户、用 EnvironmentFile= 外置敏感配置;用 journalctl -u 服务名 -f 一条命令看日志。写完 enable + start,你的 VPS 进程从此就有人 24 小时看护了,重启不怕丢、崩溃不怕停。RackNerd、Colocrossing、Vultr、Hetzner 这些常见 VPS 上都是这套玩法,照着抄就能用。
常见问题 FAQ
问:为什么别再用 nohup? nohup/screen 只是临时把进程丢后台,服务器一重启或掉线进程就没了,崩溃也没人管。systemd 可设 Restart=always 实现崩溃自愈与开机自启,不必再人工盯梢。
问:写一个 .service 文件要几步? 在 /etc/systemd/system/ 下新建 xxx.service,写 [Unit] 描述、[Service] 里指定 User、WorkingDirectory、ExecStart 与 Restart=always,再执行 systemctl daemon-reload 和 systemctl enable --now xxx 即可。
问:怎么查看服务日志? 用 journalctl -u 服务名 查看;加 -f 实时跟踪输出,-b 只看本次启动后的日志,比散落在各处的 nohup.out 文件集中好查,排错效率高得多。