Linux服务器如何设置开机自启动?Systemd服务配置教程

自己写的应用部署到服务器上,重启后不会自动运行,这是新手必然遇到的第一个问题。早年的做法是往 /etc/rc.local 里加一行,或者用 crontab 的 @reboot,但这两者都没有依赖控制、没有自动重启、没有统一的日志,服务挂了也没人管。

现代 Linux 发行版(RHEL 7 及以上、CentOS 7 及以上、Rocky、AlmaLinux、Debian 8 及以上、Ubuntu 16.04 及以上)都使用 systemd 作为 init 系统。用 systemd 管理自启动,你能获得开机自动拉起、崩溃自动重启、依赖顺序控制、资源限制、日志统一收集这一整套能力。这篇文章讲清楚 unit 文件的三个段落、Type 与 Restart 的取值、enable 的原理,并给出 Node.js、Java、Gunicorn 三个可直接改用的实例,最后对比 rc.local 与 crontab @reboot 的适用边界。

systemd 与 unit 的基本概念

systemd 是 PID 1 的 init 进程,负责启动和管理系统中的所有服务。它管理的对象统称 unit,用 unit 文件描述,常见类型:

类型 后缀 用途
Service .service 服务进程,本文主角
Target .target 一组 unit 的逻辑分组,类似旧的运行级别
Timer .timer 定时任务,可替代 cron
Socket .socket socket 激活,按需启动服务
Mount .mount 挂载点
Path .path 路径变化时触发任务
Slice .slice 资源分组,如 cgroup 层级

unit 文件有三个存放位置,优先级从高到低:

目录 用途 优先级
/etc/systemd/system/ 管理员自定义,推荐使用 最高
/run/systemd/system/ 运行时生成,临时 中
/usr/lib/systemd/system/(RHEL 系)
/lib/systemd/system/(Debian 系)
软件包安装 最低

自定义服务一律放 /etc/systemd/system/,不要修改软件包安装的文件,因为软件升级会覆盖它。想修改已有服务的参数,用 drop-in 目录:

sudo systemctl edit nginx.service
# 会创建 /etc/systemd/system/nginx.service.d/override.conf

unit 文件结构

一个典型的 service unit 分三段,每段有明确的职责:

[Unit]
Description=我的应用服务
Documentation=https://example.com/docs
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=appuser
Group=appgroup
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/node /opt/myapp/server.js
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

[Unit] 段:描述与依赖关系

指令 含义
Description 服务描述,systemctl status 里会显示
Documentation 文档地址
Before= / After= 定义启动顺序,不表示依赖
Requires= 强依赖,依赖失败则本服务失败
Requisite= 依赖必须已启动,否则直接失败
Wants= 弱依赖,依赖失败不影响本服务
BindsTo= 强关联,依赖停止则本服务也停止
Conflicts= 冲突,两者不能同时运行
ConditionPathExists= 条件成立才启动

After 和 Requires 的区别必须搞清楚。 After=mysql.service 只表示"在 mysql 之后启动",如果 mysql 没装,本服务照常启动。Requires=mysql.service 才表示"mysql 必须成功启动,否则本服务也不启动",但它不保证顺序,所以通常两者一起写:

Requires=mysql.service
After=mysql.service

网络依赖是个高频坑。network.target 只表示网络配置已开始,不代表真的能上网;network-online.target 才表示网络真正就绪。需要联网的服务(比如启动后要调外部接口、要注册到注册中心)必须这样写:

After=network-online.target
Wants=network-online.target

同时要确保系统的网络等待服务已启用:systemctl enable --now NetworkManager-wait-online.service(使用 NetworkManager 时)或 systemd-networkd-wait-online.service。

[Service] 段:执行细节

指令 含义
Type 进程类型,见下节
ExecStart 启动命令,必须有绝对路径
ExecStartPre / ExecStartPost 启动前后执行
ExecStop 停止命令
ExecReload 重载命令
User / Group 运行身份
WorkingDirectory 工作目录
Environment 环境变量,可多行
EnvironmentFile 从文件读取环境变量
Restart 重启策略
RestartSec 重启前等待秒数
TimeoutStartSec / TimeoutStopSec 启动停止超时
KillSignal 停止时发送的信号,默认 SIGTERM
StandardOutput / StandardError 输出去向,默认 journal
LimitNOFILE 文件描述符上限
PrivateTmp 私有 /tmp 目录
ProtectSystem 保护系统目录只读

几个易错点:ExecStart 必须是绝对路径,不能写 node server.js;命令中不支持 shell 的管道和重定向,需要的话要显式调用 shell:

ExecStart=/bin/bash -c '/usr/bin/node /opt/app/server.js >> /var/log/app.log 2>&1'

User 不写默认是 root,生产环境应创建专用用户,遵循最小权限原则。Environment 可以写多行,也可以用 EnvironmentFile 引入一个 KEY=VALUE 格式的文件,后者更适合管理密码等配置(文件权限设 600)。

[Install] 段:启用时的安装信息

这段决定 systemctl enable 会把服务的软链接放到哪里:

[Install]
WantedBy=multi-user.target

WantedBy 表示"当这个 target 被启动时,需要本服务"。最常用的 target:

target 含义
multi-user.target 多用户命令行模式,服务器默认
graphical.target 图形界面模式
network-online.target 网络就绪
default.target 默认 target 的软链接

也可以用 RequiredBy 表示强依赖。日常用 WantedBy=multi-user.target 就够了。

查看当前默认 target:

systemctl get-default
sudo systemctl set-default multi-user.target

Type 类型的选择

Type 决定了 systemd 如何判断服务"启动完成",选错会导致启动超时或者误判失败。

Type 适用场景 systemd 的判定方式
simple 默认,前台运行的程序 认为 ExecStart 进程就是主进程,启动即完成
exec 类似 simple,但等 exec 完成 等 execve 成功后才算启动
forking 传统守护进程,会 fork 到后台 等父进程退出,用 PIDFile 确定主进程
oneshot 一次性任务,执行完就退出 等进程退出才算完成
notify 支持 sd_notify 的程序 等程序主动发 READY=1
dbus 通过 D-Bus 注册的服务 等 BusName 出现在 D-Bus
idle 等其他任务空闲后执行 延迟执行,极少用

判断方法很简单:程序启动后一直占着前台不退出(Node、Python、Go、Java 主类),用 simple;程序自己 fork 到后台并写 pid 文件(Nginx、传统 PHP-FPM、Redis 的旧配置),用 forking;跑完就退出的脚本(初始化数据、清理任务),用 oneshot。

forking 必须配合 PIDFile 使用,否则 systemd 找不到主进程:

Type=forking
PIDFile=/run/myapp/myapp.pid

oneshot 常用于初始化任务,通常需要加 RemainAfterExit=yes 让 systemd 把服务标记为 active,否则执行完状态变成 inactive:

Type=oneshot
RemainAfterExit=yes

Restart 策略

Restart 决定进程退出后 systemd 是否自动重启,这是 systemd 相比 rc.local 的核心优势之一。

取值 行为
no 不重启,默认
on-success 仅在正常退出(码 0)时重启
on-failure 异常退出、被信号终止、超时才重启,最常用
on-abnormal 被信号终止或超时才重启
on-watchdog 看门狗超时才重启
on-abort 因未捕获信号终止才重启
always 无论如何都重启,包括正常退出

推荐配置:

Restart=on-failure
RestartSec=5
StartLimitIntervalSec=300
StartLimitBurst=5

StartLimitBurst 与 StartLimitIntervalSec 组合,表示在指定时间窗口内最多重启多少次,超过后 systemd 会放弃并把服务置为 failed,避免"崩溃—重启—再崩溃"的无限循环打满日志和 CPU。这是必须配置的安全阀。

如果程序本身是跑批任务(跑完就该退出),不要用 Restart=always,否则它会一遍遍重复执行。

完整操作流程

写完 unit 文件后,按这个流程操作:

sudo vi /etc/systemd/system/myapp.service
sudo systemctl daemon-reload          # 修改了 unit 文件必须执行
sudo systemctl enable --now myapp     # 设开机自启并立即启动
systemctl status myapp --no-pager
journalctl -u myapp -f                # 实时看日志

daemon-reload 是新手最容易忘记的一步。它的作用是让 systemd 重新读取 unit 文件,不做的话你的修改不会生效,表现是"明明改了配置,重启服务还是老样子"。

其他常用命令:

systemctl start myapp
systemctl stop myapp
systemctl restart myapp
systemctl reload myapp              # 需要服务支持 ExecReload
systemctl enable myapp              # 只设自启,不立即启动
systemctl disable myapp             # 取消自启
systemctl is-enabled myapp
systemctl is-active myapp
systemctl daemon-reload             # 重载 unit 配置
systemctl list-units --type=service --state=running
systemctl list-unit-files --type=service | grep enabled
systemctl --failed                  # 只看失败的
systemctl cat myapp                 # 查看最终生效的完整配置
systemctl show myapp                # 查看所有属性

systemctl cat 非常有用,它会显示 unit 文件本体加上所有 drop-in 覆盖,能确认最终生效的配置是什么。

enable 到底做了什么? 它只是在 /etc/systemd/system/multi-user.target.wants/ 目录下创建了一个指向 unit 文件的软链接。disable 就是删掉这个链接。所以手工创建软链接也能达到同样效果,但不推荐,用标准命令更可靠。

验证自启配置是否正确:

systemctl is-enabled myapp
ls -l /etc/systemd/system/multi-user.target.wants/ | grep myapp

三个可直接改用的实例

Node.js 应用

[Unit]
Description=Node.js 应用服务
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=nodeapp
Group=nodeapp
WorkingDirectory=/opt/nodeapp
Environment=NODE_ENV=production
Environment=PORT=3000
ExecStart=/usr/bin/node /opt/nodeapp/server.js
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=nodeapp
LimitNOFILE=65535

[Install]
WantedBy=multi-user.target

用 nvm 安装的 Node 路径可能是 /home/用户名/.nvm/versions/node/v20.x/bin/node,要用 which node 或 readlink -f $(which node) 确认真实路径。不要写 ExecStart=npm start,npm 会额外包一层进程,信号传递和退出码都会出问题。

Java 应用

[Unit]
Description=Java 应用服务
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=javaapp
WorkingDirectory=/opt/javaapp
Environment=JAVA_HOME=/usr/lib/jvm/java-17-openjdk
EnvironmentFile=/opt/javaapp/app.env
ExecStart=/usr/lib/jvm/java-17-openjdk/bin/java \
  -Xms512m -Xmx1024m \
  -jar /opt/javaapp/app.jar \
  --spring.profiles.active=prod
SuccessExitStatus=143        # SIGTERM,让优雅停机不被判为失败
Restart=on-failure
RestartSec=10
TimeoutStopSec=60            # 给 JVM 足够时间优雅关闭
LimitNOFILE=65535

[Install]
WantedBy=multi-user.target

SuccessExitStatus=143 是个实用技巧:systemd 停止服务时发 SIGTERM,Java 收到后退出码是 143,如果不声明,systemd 会认为服务异常退出。TimeoutStopSec 给 JVM 足够时间执行 shutdown hook,避免强行 kill 导致事务中断。

Python Gunicorn

[Unit]
Description=Gunicorn 服务
After=network-online.target
Wants=network-online.target

[Service]
Type=notify
User=webapp
Group=webapp
WorkingDirectory=/opt/webapp
Environment=PATH=/opt/webapp/venv/bin:/usr/bin:/bin
EnvironmentFile=/opt/webapp/.env
ExecStart=/opt/webapp/venv/bin/gunicorn \
  --workers 4 \
  --bind 127.0.0.1:8000 \
  --access-logfile - \
  --error-logfile - \
  app:application
ExecReload=/bin/kill -HUP $MAINPID
KillSignal=SIGTERM
Restart=on-failure
RestartSec=5
RuntimeDirectory=gunicorn

[Install]
WantedBy=multi-user.target

Type=notify 需要 Gunicorn 安装 gunicorn[gevent] 之外的 sd_notify 支持,实际使用中如果程序不发 READY 信号会导致启动超时,稳妥起见可以改用 Type=simple。RuntimeDirectory=gunicorn 会让 systemd 自动创建 /run/gunicorn 并设置权限,比手工 mkdir 干净。

启动失败怎么排查

systemctl status myapp --no-pager
journalctl -u myapp -n 50 --no-pager
journalctl -u myapp --since '-10 min' --no-pager
journalctl -xe --no-pager
systemd-analyze verify /etc/systemd/system/myapp.service

systemd-analyze verify 能检查 unit 文件的语法和常见错误,写完配置后先跑一遍很有必要。

常见原因按出现频率排序:

一是路径问题。 ExecStart 用了相对路径或者程序不在 PATH 里。用 command -v 确认绝对路径。

二是权限问题。 User 指定的用户对 WorkingDirectory、日志目录、配置文件没有读写权限。检查:

sudo -u appuser ls -l /opt/myapp
sudo -u appuser test -r /opt/myapp/config.yaml && echo "可读"

三是端口冲突或依赖未就绪。 看日志里有没有 Address already in use,用 ss -lntp 确认。

四是环境变量缺失。 systemd 不会加载用户的 .bashrc。用 Environment 或 EnvironmentFile 显式声明,这一点和 crontab 的坑一样。

五是 SELinux 限制(RHEL 系)。 自定义路径和端口可能被 SELinux 拒绝,看 /var/log/audit/audit.log 有没有 denied,或临时用 setenforce 0 验证是不是这个原因(验证后记得恢复):

sudo ausearch -m avc -ts recent
sudo journalctl -t setroubleshoot --no-pager

六是 Type 选错导致超时。 前台程序写成 forking 会一直等待父进程退出,最终超时失败。

rc.local 与 crontab @reboot 的对比

这两者仍然可用,但适用场景已经很窄。

维度 systemd service rc.local crontab @reboot
依赖顺序 精确控制 After、Requires 无,几乎最后执行 无,cron 启动时执行
崩溃自动重启 支持 Restart 不支持 不支持
日志 统一进 journald 需自行重定向 需自行重定向
资源限制 支持 CPU、内存、句柄限制 不支持 不支持
停止与重载 标准命令 需自己写脚本 无停止概念
状态查看 systemctl status 无 无
适用场景 所有常驻服务 一次性开机脚本 简单的启动钩子

rc.local 在新系统上默认不存在,需要手工创建并赋予执行权限,还要有一个 systemd 单元去调用它:

sudo chmod +x /etc/rc.local
sudo systemctl enable --now rc-local

Debian、Ubuntu 上对应的是 /etc/rc.local,RHEL 系是 /etc/rc.d/rc.local,且都需要 chmod +x。多数 systemd 系统自带 rc-local.service 单元,但默认未启用。

结论:常驻服务一律用 systemd service;只有那种"开机时跑一次、跑完就结束、不需要管理"的初始化脚本才考虑 rc.local。 比如开机时设置某个内核参数、初始化目录权限、写入一个标记文件。@reboot 的限制更多,它连执行顺序都保证不了,只有最简单的场景才用。

常见问题

改了 unit 文件,重启服务没生效?

需要 systemctl daemon-reload。这不是重启服务,而是让 systemd 重新读取配置文件。修改任何 unit 文件后都必须执行它,然后再 systemctl restart。

enable 之后重启机器服务还是没起来?

先确认 systemctl is-enabled 返回 enabled,再看 journalctl -b -u 服务名 查上次启动时的日志。常见原因是依赖的 target 没达到(比如 After=network-online.target 但 wait-online 服务没启用),或者服务在启动阶段就失败了。

Type 该选 simple 还是 forking?

看程序行为。启动后一直占着终端不退出的是 simple;自己 fork 到后台、父进程立刻退出并留下 pid 文件的是 forking。不确定时先试 simple,如果 systemd 一直卡在 activating 状态直到超时,就改成 forking 并配 PIDFile。

服务一直重启,刷爆日志怎么办?

配置 StartLimitBurst 和 StartLimitIntervalSec 限制重启次数。同时用 systemctl reset-failed 服务名 清除失败计数。根本解决还是要修掉崩溃原因,看 journalctl -u 服务名 里的退出原因。

想让服务只运行一次,不要自动重启?

用 Type=oneshot 配合 RemainAfterExit=yes,Restart 设为 no。这种配置适合开机初始化任务,比如导入初始数据、预热缓存。

多个服务有先后顺序怎么配?

在被依赖的服务里写 Requires=依赖服务 加上 After=依赖服务,不要在依赖方写 Before。如果只需要顺序不需要强依赖,用 Wants 和 After 组合。target 可以用来聚合一组服务,先启动 target 再启动业务服务。

总结

  • 自定义服务放在 /etc/systemd/system/,修改已有服务用 systemctl edit 创建 drop-in 覆盖,不要改软件包自带的文件。
  • unit 三段各司其职:[Unit] 管描述与依赖(After 管顺序、Requires 管成败,两者常一起写),[Service] 管执行细节,[Install] 的 WantedBy=multi-user.target 决定 enable 时的挂载点。
  • Type 按程序行为选择:前台常驻用 simple,自己 fork 到后台用 forking 并配 PIDFile,一次性任务用 oneshot 加 RemainAfterExit=yes。
  • 必配 Restart=on-failure 与 StartLimitBurst 限流,前者保证崩溃自动拉起,后者防止无限重启打满资源。
  • 改完 unit 文件必须 systemctl daemon-reload,排查用 systemctl status 加 journalctl -u,rc.local 和 @reboot 只适合不需要管理的开机一次性脚本。