真实体验分享:第一次配置开机脚本我是这样成功的
你有没有过这样的经历——写好了一个监控脚本、一个数据同步工具,或者一个轻量级服务,每次重启服务器后都得手动敲一遍bash /opt/mytool/start.sh?我有。上周五下午三点十七分,我盯着黑乎乎的终端,第7次输入systemctl status my_script.service,看着它报错failed to start: exit code 1,心里默念:“这次一定行。”
这不是一篇教科书式的“标准流程说明”,而是一份带着手汗、截图失败、日志翻到凌晨两点的真实记录。没有完美开局,只有踩坑、回退、再试、终于亮起绿色的active (running)。如果你也正站在开机脚本的门口犹豫不决,这篇文字就是为你写的。
1. 我为什么非得搞开机脚本不可?
先说清楚动机,不是为了炫技,而是被现实逼的。
我用的是一台部署在本地机房的 Ubuntu 22.04 服务器,跑着一个 Python 写的简易设备状态采集器(监听串口 + HTTP API)。它本身很简单,但有两个硬性要求:
- 必须在系统启动后自动运行,不能等人登录;
- 必须等网络就绪后再启动,否则连不上远程数据库;
- 一旦崩溃,最好能自动拉起来,别让我半夜爬起来 SSH。
最开始我用nohup python3 /opt/collector/main.py &手动启,结果某天断电重启,整个采集链路中断了18小时——客户发来截图问“你们的仪表盘怎么一直显示离线?”那一刻,我决定:必须上真正的开机启动。
不是“想试试”,是“不得不做”。
2. 我试了三种方法,只有一种真正稳住了
网上搜一圈,“Linux 开机启动脚本”相关教程铺天盖地。我按顺序试了三个主流方案,每一步都记下了时间、命令、报错和解决思路。下面不是罗列理论,而是还原我的真实操作流。
2.1 第一次尝试:/etc/rc.local—— 看似简单,实则埋雷
我心想:“老办法最直白。”于是编辑/etc/rc.local:
sudo nano /etc/rc.local在exit 0前加了一行:
/usr/bin/python3 /opt/collector/main.py >> /var/log/collector.log 2>&1保存,赋权:
sudo chmod +x /etc/rc.local重启测试……失败。journalctl -b | grep rc.local显示:
rc-local.service: Failed to execute /etc/rc.local: Exec format error查资料才明白:Ubuntu 22.04 默认禁用rc.local,且它的 shebang 必须是#!/bin/sh -e,而我直接往里塞了 Python 命令,没走 shell 封装。
教训:rc.local不是万能胶布,它是历史遗留接口,在 systemd 系统里需要额外启用服务,还容易因环境变量缺失导致 Python 找不到模块(比如import serial报错)。
我放弃了。不是它不行,是我不想花两小时配一个正在被淘汰的机制。
2.2 第二次尝试:cron @reboot—— 快速但不可靠
换思路:crontab -e,加一行:
@reboot /usr/bin/python3 /opt/collector/main.py >> /var/log/cron_collector.log 2>&1保存退出,sudo reboot。
开机后检查日志:空的。ps aux | grep collector:没进程。
为什么?因为@reboot的执行时机太早——它在用户 session 启动前运行,但cron的环境极简:PATH=/usr/bin:/bin,没有python3的完整路径?不对,/usr/bin/python3是绝对路径。再查,发现根本原因是:@reboot不等待网络就绪。我的脚本第一行就requests.get("https://api.example.com"),而此时网卡刚 up,路由还没通,直接 timeout 退出,且 cron 不捕获或重试。
教训:@reboot适合“启动即完事”的一次性任务(比如清临时目录),不适合依赖外部服务的长期进程。它不提供依赖声明、不记录结构化日志、失败无声无息。
我删掉了那行 crontab,关掉了这个选项。
2.3 第三次也是最终选择:systemdservice —— 慢热,但越用越踏实
这次我没抄现成模板,而是打开man systemd.service,逐行读Type=、After=、WantedBy=的含义。然后,从零建了一个最小可行单元。
2.3.1 先写脚本:加壳、加日志、加容错
我把原始 Python 脚本封装进一个 bash wrapper,叫/usr/local/bin/start-collector.sh:
#!/bin/bash # /usr/local/bin/start-collector.sh LOG_FILE="/var/log/collector/startup.log" PID_FILE="/var/run/collector.pid" echo "[$(date)] Starting collector..." >> "$LOG_FILE" # 确保工作目录存在 cd /opt/collector || { echo "[$(date)] cd failed" >> "$LOG_FILE"; exit 1; } # 检查 Python 环境(避免 pip install 后路径漂移) if ! command -v python3 >/dev/null 2>&1; then echo "[$(date)] python3 not found" >> "$LOG_FILE" exit 1 fi # 启动主程序,后台运行并记录 PID /usr/bin/python3 main.py >> /var/log/collector/output.log 2>&1 & echo $! > "$PID_FILE" echo "[$(date)] Started with PID $(cat $PID_FILE)" >> "$LOG_FILE" exit 0关键点:
- 所有路径用绝对路径;
- 加了
cd切工作目录,避免相对路径报错; - 用
command -v检查依赖,失败立刻退出并记日志; - 主进程后台运行,PID 写入文件(为后续 stop 做准备)。
赋权:
sudo chmod +x /usr/local/bin/start-collector.sh2.3.2 再建 service 文件:精准控制启动时机
创建/etc/systemd/system/collector.service:
[Unit] Description=Device Collector Service Documentation=https://internal/docs/collector After=network-online.target Wants=network-online.target [Service] Type=forking ExecStart=/usr/local/bin/start-collector.sh ExecStop=/bin/sh -c 'kill $(cat /var/run/collector.pid) 2>/dev/null || true' Restart=on-failure RestartSec=10 User=collector Group=collector Environment="PYTHONUNBUFFERED=1" StandardOutput=journal StandardError=journal [Install] WantedBy=multi-user.target逐项解释我的选择理由:
After=network-online.target:明确告诉 systemd,“等网络真通了再启动”,不是“网卡 up 就行”;Type=forking:因为我的 wrapper 启动后会 fork 出子进程并退出,符合 fork 模式;ExecStop:手动 kill,配合 PID 文件,比killall更精准;Restart=on-failure:只要 Python 进程退出码非 0,就 10 秒后重启;User=collector:我提前用sudo adduser --disabled-password --gecos "" collector创建了专用低权限用户,安全第一;StandardOutput=journal:所有输出自动进journalctl,不用自己重定向。
2.3.3 最后三步:加载、启用、验证
# 1. 重载配置(让 systemd 知道新 service) sudo systemctl daemon-reload # 2. 启用开机自启 sudo systemctl enable collector.service # 3. 立即启动(测试) sudo systemctl start collector.service验证是否成功:
# 看状态 sudo systemctl status collector.service # 应该显示 active (running) # 看实时日志 sudo journalctl -u collector.service -f # 应该滚动显示 “Starting collector...” 和 Python 输出 # 查看进程归属 ps aux | grep collector # USER 列应为 collector,不是 root我盯着journalctl -f窗口,看到第一行Started Device Collector Service时,长舒一口气。
3. 那些没写在文档里,但让我多折腾两小时的细节
官方文档不会告诉你这些,但它们真实存在:
3.1 日志里出现Permission denied?检查 SELinux 或 AppArmor
我在一台 CentOS 7 测试机上遇到ExecStart报Permission denied,明明权限是对的。查sudo ausearch -m avc -ts recent发现是 SELinux 拦截。临时放行:
sudo setsebool -P httpd_can_network_connect 1 # 或更精准:audit2allow -a -M mycollector && sudo semodule -i mycollector.ppUbuntu 上则是 AppArmor,用sudo aa-status查,临时禁用测试:sudo systemctl stop apparmor。
建议:首次调试,可先临时关闭安全模块,确认逻辑无误后再精细授权。
3.2systemctl start成功,但reboot后失败?检查WantedBy
我最初把[Install]写成WantedBy=graphical.target,结果服务器是纯命令行,没图形环境,enable实际没生效。systemctl is-enabled collector.service返回disabled。改成multi-user.target后一切正常。
验证命令:
sudo systemctl is-enabled collector.service # 应返回 enabled ls /etc/systemd/system/multi-user.target.wants/ | grep collector # 应有软链接3.3 Python 报ModuleNotFoundError?环境隔离是关键
我的脚本用了pyserial和requests,用pip3 install --user装的,但systemd下--user安装的包不在root或collector用户的PYTHONPATH里。
解法一(推荐):用venv创建独立环境:
sudo -u collector python3 -m venv /opt/collector/venv sudo -u collector /opt/collector/venv/bin/pip install pyserial requests然后在 service 文件中改ExecStart:
ExecStart=/opt/collector/venv/bin/python main.py解法二:用pip3 install --system全局安装(不推荐,污染系统)。
4. 我现在每天用的 3 个运维小习惯
脚本跑稳了,不等于可以躺平。以下是我在生产环境固化下来的日常动作:
每日晨检脚本:
写了个check-startup.sh,放在 crontab 每天 8:00 运行:#!/bin/bash if ! systemctl is-active --quiet collector.service; then echo "$(date): collector DOWN!" | mail -s "ALERT: collector down" admin@example.com systemctl start collector.service fi日志轮转:
防止/var/log/collector/塞爆磁盘,创建/etc/logrotate.d/collector:/var/log/collector/*.log { daily missingok rotate 30 compress delaycompress notifempty create 644 collector collector }一键重装 service(开发迭代用):
当我要更新脚本逻辑,不想反复disable/enable,写了个reinstall-collector.sh:sudo systemctl stop collector.service sudo systemctl disable collector.service sudo rm /etc/systemd/system/collector.service sudo systemctl daemon-reload # ... 复制新脚本、新 service 文件 ... sudo systemctl enable collector.service sudo systemctl start collector.service
5. 给新手的 4 条真心话
写到这里,我想把这周踩过的坑,浓缩成几句大实话:
- 不要追求“一次写对”:
systemd配置不是代码,是系统契约。daemon-reload+start+status+journalctl是你的黄金四件套,每改一行就跑一遍,比看十篇教程管用。 - 日志是你唯一的证人:所有
echo、所有>>、所有journalctl,不是为了凑字数,是当你深夜被告警叫醒时,唯一能告诉你“它在哪一步倒下”的线索。 - 权限最小化不是教条,是生存法则:用
root跑脚本就像穿睡衣开飞机。创建专用用户、用venv、禁用密码登录,这些多花的15分钟,会在某次漏洞爆发时救你一命。 - “能跑”和“稳跑”之间,隔着 10 次重启:我是在第 5 次重启后才发现
After=network.target不够,必须After=network-online.target;是在第 8 次后才给RestartSec=10加上,避免高频重启打崩数据库。稳定,是时间喂出来的。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。