ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Ubuntu开机自动执行脚本完全指南:systemd、crontab与rc.local实战

Ubuntu开机自动执行脚本完全指南:systemd、crontab与rc.local实战 这个需求看起来简单实际操作倒有不少门道。Ubuntu 系统里想实现“开机或重启后自动执行 / play a script called run1.sh”其实并不只是往启动目录丢一个文件就完事里面涉及 systemd 的启动顺序、脚本运行环境、用户权限、工作目录这些细节踩过一次坑就长记性了。我一开始也是直接把 run1.sh 扔进 /etc/rc.local 指望它自动跑结果重启之后右等左等没效果后来才发现 Ubuntu 18.04 之后默认就不执行 rc.local 了得自己手动把服务拉起来。这篇文章把这几种常见做法全部拆开讲配上我实际调试过程中的踩坑记录你可以根据自己的脚本类型、网络依赖和运维习惯直接选一种来用。无论是刚接触 Ubuntu 的小白还是被各种启动脚本折磨过的老手照着操作基本都能跑通。1. 先说清楚开机启动这个事本质是和 systemd 打交道1.1 从按下开机键到脚本运行中间发生了什么很多人以为“开机启动脚本”就是把脚本放在某个固定位置系统重启后自动执行。但现代 Ubuntu 从 15.04 开始就用 systemd 作为 init 系统了系统的启动过程大概是 BIOS/UEFI → GRUB → 内核 → initramfs → systemd(PID 1) → 各种 target 单元。systemd 会按照不同单元之间的依赖关系把服务并行或者串行地启动起来。你写的 run1.sh 如果是要跟着系统开机运行就要让 systemd 在启动阶段把它拉起来或者找一个系统启动时固定会执行的方式挂进去。理解了这一点很多配置问题就说得通了为什么脚本明明有执行权限还是启动失败为什么脚本里用到的网络命令在启动时连不通为什么写日志的文件目录在开机那一刻还不存在。这些不是脚本写得不好而是启动时机和环境上下文不对。1.2 常见的三把刀systemd 服务、crontab reboot、rc.local目前在 Ubuntu 上实现开机/重启执行脚本最常见的有三条路给脚本写一个 systemd 服务单元设置开机自启。这是最规范、最推荐的方式能够控制依赖、日志、用户、重启策略。用 cron 的 reboot 特殊时间字段在用户 crontab 里挂一条命令。优点是配置极简单适合快速处理缺点是环境变量和日志管理比较弱。使用传统的 /etc/rc.local 文件。这是老派运维的惯性选择但 Ubuntu 18.04 以后默认不加载这个文件需要手动启用 rc-local.service不然写了一堆脚本等于白写。我这些年折腾下来的习惯是凡是比较重要的脚本优先用 systemd 服务凡是临时性、一次性、调试用的脚本用 crontab reboot 图个快rc.local 基本只用来处理那种“不管三七二十一开机之后环境已经就绪随便跑点啥”的场合。2. 最稳的做法给 run1.sh 写一个 systemd 服务2.1 服务单元文件可以这样写在 /etc/systemd/system/ 下创建一个服务文件名字随便比如 run1.service。内容我直接给一个我测试过很多遍的“标准版本”[Unit] DescriptionRun run1.sh at startup Afternetwork-online.target Wantsnetwork-online.target [Service] Typesimple Userroot Grouproot WorkingDirectory/opt/scripts ExecStart/opt/scripts/run1.sh StandardOutputjournal StandardErrorjournal Restartno [Install] WantedBymulti-user.target这里的几个关键字段我逐个解释一下。Afternetwork-online.target的作用是告诉 systemd尽量在网络就绪之后再启动这个服务。如果你的 run1.sh 里有 curl、wget、ping 这类需要网络的操作这个字段很重要。但不是所有 Ubuntu 系统都会等网络完全可用建议配合Wantsnetwork-online.target一起写提示系统尽量满足网络在线。ExecStart/opt/scripts/run1.sh是真正执行你脚本的入口。注意这里一定要写绝对路径不要写相对路径因为 systemd 启动服务时的工作目录不一定是脚本所在目录相对路径分分钟找不到文件。如果你脚本内部引用了其他文件最好在脚本开头先cd到脚本所在目录或者在服务里设置WorkingDirectory。Typesimple表示 systemd 认为脚本一旦被启动就处于运行中服务状态。如果你的 run1.sh 是一个一次性任务执行完就退出那也可以改用Typeoneshot并且加一个RemainAfterExityes这样服务状态看起来会比较干净不会出现“明明脚本跑完了却看到服务失败”的情况。2.2 启用服务之前少一步都白搭写完服务文件后需要用下面的命令让 systemd 重新加载配置并且设置开机自启sudo systemctl daemon-reload sudo systemctl enable run1.service sudo systemctl start run1.servicedaemon-reload很多人会漏掉。如果你改过服务文件里的内容不执行这个命令直接 startsystemd 可能还在用旧的内存配置你会看到一个诡异的“服务已启动但表现完全不对”的现象。全部执行完以后检查一下状态systemctl status run1.service journalctl -u run1.service -n 50 --no-pager如果你看到 Active: active (running) 或者 Active: inactive (dead) 但日志显示正常这就说明服务已经成功执行过了。如果是要验证“开机启动”本身是否生效最直接的办法就是执行sudo reboot重启一次重启后回来再看服务状态和日志。我特别想强调一个实际经验第一次配置 systemd 服务时建议先手动sudo systemctl start run1.service验证脚本本身没问题再重启验证开机自启免得脚本出问题时你都不知道是该排查脚本还是排查服务配置。2.3 输出日志和调试信息这样接systemd 的一个很大的优势是把 stdout 和 stderr 都收集起来了。你可以在服务文件里写StandardOutputjournal StandardErrorjournal这样脚本里用 echo 打出来的信息都会被记录到 journald 中查看方式就是journalctl -u run1.service。如果脚本内部自己还有日志逻辑比如echo start $(date) /var/log/run1.log那就要保证脚本运行用户对那个日志文件有写权限。systemd 默认以 root 用户运行这通常没问题。但我遇到过一个比较常见的坑日志文件以前用某用户手动创建过文件属主是普通用户而 systemd 服务里又指定了别的 User结果日志怎么都写不进去服务还反复重启。后来我用绝对路径重新创建日志文件并调整权限问题才解决。2.4 脚本执行完就退出服务状态如何看如果你的 run1.sh 就是一个安装环境、初始化配置、同步文件这类一次性脚本跑完就没需求继续“活着”了那么 Typeoneshot 更合适[Service] Typeoneshot RemainAfterExityes ExecStart/opt/scripts/run1.shRemainAfterExityes的意思是即使脚本执行完退出了这个服务仍然标记为 active。这样你在命令行看服务状态时不会一脸懵不会看到一堆 failed 红色标记。实际上很多运维脚本挂 systemd 时都用 oneshot因为脚本本身不是常驻进程真正的常驻程序才应该用simple或者forking。3. 两条命令就能跑crontab 的 reboot 写法3.1 一行配置挂到用户 crontab 里如果你不想写 systemd 服务文件或者说这只是个临时脚本那用 cron 的 reboot 是最快的办法。打开当前用户的 crontabcrontab -e在文件最后加一行reboot /opt/scripts/run1.sh /var/log/run1.log 21保存退出后下次系统重启时cron 服务会把这一条命令执行一次。注意看后半段 /var/log/run1.log 21这我强烈建议你加上因为 cron 环境下面脚本的错误输出很容易消失你加完这一条脚本打印的内容都落到日志文件里事后排查会省很多事。这个写法的好处是简单对 cron 不熟悉的用户也容易上手。但要注意cron 的 reboot 触发时机比 systemd 服务要晚而且不保证网络已经完全可用。如果你的 run1.sh 要连远程服务器、要访问数据库可能要自己在脚本里做等待重试。3.2 用户环境这个坑cron 比 systemd 更明显用 cron 执行命令时不会自动加载用户的 .bashrc、.profile 这些环境变量文件所以 PATH 是很干净的默认值。这意味着你在终端里用着很顺手的命令比如 docker compose、python3 -m venv、node、yarn在 cron 环境下可能直接 command not found。我举一个实际例子有个脚本里用了python3 /home/user/run1.py在命令行手动执行一切正常结果 reboot 后一点反应都没有。最后查日志才发现cron 环境下的 PATH 不包含 /usr/local/bin而那个 python3 链接恰好装在 /usr/local/bin 里。解决办法有两个要么在脚本开头写#!/bin/bash export PATH/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin要么在 crontab 里显式指定绝对路径reboot /usr/local/bin/python3 /home/user/run1.py别笑话这种问题实际工作中这种“手动跑没问题、自动跑就是不行”的情况绝大多数都是环境变量惹的祸。3.3 有延迟需求给 reboot 加上 sleep还有一个小技巧如果你想等系统完全稳定下来再执行脚本可以写reboot sleep 20 /opt/scripts/run1.sh或者让脚本内部自己写sleep 20。我通常会给“需要访问外部服务”的脚本加 5 到 20 秒的等待时间。因为开机瞬间系统的网络栈可能还在初始化你立刻访问外网或者内网服务连接经常会超时等几秒钟成功率会高非常多。4. 传统方案把 rc.local 从历史棺材里拉出来4.1 为什么 Ubuntu 18.04 以后 rc.local 默认没用了早年间 Ubuntu 用 SysVinit/etc/rc.local 是开机脚本人见人爱的小板子往里面丢一行命令就行。后来 systemd 接管了开机流程Ubuntu 18.04 开始 rc-local.service 默认不启用了。有的朋友还停留在“只要把脚本扔进 rc.local 就能执行”的旧经验里结果换了新系统之后发现怎么都不生效。现行做法是如果确实想用 rc.local需要确认系统里是否存在 /lib/systemd/system/rc-local.service 这个文件。在 Ubuntu 22.04、24.04 上这个服务文件还在只是没有启用也没有 /etc/rc.local 文件。手动补上就行。4.2 手动启用 rc-local 的完整步骤一步步操作先把 /etc/rc.local 创建出来sudo vi /etc/rc.local内容写上#!/bin/bash /opt/scripts/run1.sh exit 0然后赋予执行权限sudo chmod x /etc/rc.local接着启用并启动服务sudo systemctl enable rc-local.service sudo systemctl start rc-local.service执行systemctl status rc-local.service看一下状态如果是 active (exited)那就成功了。之后再重启系统rc.local 里的内容就会在启动时被自动执行。注意一个细节/etc/rc.local 最后我还加了一个exit 0这是老传统意思是告诉调用者这个脚本正常结束。如果你漏写某些瘦身过的 systemd 配置可能会认为脚本异常退出。另外这个文件里的命令也是在 root 环境下跑的脚本里不需要再用 sudo否则反而容易节外生枝。4.3 rc.local 适合什么不适合什么rc.local 最大的优点是“无脑”往里面塞命令开机就跑不用考虑单元文件语法也不用管 Type、After 这些概念。它适合那种“系统启动之后随便什么阶段把 run1.sh 执行一遍就行”的场景比如同步时钟、备份目录、清理临时文件、初始化硬件参数。它最大的缺点是没有直观的依赖和失败处理。如果 run1.sh 在 network 没起来时执行脚本里网络请求失败rc.local 不会自动重试也不会把日志归档一切只能靠脚本自己处理。而且多脚本同时跑的时候还得靠脚本内部分号、换行来控制顺序远不如 systemd 的依赖管理清晰。所以如果让你做生产环境的开机启动项目我的建议仍然是优先 systemd。rc.local 更适合用来处理那些“系统里其他服务都不重要我就是要执行这个兜底脚本”的场景。5. run1.sh 自身的细节决定你能不能少熬夜5.1 可执行权限和 shebang 必须搞好很多人配置文件倒是写得没问题结果 run1.sh 执行失败原因是文件本身没有执行权限。命令行里先确认一下ls -l /opt/scripts/run1.sh如果没有 x 权限执行chmod x /opt/scripts/run1.sh还要确保文件第一行有 shebang#!/bin/bash或者你用 zsh、python 写的就写对应的解释器路径。systemd 执行脚本时如果文件没有 x 权限或者没有 shebang会直接报 permission denied 或者 exec format error。这个报错在 journalctl 里非常常见属于新手大坑。另外文件的行尾符也要注意。我有一次拿到一个在 Windows 上编辑过的脚本文件里带了 \r\n 回车符shebang 变成了#!/bin/bash\rbash 会误以为要找不存在的解释器各种诡异报错。可以用cat -A /opt/scripts/run1.sh查看有 ^M 的话就用sed -i s/\r$// /opt/scripts/run1.sh清理。5.2 脚本里的命令、路径、日志全部尽量用绝对路径前面提过systemd 和 cron 默认的工作目录都不是脚本目录。run1.sh 里如果写的是./config.ini那大概率会找不到这个文件。稳健的做法是在脚本开头就切换目录#!/bin/bash cd /opt/scripts然后再用相对路径去读文件。或者干脆养成一个习惯脚本里所有引用其他文件的位置都写全路径。比如cp /opt/scripts/config.ini /etc/backup/config.ini日志输出也是同一个道理最好指定到 /var/log/ 下固定位置。因为脚本跑在开机阶段环境不完整$HOME可能不是你想要的那个目录盲目用~写日志容易把日志写到 root 或某个默认用户的主目录里到时候找半天才发现。命令本身也尽量写绝对路径。怎么找命令路径在终端里执行which python3、which curl得到路径之后直接写进脚本。这不是强迫症而是开机启动阶段 PATH 环境变量真的不可控绝对路径能帮你避开一个巨大的坑。5.3 网络依赖的脚本要有等待和重试如果你的 run1.sh 里有访问外部服务的动作比如通过 curl 请求一个 API、拉取远程文件、连接数据库那就必须有等待和重试逻辑。系统刚启动时network-online.target 只是说网络配置已经就绪但到真正能够解析 DNS、连上外网可能还有一小段时间。我的常规做法是在脚本里加一个简单的循环for i in {1..30}; do if ping -c1 -W1 example.com /dev/null; then break fi sleep 2 done或者用 curl 的重试参数curl --retry 5 --retry-delay 3 --retry-all-errors https://example.com这种方式比单纯 sleep 20 要聪明一些网络早点就绪就快点执行晚点就绪也能等到而不是固定等 20 秒浪费启动时间。6. 遇到问题别慌常见坑和排查路径6.1 开机执行了但没效果先看日志位置我排查的时候第一件事永远是看脚本有没有真的跑起来。systemd 服务法用journalctl -u run1.servicecrontab 法用日志重定向到 /var/log/run1.logrc.local 法靠写日志或者看systemctl status rc-local.service。很多“脚本没执行”的假象其实是脚本执行了但缺依赖失败了而你又没有日志才看起来像没跑。6.2 报错 permission denied 的基本排查顺序/permission denied 是这类问题里最常见的报错之一。我发现大多数情况是以下三种之一/etc/rc.local 或者 run1.sh 没有可执行权限。run1.sh 里有相对路径而启动时的工作目录不是脚本目录导致引用的文件打不开。脚本要读取的关键文件权限不足比如普通用户身份跑 systemd 服务但脚本要写 /root/ 下的文件。排查顺序也很简单先chmod x解决明显的执行权限再用绝对路径替换所有文件引用最后在脚本开头加几条调试输出让脚本运行到了哪一行都能在日志里看得到。这种办法虽然笨但面对开机脚本这类极难复现场景时是最有效的。6.3 systemd 到底生效没有看这几处如果服务已经 enable 了但你怀疑它没有真正开机组到可以看符号链接ls -l /etc/systemd/system/multi-user.target.wants/run1.service如果能看到一个指向 /etc/systemd/system/run1.service 的链接说明 enable 成功了。看不到就说明 enable 没执行或者执行后又被别的工具覆盖了。另外如果改过服务文件但没daemon-reloadsystemd 会沿用旧配置这个是超高频坑改完文件千万记得 reload。6.4 cron reboot 不执行可能要怀疑 cron 服务本身在 Ubuntu 上cron 守护进程默认是安装并运行的。但有些精简环境里可能没有 cron或者服务没启动。先确认一下systemctl status cron如果 cron 没跑先启动并设置自启。另外用户级 crontab 里写 reboot是在该用户登录之前就执行的吗实际上是系统启动时 cron 会扫描所有用户的 crontab然后以对应用户身份执行 reboot 任务。如果你的 run1.sh 需要某些用户特定的环境变量cron 里也拿不到这也是一个容易让人困惑的点。6.5 服务文件改了 n 次系统还是“老样子”这种时候除了daemon-reload之外还要注意是否有别的同名服务在捣乱。比如你创建一个 run1.service但系统里可能已经存在同名的其他服务导致 enable 时提示找不到或者存在又或者权限冲突。碰到诡异问题先systemctl cat run1.service查看最终生效的单元文件内容到底长什么样很多配置问题一看就明白了。7. 三种方案怎么选我的实际建议这三种方式没有绝对的优劣具体看场景。为了让你一眼判断我把它们的特性差异列成了表对比点systemd 服务crontab rebootrc.local配置复杂度中要写单元文件低一行命令低写个脚本文件启动时机受 after/依赖控制较早比较晚cron 启动后晚基本是启动收尾阶段日志管理journalctl 统一收集强弱要靠重定向到文件弱靠脚本自己处理环境变量需要自行配置几乎为空很基础适合场景长期运行服务或重要任务临时、简单的启动任务老系统习惯、兜底脚本如果 run1.sh 是那种长期运行的服务比如启动一个 web 服务、一个监控进程那不用想了直接 systemd 是最标准的还能配合 Restarton-failure 做崩溃重启。如果 run1.sh 只是做一次性的环境准备、数据同步加个 crontab reboot 就够省事。如果你本身对 systemd 语法不熟只想把以前老机器上的一套搬过来用rc.local 也是可以救回来的毕竟它语法门槛基本为零。我个人实际操作的体会是哪怕最后选了 crontab 或者 rc.local也建议你在第一次调试时用 systemd 跑一遍看日志会方便很多。等彻底确认 run1.sh 行为正常再迁回你选定的方式来。还有一个小技巧生产服务器验证开机脚本最好先找一台同镜像的虚拟机或者低峰期来因为每重启一次你都是在和整个系统的启动时序赛跑这些经验是真的要在实操中才能得出来。
返回列表