必须用 systemd 管理服务,需编写 /etc/systemd/system/ 下的 .service 文件,设 Type=simple 或 notify、Restart=on-failure、资源限制、日志接入 journald,禁用 /dev/null 重定向,敏感信息用 EnvironmentFile 或 Secret 管理。

服务必须用 systemd 管理,不能直接 run.sh 启动
手动启动的服务在崩溃、重启、OOM 后不会自动恢复,运维上等于裸奔。systemd 提供了进程守护、依赖管理、日志归集和资源限制能力,是现代 Linux 服务部署的底线。
- 必须写
.service文件,放在/etc/systemd/system/下,不能放/usr/lib/systemd/system/(后者属系统包管理范畴) -
Type=推荐用simple(默认)或notify(需程序主动发sd_notify),避免用forking—— 容易因 PID 文件时机错乱导致状态误判 - 务必设置
Restart=on-failure或Restart=always,并配RestartSec=5防止密集重启打满日志 - 加
LimitNOFILE=65536和MemoryLimit=2G等硬限,避免单服务吃光系统资源
日志不重定向到 /dev/null,也不全靠 print 打屏
systemd 服务的标准输出/错误会自动接入 journald,但很多人仍习惯在启动脚本里加 > /dev/null 2>&1 或用 nohup,这会导致日志彻底丢失,排查时只能干瞪眼。
- 禁用所有重定向到
/dev/null的操作;如果程序本身不支持 stdout/stderr 输出,用StandardOutput=journal+StandardError=journal显式声明 - 避免在代码里用
print()或console.log()替代结构化日志;关键路径必须打level=info/error级别,并带 trace_id 或 request_id - 用
journalctl -u实时看日志;长期归档需配置-f /etc/systemd/journald.conf中的Storage=persistent和SystemMaxUse=512M
端口绑定失败?先查 ListenStream 和 CapabilityBoundingSet
服务启动报 bind: permission denied 或 Address already in use,90% 不是端口真被占,而是权限或 socket 复用配置没对。
- 非 root 服务想监听 1024 以下端口(如 80/443),不能靠
setcap 'cap_net_bind_service=+ep' /path/to/binary了事 —— 要配合CapabilityBoundingSet=CAP_NET_BIND_SERVICE和SecureBits=keep-caps,否则 cap 会在 execve 后被丢弃 -
Address already in use常见于服务异常退出后 socket 处于TIME_WAIT,加SO_REUSEADDR是应用层责任;systemd 层面可加ExecStartPre=/bin/sh -c 'ss -tuln | grep :8080 || true'做轻量预检 - 若用反向代理(如 nginx),服务应绑定
127.0.0.1:8080而非0.0.0.0:8080,避免暴露内网端口
环境变量不能写进 service 文件明文,更不能硬编码在代码里
数据库密码、API Token、密钥等敏感信息一旦出现在 .service 文件或构建产物中,就等于放进 Git 或镜像层,泄露风险极高。
- 用
EnvironmentFile=/etc/default/myapp单独存变量,该文件权限设为600,属主为root,且不在任何版本控制中 - 密码类字段禁止用
Environment=直接写在 service 文件里 ——systemctl show myapp.service会直接回显明文 - 容器化部署时,优先用
docker run --env-file或 KubernetesSecret挂载,而非-e DB_PASS=xxx - 代码中读取环境变量要加校验,比如 Python 用
os.getenv('DB_URL') or die("missing DB_URL"),而不是静默 fallback 到本地 sqlite
[Unit] Description=My App Service After=network.target [Service] Type=simple User=myapp Group=myapp WorkingDirectory=/opt/myapp EnvironmentFile=/etc/default/myapp ExecStart=/opt/myapp/bin/myapp-server Restart=on-failure RestartSec=5 LimitNOFILE=65536 MemoryLimit=2G CapabilityBoundingSet=CAP_NET_BIND_SERVICE SecureBits=keep-caps [Install] WantedBy=multi-user.target真正难的不是写完 service 文件,而是每次上线前确认
systemctl daemon-reload 执行了、systemctl enable 加入了开机自启、且 systemctl status 里看到的是 active (running) 而不是 active (exited) —— 后者往往意味着 Type 配错了或者启动命令当场返回了。










