别再用 nohup 了:把你的程序交给 systemd——开机自启、崩溃重启、日志与安全加固
很多自己写的小服务,第一次部署到 Linux 上时大概都是这样:
./myapp
发现 SSH 一断程序也没了,于是升级成:
nohup ./myapp > app.log 2>&1 &
再熟练一点,可能还会顺手记下 PID:
echo $! > app.pid
这当然能用。
但只要这个程序真的开始承担服务职责,问题很快就会出现:
- 服务器重启以后谁把它拉起来?
- 程序崩了谁负责重启?
- 到底跑了几个实例?
- PID 文件失效怎么办?
- 日志越来越大怎么办?
- 环境变量放在哪里?
- 服务为什么要用 root 跑?
- SSH 退出以后程序还在,但到底是不是健康的?
- 想停服务时,应该
kill哪个进程?
这时候继续往 nohup 外面套 Shell 脚本,通常只是在自己重新实现一套更差的服务管理器。
如果你的 Linux 使用 systemd,更直接的办法是:
把进程交给 systemd。
本文从一个普通的可执行程序开始,把它改造成一个具备开机启动、崩溃重启、统一日志、独立用户、配置隔离和基础安全加固的系统服务。
本文适用于 Ubuntu、Debian、Rocky Linux、CentOS Stream、openEuler、Alibaba Cloud Linux 等使用 systemd 的发行版。不同发行版的 systemd 版本可能不同,个别安全参数如果版本过旧不支持,删掉对应参数即可。
一、先确认你的机器是不是 systemd
先执行:
ps -p 1 -o comm=
如果输出:
systemd
基本就可以继续了。
再看一下版本:
systemctl --version
例如:
systemd 252
这里不要求一定是某个特定版本。本文最核心的 systemctl、journalctl、.service 都已经是现代 Linux 上非常成熟的能力。
二、为什么不建议长期用 nohup
先说清楚:nohup 本身没有错。
它非常适合:
- 临时跑一个脚本;
- 临时进行数据处理;
- 跑一次性任务;
- 开发测试时让进程在退出 SSH 后继续工作。
问题是,它并不是服务管理器。
假设我们这样启动:
nohup ./myapp > app.log 2>&1 &
实际上只是完成了两件事:
- 忽略挂断信号;
- 把输出重定向到文件。
它并不会自动帮我们解决:
生命周期管理
├── 开机启动
├── 崩溃重启
├── 优雅停止
└── 启动顺序
运行身份
├── 使用哪个用户
└── 拥有哪些系统权限
可观测性
├── 当前状态
├── 退出码
├── 启动次数
└── 统一日志
资源与安全
├── 文件系统访问
├── Home 目录访问
├── 提权限制
└── 内核资源访问
这些才是一个长期运行服务真正需要解决的问题。
三、我们要部署什么
为了让教程可以直接套用,假设现在有一个程序:
myapp
它可以是:
- Go 编译出来的单文件二进制;
- Rust 程序;
- C/C++ 程序;
- Java 的启动脚本;
- Python 虚拟环境中的入口程序;
- Node.js 服务。
为了简单,我们先假设它本身可以直接运行:
./myapp
监听:
127.0.0.1:8080
并通过环境变量读取配置。
最终目录结构规划成:
/opt/myapp/
└── myapp # 程序本体,root 拥有
/etc/myapp/
└── myapp.env # 配置文件
/var/lib/myapp/ # 程序需要写入的数据
/etc/systemd/system/
└── myapp.service # systemd 服务定义
这个结构有一个很重要的思想:
程序、配置和运行数据分开。
不要把数据库文件、上传文件、日志、密钥和程序二进制全部扔进同一个目录。
四、第一步:不要让服务直接以 root 身份运行
很多人为了省事会直接:
sudo ./myapp
然后 service 文件里也不指定用户。
这样 systemd 系统服务默认就可能以 root 身份执行。
如果你的 Web API 只是监听 8080、读数据库、访问自己的数据目录,它通常根本不需要 root。
创建一个专用系统用户:
NOLOGIN="$(command -v nologin || echo /sbin/nologin)"
sudo useradd --system --no-create-home --shell "$NOLOGIN" myapp
如果提示用户已经存在,不需要重复创建。
检查:
getent passwd myapp
这个账号的作用不是拿来 SSH 登录,而是专门承担服务进程身份。
这样即使程序未来出现漏洞,攻击者拿到的首先也只是 myapp 用户的权限,而不是整台服务器的 root。
五、第二步:安装程序和配置
创建目录:
sudo install -d -o root -g root -m 0755 /opt/myapp
sudo install -d -o root -g myapp -m 0750 /etc/myapp
sudo install -d -o myapp -g myapp -m 0750 /var/lib/myapp
把程序安装进去:
sudo install -o root -g root -m 0755 ./myapp /opt/myapp/myapp
注意这里故意让程序文件归:
root:root
而不是:
myapp:myapp
原因很简单:
运行服务的账号最好不能修改自己的程序本体。
否则一旦服务进程被利用,攻击者可能直接覆盖可执行文件,形成持久化。
配置环境变量
新建:
sudo nano /etc/myapp/myapp.env
例如:
MYAPP_ADDR=127.0.0.1:8080
MYAPP_DATA_DIR=/var/lib/myapp
DATABASE_URL=postgres://myapp:change-me@127.0.0.1:5432/myapp
设置权限:
sudo chown root:myapp /etc/myapp/myapp.env
sudo chmod 0640 /etc/myapp/myapp.env
检查:
ls -l /etc/myapp/myapp.env
应该类似:
-rw-r----- 1 root myapp ... /etc/myapp/myapp.env
这至少比把数据库密码直接写进 .service 文件好维护得多。
如果涉及真正高价值的生产密钥,最好进一步接 Secret Manager、systemd credentials 或其他密钥管理方案,而不是长期把所有密钥作为普通环境变量保存。
六、第三步:创建第一个 systemd Service
新建:
sudo nano /etc/systemd/system/myapp.service
先写一个基础版:
[Unit]
Description=MyApp Service
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/var/lib/myapp
EnvironmentFile=/etc/myapp/myapp.env
ExecStart=/opt/myapp/myapp
Restart=on-failure
RestartSec=3s
TimeoutStopSec=30s
KillSignal=SIGTERM
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp
[Install]
WantedBy=multi-user.target
一份真实的 systemd 服务文件大概就是这种结构。下面这张图是国内技术社区中常见的 Service 配置示例,可以对照理解:

配图来源:CSDN 国内图床;示例原文可见博客园《Centos7 的 Systemctl 与创建系统服务》。本文配置以本文给出的内容为准。
接下来逐块解释。
七、[Unit]:这个服务是谁,它依赖什么
[Unit]
Description=MyApp Service
Wants=network-online.target
After=network-online.target
Description 就是描述。
以后执行:
systemctl status myapp
会看到这个名字。
After=network-online.target 表示启动顺序上希望放在网络在线目标之后。
Wants=network-online.target 则把这个目标作为弱依赖拉进来。
但要注意:
network-online.target 并不等于“互联网一定能访问”。
它只是 systemd 与网络管理组件之间的启动同步机制。
如果你的服务必须等待某个远端数据库真正可以访问,仍然应该在程序里实现连接重试,而不是把全部希望寄托在启动顺序上。
八、[Service]:真正决定程序怎么跑
1. Type=simple
Type=simple
对于 Go、Rust、Python、Node.js 这类直接以前台进程运行的现代服务,大多数时候 simple 就够了。
程序不要自己 fork 到后台。
例如你的 Go 服务就正常:
/opt/myapp/myapp
然后一直占着前台运行。
后台管理交给 systemd。
这也是一个非常重要的思路:
应用程序负责业务,systemd 负责守护。
2. User 和 Group
User=myapp
Group=myapp
明确告诉 systemd:
不要用 root 跑。
这是整份配置里最值得养成习惯的一项。
3. WorkingDirectory
WorkingDirectory=/var/lib/myapp
不要依赖“我从哪个目录启动程序”。
systemd 启动服务时和你在 SSH 终端手工执行的环境并不一样。
程序如果使用相对路径:
./data
./config.json
是否设置 WorkingDirectory 会直接影响它最终访问哪里。
更好的应用本身应该优先使用明确路径,但服务层把工作目录固定下来仍然很有价值。
4. EnvironmentFile
EnvironmentFile=/etc/myapp/myapp.env
把运行配置和 service 定义分离。
以后换数据库地址,只需要修改:
sudo nano /etc/myapp/myapp.env
sudo systemctl restart myapp
不需要每次都改 unit 文件。
5. ExecStart
ExecStart=/opt/myapp/myapp
这是服务真正执行的程序。
这里有一个新手非常容易踩的坑:
不要把它当成一整行 Bash。
比如你写:
ExecStart=/opt/myapp/myapp > /tmp/myapp.log 2>&1
通常不会像你在 Shell 里想象的那样工作。
>、2>&1、| 这些是 Shell 语法,而 systemd 默认不会先偷偷给你套一层 Bash。
需要日志就交给 journald;真的需要 Shell,再显式写:
ExecStart=/bin/bash -lc '...'
但普通服务尽量不要这么做。
九、程序崩了以后自动拉起来
核心是:
Restart=on-failure
RestartSec=3s
on-failure 非常适合作为普通长期服务的默认策略之一。
当程序异常退出时,systemd 会尝试重新启动。
比如我们故意杀掉进程:
sudo systemctl kill -s SIGSEGV myapp
稍等几秒,再看:
systemctl status myapp
如果一切正常,服务应该重新起来。
但生产环境还要防另一种情况:
启动
→ 立即崩溃
→ 重启
→ 立即崩溃
→ 无限循环
可以在 [Unit] 增加启动频率限制:
StartLimitIntervalSec=60
StartLimitBurst=5
完整变成:
[Unit]
Description=MyApp Service
Wants=network-online.target
After=network-online.target
StartLimitIntervalSec=60
StartLimitBurst=5
意思可以粗略理解为:
在一段时间内失败得太频繁,就先别疯狂重启了。
如果修复问题以后发现服务处于 start-limit-hit,可以:
sudo systemctl reset-failed myapp
sudo systemctl start myapp
十、启动、开机自启和配置重载
写完 service 后,先让 systemd 重新扫描配置:
sudo systemctl daemon-reload
然后启动:
sudo systemctl start myapp
看状态:
systemctl status myapp
确认没有问题以后设置开机启动:
sudo systemctl enable myapp
也可以一步完成:
sudo systemctl enable --now myapp
以后最常用的命令基本就是:
sudo systemctl start myapp
sudo systemctl stop myapp
sudo systemctl restart myapp
sudo systemctl status myapp
查看是否开机启用:
systemctl is-enabled myapp
查看当前是否真的在运行:
systemctl is-active myapp
systemctl status 的输出除了告诉你 running/failed,还会带上 PID、最近日志和退出状态。下面是一个典型的 systemctl status 终端示例:

配图使用 CSDN 国内图床,图中具体服务仅用于展示
systemctl status的输出形式。
十一、终于可以不用自己管理 app.log 了
如果程序把日志正常输出到:
stdout
stderr
我们配置了:
StandardOutput=journal
StandardError=journal
那么就可以直接使用 journald。
查看全部日志:
journalctl -u myapp
只看最近 100 行:
journalctl -u myapp -n 100
实时跟踪:
journalctl -u myapp -f
只看今天:
journalctl -u myapp --since today
看最近 30 分钟:
journalctl -u myapp --since "30 min ago"
看某一个时间段:
journalctl -u myapp \
--since "2026-08-10 02:00:00" \
--until "2026-08-10 03:00:00"
只看 warning 以上:
journalctl -u myapp -p warning
腾讯云开发者社区的 systemd/journalctl 资料中也给出了 journalctl -u <service>、-f、--since、--disk-usage 等常用操作。对于服务器上的长期服务,这种按 Unit 聚合日志的方式比散落一堆 nohup.out 更容易追踪。
十二、日志会不会把磁盘写满?
先看当前 journal 占用:
journalctl --disk-usage
临时清理到指定大小:
sudo journalctl --vacuum-size=1G
或者只保留一定时间:
sudo journalctl --vacuum-time=14d
更长期的限制应该放到 journald 配置里,而不是每天手工清理。
例如:
sudo nano /etc/systemd/journald.conf
可以根据服务器磁盘大小设置类似:
[Journal]
SystemMaxUse=1G
RuntimeMaxUse=256M
然后:
sudo systemctl restart systemd-journald
具体数值不要照抄,要根据你的磁盘、日志量和审计保留需求决定。
十三、让服务真正“少一点权限”
到目前为止,我们已经解决了:
- 不用 root;
- 自动重启;
- 开机自启;
- 统一日志;
- 独立配置目录。
但 systemd 还能做更多。
在 [Service] 中追加一组相对保守的安全参数:
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
ReadWritePaths=/var/lib/myapp
UMask=0027
分别看一下。
NoNewPrivileges=true
NoNewPrivileges=true
限制服务及其子进程通过某些机制获得新的额外权限。
对于普通业务服务,通常值得开启。
PrivateTmp=true
PrivateTmp=true
给服务一个隔离的临时目录视图,降低它和其他服务通过 /tmp 相互影响的机会。
如果你的程序依赖和其他进程通过 /tmp 共享文件,就不要盲开。
ProtectSystem=strict
ProtectSystem=strict
把大部分文件系统对这个服务变成只读。
但我们的程序需要写:
/var/lib/myapp
所以显式放行:
ReadWritePaths=/var/lib/myapp
最终效果就是:
系统大部分位置:不能写
/var/lib/myapp:允许写
这比“进程理论上可以到处写,只是希望它不要乱写”要可靠得多。
ProtectHome=true
ProtectHome=true
如果一个后端 API 根本不需要读取:
/root
/home
/run/user
那就不要让它读。
假设服务未来被利用,也可以少暴露一部分 SSH Key、开发者配置和用户文件。
ProtectKernel*
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
一个普通 Web 服务一般没有理由去修改内核参数、内核模块或者 cgroup 层级。
不需要的能力就关掉。
这其实就是最小权限原则。
十四、一份更完整的最终配置
于是我们的 /etc/systemd/system/myapp.service 可以变成:
[Unit]
Description=MyApp Service
Wants=network-online.target
After=network-online.target
StartLimitIntervalSec=60
StartLimitBurst=5
[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/var/lib/myapp
EnvironmentFile=/etc/myapp/myapp.env
ExecStart=/opt/myapp/myapp
Restart=on-failure
RestartSec=3s
TimeoutStopSec=30s
KillSignal=SIGTERM
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
ReadWritePaths=/var/lib/myapp
UMask=0027
[Install]
WantedBy=multi-user.target
修改后不要忘记:
sudo systemctl daemon-reload
sudo systemctl restart myapp
然后:
systemctl status myapp
journalctl -u myapp -n 100 --no-pager
确认程序确实正常工作。
安全选项不要一次性复制到所有程序。比如程序需要访问用户 Home、挂载 FUSE、调用某些内核接口时,对应限制就可能导致它无法运行。正确方法是逐步收紧,然后测试。
十五、先验证 Unit 文件,再重启
生产服务器上最尴尬的事情之一是:
配置改完
→ 直接 restart
→ 服务起不来了
systemd 提供了验证工具。
可以先执行:
sudo systemd-analyze verify /etc/systemd/system/myapp.service
没有输出通常意味着没有发现明显问题。
再让 systemd 重载:
sudo systemctl daemon-reload
如果系统支持,还可以查看服务的安全暴露情况:
systemd-analyze security myapp.service
它会根据各种 namespace、capability、文件系统保护等配置给出分析。
不要为了追求一个“满分”盲目把所有选项全开。
目标应该是:
在不破坏业务的前提下,把服务不需要的权限尽量拿掉。
十六、程序更新应该怎么做
最简单的更新流程:
sudo systemctl stop myapp
sudo install -o root -g root -m 0755 ./myapp /opt/myapp/myapp
sudo systemctl start myapp
然后检查:
systemctl is-active myapp
journalctl -u myapp -n 50 --no-pager
如果程序支持优雅退出,systemd 在停止时会向主进程发送停止信号。你的程序应该正确处理 SIGTERM:
收到 SIGTERM
→ 停止接收新请求
→ 等待正在执行的请求结束
→ 关闭数据库连接
→ 刷新必要数据
→ 正常退出
而不是一收到信号就直接丢掉所有正在处理的任务。
对于 Go HTTP 服务,可以结合 http.Server.Shutdown;其他语言和框架也通常有对应的 graceful shutdown 能力。
十七、不要在 ExecStart 里偷偷启动后台进程
这是另一个很常见的问题。
错误思路:
ExecStart=/opt/myapp/start.sh
然后 start.sh:
nohup /opt/myapp/myapp > app.log 2>&1 &
exit 0
这等于:
systemd 管 start.sh
start.sh 又用 nohup 管 myapp
systemd 很快看到脚本退出,就会认为主进程结束了。
你又重新回到了 PID 跟踪和后台进程的问题里。
正确方式通常就是:
ExecStart=/opt/myapp/myapp
让真正的服务进程保持前台运行。
十八、Python、Node.js 和 Java 怎么写?
原理完全一样,只是 ExecStart 不同。
Python 虚拟环境
ExecStart=/opt/myapp/venv/bin/python /opt/myapp/app.py
或者:
ExecStart=/opt/myapp/venv/bin/gunicorn -b 127.0.0.1:8000 app:app
不要指望 systemd 自动执行:
source venv/bin/activate
直接使用虚拟环境里的解释器绝对路径即可。
Node.js
先找 Node 的绝对路径:
command -v node
例如:
ExecStart=/usr/bin/node /opt/myapp/server.js
如果 Node 是通过 nvm 装在个人 Home 下,就会马上碰到一个部署设计问题:
生产服务是否应该依赖某个登录用户的 nvm 环境?
一般来说不建议。
服务运行环境最好有明确、稳定、可重复的运行时路径。
Java
ExecStart=/usr/bin/java -Xms256m -Xmx512m -jar /opt/myapp/app.jar
Java 也不需要自己在命令末尾加 &。
十九、最常见的十个坑
1. 改完 service 忘了 daemon-reload
sudo systemctl daemon-reload
这是第一反应。
2. ExecStart 写相对路径
不要:
ExecStart=./myapp
用:
ExecStart=/opt/myapp/myapp
3. 依赖 SSH 登录环境里的 PATH
你 SSH 登录时:
node
python
java
都能找到,不代表 systemd 的运行环境也一样。
尽量写绝对路径:
command -v python3
command -v node
command -v java
4. 用 root 只是因为“权限问题少”
权限问题少了,安全问题会变大。
正确做法是明确:
程序到底需要读什么
程序到底需要写什么
然后给对应权限。
5. 把密码直接写进 unit
比如:
Environment=DATABASE_PASSWORD=123456
service 文件经常会被复制、备份、查看。
至少单独使用权限受控的 EnvironmentFile;更敏感的场景再上专门的密钥管理。
6. 程序自己 daemonize
systemd 管理现代应用时,大多数情况下让程序保持前台运行更简单。
7. Restart=always 无脑套
有些一次性任务正常执行完就应该结束。
你却写:
Restart=always
它就会重新跑。
先搞清楚服务的生命周期。
8. 程序崩溃后无限重启刷屏
加入:
StartLimitIntervalSec=60
StartLimitBurst=5
同时真正去修崩溃原因。
9. 配置了 ProtectSystem 却忘记放行数据目录
表现通常就是:
Permission denied
Read-only file system
检查:
ReadWritePaths=/var/lib/myapp
10. 只看 status,不看 journal
systemctl status 只会展示有限的最近内容。
真正排查时直接:
journalctl -u myapp -b
或者:
journalctl -u myapp --since "10 min ago"
二十、顺手再学一个:临时命令也能交给 systemd
有时候你只是想临时把某条命令交给 systemd 管,而不想马上写 unit 文件。
可以试试:
systemd-run --unit=my-task /usr/local/bin/my-task
然后:
systemctl status my-task
journalctl -u my-task
这对于临时后台任务非常方便。
它和:
nohup command &
相比最大的区别之一,就是这个临时进程已经进入 systemd 的生命周期和日志管理体系。
二十一、什么时候仍然可以用 nohup?
写到这里也不要走到另一个极端。
如果只是:
今晚跑一个数据转换脚本
或者:
临时下载一个大文件
那:
nohup command > task.log 2>&1 &
完全没问题。
甚至用:
screen
tmux
可能更方便。
真正的分界线不是“nohup 落后,systemd 高级”。
而是:
这个进程是不是已经变成了一项需要长期维护的服务。
如果答案是“是”,那它就应该拥有明确的:
启动方式
运行身份
配置来源
日志入口
停止方式
失败策略
权限边界
而不是依靠某个人记得:
cd /home/xxx/project
nohup ./app > app.log 2>&1 &
最后
一台服务器真正开始变乱,往往不是因为上面跑了太多程序。
而是因为每个程序都有自己的一套“民间启动方式”:
这个要去 /root/a 里 nohup
那个要先 source 一个脚本
另一个 PID 写在 /tmp
还有一个挂了以后手动重启
日志有人写 app.log
有人写 nohup.out
还有人直接不知道写哪去了
当程序数量从 2 个变成 10 个,再从一个人维护变成多人维护,这种方式很快就会失控。
systemd 的价值不是让启动命令变得更复杂。
恰恰相反,它是在把这些原本散落在人脑、Shell 历史和 README 里的约定,变成机器可以执行的配置:
谁来运行
怎么启动
什么时候启动
挂了怎么办
日志去哪里
能访问什么
怎么停止
所以,如果你的程序已经准备长期留在一台 Linux 服务器上:
别只让它“在后台跑着”。
把它真正变成一个服务。
参考资料
- 腾讯云开发者社区:Systemd 服务管理与 journalctl 相关教程:https://cloud.tencent.com/developer/article/1516125
- 腾讯云开发者社区:journalctl 查看和分析 systemd 日志:https://cloud.tencent.com/developer/news/1005990
- 腾讯云开发者社区:systemd timer 与可观测定时任务实践:https://cloud.tencent.com/developer/article/2644885
- systemd 官方手册:https://www.freedesktop.org/software/systemd/man/
别再用 nohup 了:把你的程序交给 systemd——开机自启、崩溃重启、日志与安全加固
https://wangling.hauchet.cn/archives/stop-using-nohup-manage-your-app-with-systemd
评论