
简介这套一键修复与安装脚本聚焦 Linux 系统修复与服务器环境安装的自动化场景面向运维工程师、系统管理员以及有基础的 Linux 爱好者。脚本覆盖系统检测、修复策略、安装流程、自动化配置与用户交互等环节支持 Ubuntu、CentOS、Debian 等多种发行版并适配 Web、数据库、邮件等常见服务器环境能显著降低手动操作带来的风险和工作量。资源包共 24 个文件以 Shell 脚本为主17 个 sh包含网络配置、日志清理、软件包安装、环境部署等用途另有 2 个 YAML 编排文件、2 个说明文本、2 个 Markdown 文档和 1 个许可证。整体压缩包仅 44KB结构清晰便于阅读和二次修改。目前已有 150 人学习下载。通过此脚本包使用者可快速掌握一键修复与安装的编排思路获得可直接运行或定制的自动化脚本同时也能从 README 与实例中学习到多发行版系统检测、错误恢复等实用技巧是提升服务器运维效率的实用工具。1. 为什么 linux 一键修复与安装脚本要用 One-click 的方式一台 CentOS 7 因为 yum 锁卡死旁边的 Ubuntu 22.04 因为 dpkg 中断等着收尾还有一台国产 linux 系统需要装 Python 3.11。你不会想在三个会话里分别背诵 linux 常用命令更不想在脑子里维护两套包管理器的修复姿势。一键修复与安装脚本要解决的就是这个入口问题用同一个命令在本机识别发行版、选对包管理器把系统修复和服务器环境安装落成可重复、可审计的步骤。做 One-click 不是为了少敲几个字而是把“判断环境”和“执行动作”拆开。脚本能自己发现该用 apt 还是 dnf该修锁还是该补源该装系统 python 还是源码编译剩下的人在终端等结果就行。下面按骨架、环境安装、系统修复、验证技巧四块展开适合经常跨发行版维护服务器的运维、SRE 和后端工程师也适合刚写完第一个 shell 脚本、想给脚本增加容错的人。2. 脚本骨架发行版识别、包管理器选择与模块拆分2.1 用 /etc/os-release 识别 linux 发行版别再用 uname 硬猜很多 shell 脚本入门文章判断系统时习惯用 uname -s但它在 linux 上只区分内核名字不区分 Debian 和 RHEL。真正可靠的是 /etc/os-release它由 systemd 提供现在几乎每个发行版都有。读取时直接 source 而不是字符串 grep能同时拿到 ID、VERSION_ID、PRETTY_NAME 等字段。以下函数返回时对外只暴露两个全局变量PKG_MGR 和 PRETTY_NAME后续所有模块都依赖它们。#!/usr/bin/env bash set -euo pipefail log_info() { printf [INFO] %s\n $*; } log_ok() { printf [ OK ] %s\n $*; } log_err() { printf [ERR ] %s\n $* 2; } detect_distro() { [[ -f /etc/os-release ]] || { log_err 找不到 /etc/os-release仅支持 systemd 发行版 exit 1 } . /etc/os-release case $ID in debian|ubuntu|linuxmint) PKG_MGRapt-get ;; rhel|centos|almalinux|rocky|ol|openEuler|anolis) PKG_MGRdnf command -v dnf /dev/null 21 || PKG_MGRyum ;; kylin|uos|neokylin) PKG_MGRyum command -v dnf /dev/null 21 PKG_MGRdnf ;; opensuse*|suse*) PKG_MGRzypper ;; *) log_err 不支持的发行版: $ID exit 1 ;; esac log_ok 识别到 ${PRETTY_NAME:-$ID}包管理器: $PKG_MGR }一个容易踩的坑source /etc/os-release 后 ID 已经进入当前 shell如果函数里再把 ID 声明成 localcase 匹配就会全部失效所以这段代码里没有给 ID 加 local。case 分支覆盖 Debian/Ubuntu、RHEL/CentOS/Alma/Rocky/openEuler/Anolis以及 kylin、uos 这类常见国产 linux 发行版。包管理器选择上CentOS 7 只有 yumRocky 9 不再带 yum 而是 dnf所以用 command -v dnf 探测比按 VERSION_ID 硬判更不容易出错。2.2 包管理器统一入口apt、dnf、yum、zypper 的选择逻辑有了发行版 ID下一步是统一入口。不要在每个函数里写 if [ $PKG_MGR apt-get ]那会膨胀成网状分支。我一般定义 pkg_update 和 pkg_install 两个薄函数把 APT、dnf/yum、zypper 的差异吞掉后续模块只调用这两个函数不用再关心发行版。pkg_install 接受一组包名所以调用时照常写 pkg_install nginx redis。pkg_update() { case $PKG_MGR in apt-get) $PKG_MGR update ;; dnf) $PKG_MGR makecache ;; yum) $PKG_MGR makecache ;; zypper) $PKG_MGR refresh ;; esac } pkg_install() { local pkgs($) case $PKG_MGR in apt-get) $PKG_MGR install -y --no-install-recommends ${pkgs[]} ;; dnf|yum) $PKG_MGR install -y ${pkgs[]} ;; zypper) $PKG_MGR ${ZYPPER_ARGS:-} install -y ${pkgs[]} ;; esac }pkg_update 在 apt 下是 update在 dnf/yum 下是 makecache在 zypper 下是 refresh。三者的目的都是拉取远程仓库元数据但各自的参数名不通用。zypper 有的版本安装时会默认自动刷新离线环境会比较慢所以我用 ZYPPER_ARGS 这个环境变量预留了调整空间。下面这张表列出 5 个常见发行版族的对应关系发行版包管理器安装命令更新索引命令常见注意点Debian / Ubuntuapt-getpkg_install xxxpkg_update事务锁文件/var/lib/dpkg/lock-frontendCentOS 7yum同上同上仓库元数据可能过期先 makecacheRocky / Alma / openEulerdnf同上同上dnf 对仓库 GPG 校验更严openSUSE / SLESzypper同上同上安装前会自动刷新慢环境可加--no-refresh这张表说明一件事发行版之间的差异集中在锁、源、元数据刷新策略业务安装逻辑反而是共享的。所以脚本的收益主要来自把差异收敛在薄层里未来要支持 Arch只需在这个 case 里加一个 pacman 分支其他模块不用动。2.3 模块函数与 main 入口一个只做调度、不写业务的主框架主入口的职责是解析参数、按顺序调用模块函数。不要在 main 里塞安装 Redis 的代码否则参数一变就不好复用。以下参数骨架同时支持 --repair、--install、--all还留了 --python、--verify、--resume 三个扩展位。usage() { cat EOF 用法: $0 [选项] --repair 执行系统修复 --install 安装服务器环境 --all 先修复再安装 --python VER 指定 Python 版本默认 3.11 --verify 安装后执行自检 --resume 从上次中断点继续 EOF } main() { local action while [[ $# -gt 0 ]]; do case $1 in --repair) actionrepair ;; --install) actioninstall ;; --all) actionall ;; --python) shift; PYTHON_VER$1 ;; --verify) VERIFY1 ;; --resume) RESUME1 ;; -h|--help) usage; exit 0 ;; *) log_err 未知参数: $1; exit 2 ;; esac shift done [[ ${EUID:-0} -eq 0 ]] || { log_err 请以 root 运行; exit 1; } detect_distro case $action in repair) system_repair ;; install) env_install ;; all) system_repair; env_install ;; *) usage; exit 2 ;; esac [[ ${VERIFY:-0} 1 ]] verify_all } main $main 的 while 循环每次只处理一个参数shift 后自动走到下一个。--python 后面必须跟值所以它单独处理一次 shift。action 变量只会保留最后一次出现的动作值脚本里可以再加一个“冲突参数拒绝”判断同一个命令里同时出现 --repair 和 --install 时报错退出而不是静默覆盖。模块函数 system_repair、env_install 和 verify_all 是后面章节的入口这里只需要保证调用顺序正确。所有日志统一打到 stdout另有一份追加到 LOG_FILE配合最后的 exit code 就能接入监控系统。3. 服务器环境安装模块系统包、源码编译与软件源镜像参数3.1 优先用发行版自带 python3版本不足再走源码编译服务器环境安装里linux 系统安装 python 是出现频率最高的需求但一上来就编译源码并不明智。Debian 12 自带 python3 3.11Ubuntu 22.04 是 3.10CentOS 7 只有 2.7 或 3.6。先检查现有版本能满足就直接用系统包不满足再进入源码编译。版本比较不能直接用字符串 因为 3.9 会被认为比 3.10 大。先写一个按点分段比较的函数ver_ge() { local left right i IFS. read -r -a left $1 IFS. read -r -a right $2 for i in 0 1 2; do (( ${left[i]:-0} ${right[i]:-0} )) return 1 (( ${left[i]:-0} ${right[i]:-0} )) return 0 done return 0 }ver_ge 的循环写了三次比较是因为版本号最长到 patch 段短版本缺位按 0 处理。3.11.0 和 3.11.9 在前两位相等时进入第三位比较最终返回 0 表示满足。接下来 install_python 利用它做判断install_python() { local need_ver${PYTHON_VER:-3.11.9} local cur_ver if command -v python3 /dev/null 21; then cur_ver$(python3 -c import sys; print(..join(map(str, sys.version_info[:3])))) if ver_ge $cur_ver $need_ver; then log_ok python3 ${cur_ver} 满足 ${need_ver}跳过安装 return 0 fi fi case $PKG_MGR in apt-get) pkg_update pkg_install python3 python3-pip python3-venv ;; dnf|yum) pkg_install python3 python3-pip if [[ $ID centos ]] ! rpm -q epel-release /dev/null 21; then pkg_install epel-release || return 1 fi ;; zypper) pkg_install python3 python3-pip ;; esac cur_ver$(python3 -c import sys; print(..join(map(str, sys.version_info[:3])))) if ! ver_ge $cur_ver $need_ver; then if [[ ${ALLOW_SRC_BUILD:-0} 1 ]]; then src_build_python $need_ver else log_err 系统 python3 为 ${cur_ver}低于 ${need_ver}设 ALLOW_SRC_BUILD1 可源码编译 return 2 fi else log_ok python3 ${cur_ver} 可用 fi }install_python 中 command -v python3 只负责探测命令是否存在真实版本以 python3 -V 为准。版本满足直接 return 0这就是幂等的基础。版本不满足时不同发行版的包名不同CentOS 7 需要 EPELUbuntu 需要 python3-venv所以包列表不能写死。如果需要更高版本再走源码编译src_build_python() { local need_ver$1 local prefix/usr/local/python${need_ver%.*} pkg_install build-essential zlib1g-dev libffi-dev libssl-dev 2/dev/null || \ pkg_install gcc make zlib-devel libffi-devel openssl-devel curl -fsSL https://www.python.org/ftp/python/${need_ver}/Python-${need_ver}.tgz \ -o /tmp/python.tgz tar xzf /tmp/python.tgz -C /tmp ( cd /tmp/Python-${need_ver} || exit 1 ./configure --prefix$prefix --enable-optimizations make -j$(nproc) make install ) ln -sf $prefix/bin/python3 /usr/local/bin/python3 }源码编译的 prefix 放到 /usr/local/python3.11与系统 python 隔离。ALLOW_SRC_BUILD 默认是 0不传就不会编译。下载 tarball 用 curl -fSL-f 是遇到 HTTP 错误立即退出-L 是跟随跳转避免留下残缺源码包继续 configure。这里用 || 做了一个发行版依赖包的兼容跳转真正的项目最好按发行版拆分依赖组避免把 build-essential 安装失败的真实原因隐藏掉。3.2 软件源镜像参数不硬编码仓库地址默认尊重系统源服务器环境安装最容易翻车的是软件源。很多安装脚本为了提速会写死阿里、清华镜像公网环境没问题到了内网或开发调试环境就成了灾难。常见做法是默认保留系统源只有外部显式传 REPO_MIRROR 时才替换仓库地址。替换前先备份带时间戳的 sources.list再刷新元数据。REPO_MIRROR${REPO_MIRROR:-} backup_sources() { local ts ts$(date %Y%m%d%H%M%S) cp /etc/apt/sources.list /etc/apt/sources.list.bak.${ts} } install_nginx() { if [[ -z $REPO_MIRROR ]]; then pkg_install nginx else backup_sources sed -i -E s#^deb https?://[^/]*#deb ${REPO_MIRROR}# /etc/apt/sources.list pkg_update pkg_install nginx fi }这里 sed 用 -E 把 deb 行开头的 http:// 或 https:// 后第一段域名替换成 REPO_MIRROR保留后面的 bookworm main 等路径。如果镜像站没有对应套件结构404 会立刻暴露。yum 系的替换类似但要多处理一个 mirrorlist需要注释掉 mirrorlist 保留 baseurl或者直接新增 repo 文件。不要在图快时把整个 /etc/yum.repos.d 删掉内网有大量自定义仓库时删一次就全丢了。备份文件再操作恢复成本很低。3.3 安装动作的幂等已装则跳过装错则回滚幂等是一键脚本和一次性脚本的分水岭。同一台机器跑第二遍不能重复下包也不能把已有配置改坏。最简单的检查方式是“安装前探测、安装后记录”。对 systemd 服务检查 systemctl is-active对二进制检查 test -x对源码编译检查目标目录下的版本文件。下面的表格给出四个常用安装对象的幂等方案安装对象幂等检查命令安装动作回滚动作Python 3python3 -V版本比较pkg_install 或源码编译不动系统 python编译版本放独立 prefixOpenJDKjava -versionpkg_install openjdk-17-jdk-headlesspkg_remove openjdk-17-jdk-headlessNginxtest -x /usr/sbin/nginxpkg_install nginx卸载包但保留 /etc/nginx 配置备份Redisredis-cli pingpkg_install redis-server停服并卸载包表格里的回滚动作不是每个都能在生产直接执行。举例来说Nginx 的卸载会连带移除 /etc/nginx 下的站点配置所以回滚前要先 tar 备份配置目录。pkg_remove 函数和 pkg_install 一样需要 case 分发不能假设所有发行版都叫同一个包名。幂等判断错误的代价是安装脚本第二次运行时把用户手动修改的配置文件覆盖掉因此所有替换文件的动作都放在备份函数后面。4. 系统修复模块锁文件、磁盘与 DNS 的常见修复路径4.1 dpkg 与 yum 锁残留杀掉进程再清锁比盲目 rm 安全系统修复最常见的场景是上一个安装命令被 CtrlC 中断留下锁文件。很多修复教程让你直接 rm -f /var/lib/dpkg/lock这是最危险的做法如果另一个 apt 进程真的在写库删除锁会让两个进程同时操作 dpkg 状态最终损坏数据库。正确路径是先看锁被谁占用再决定是等还是 kill。repair_dpkg_lock() { local locks(/var/lib/dpkg/lock /var/lib/dpkg/lock-frontend) for lock in ${locks[]}; do if fuser -v $lock /dev/null 21; then local pids pids$(fuser $lock 21 | awk {for(i1;iNF;i) if($i ~ /^[0-9]$/) print $i}) for pid in $pids; do kill $pid 2/dev/null || true done fi done dpkg --configure -a || return 1 }fuser -v 在锁被占用时列出进程awk 抽取纯数字 pid 后逐个 kill。真实环境不一定只有这一个锁/var/lib/dpkg/updates/ 下的临时文件也可能残留用 dpkg --configure -a 会让 dpkg 自己收尾。yum/dnf 锁在 /var/run/yum.pid处理逻辑类似先读 pid再用 ps -p 确认进程仍存活确认不是自己再 kill进程已死就直接删 pid 文件不要对存活的 pid 盲目 kill -9。4.2 磁盘空间与日志清理先看再删journal 要设上限磁盘写满时安装动作会以各种奇怪方式失败比如 dpkg 只能写到一半、make 临时文件报错。修复脚本里要先放检测再放清理。检测动作只读不产生副作用disk_audit() { find /var/log -type f -size 1G -exec ls -lh {} \; | awk {print $5, $9} journalctl --disk-usage } disk_repair() { disk_audit journalctl --vacuum-size200M journalctl --vacuum-time7d }journalctl --disk-usage 查看的是 systemd journal它不是系统日志的全部但通常是 /var/log/journal 膨胀的元凶。--vacuum-size200M 会立刻回收空间到目标大小--vacuum-time7d 按时间维度限制两个参数可以同时用。对普通日志文件不要 find /var/log -type f -delete因为被进程占用的文件不释放空间。删除后服务还在继续写同一个 inode用户看到的是磁盘空间没有变化反而丢了历史日志。正确做法是找到占用进程并重启服务。注意/var/log下有些文件正在被进程占用删除文件并不会立刻释放磁盘空间需要确认占用进程并重启服务。4.3 DNS 与时间同步配置修复到能解析、能对表服务器能 ping 通但 curl 解析域名失败第一反应是查 /etc/resolv.conf。如果文件存在但 nameserver 为空写回系统 DNS 能应急。但 systemd 发行版的 /etc/resolv.conf 往往是指向 /run/systemd/resolve/stub-resolv.conf 的软链接直接重定向写入会在下次 NetworkManager 更新时被还原。常见做法是落到 /etc/systemd/resolved.conf 的 DNS 行再重启服务。repair_dns() { if [[ -L /etc/resolv.conf ]] [[ -d /etc/systemd ]]; then sed -i s/^#DNS/DNS223.5.5.5 114.114.114.114/ /etc/systemd/resolved.conf systemctl restart systemd-resolved elif ! grep -qE ^nameserver /etc/resolv.conf; then { echo nameserver 223.5.5.5 echo nameserver 114.114.114.114 } /etc/resolv.conf fi timedatectl set-ntp true 2/dev/null || systemctl enable --now chronyd }时间同步是系统修复里容易被忽略的一环。证书校验失败、cron 任务乱跑很多都源于系统时间漂移超过几分钟。修复动作很简单timedatectl set-ntp true 或启用 chronyd。脚本里要同时验证 systemctl is-active systemd-timesyncd而不是只改配置。下面这张表把修复项、检测命令和风险点列全修复项检测命令最小修复动作风险点dpkg 锁fuser /var/lib/dpkg/lockdpkg --configure -a勿直接删锁yum/dnf 锁cat /var/run/yum.pid验证 pid 后 kill确认不是当前脚本journal 膨胀journalctl --disk-usagejournalctl --vacuum-size200M清理的是可重建日志DNS 空配置grep nameserver /etc/resolv.conf写 systemd-resolved 配置重启网络会被覆盖时间偏差timedatectltimedatectl set-ntp true跳过可能影响证书校验修复项必须可验证否则执行完不知道是否成功。dpkg 锁修复后看 dpkg --audit 返回磁盘清理后看 df 的输出DNS 生效用 getent hosts 而不是 ping因为 ping 可能走 ICMP 但解析已经失败。5. 给一键脚本加断点续跑与 verify 自检5.1 state 文件记录完成步骤一键脚本执行到一半失败非常常见网络超时、仓库源失效、磁盘写满都可能是中断点。加 state 文件后脚本自己知道哪一步已完成下次执行跳过这些部分。文件可以放在 /var/log/one_click_fix_install.state而不是 /tmp/tmp 会被系统清理而且不同用户跑出来的权限混乱。MARK_FILE/var/log/one_click_fix_install.state mark_step() { local step$1 [[ -d /var/log ]] || mkdir -p /var/log echo ${step}: ok $MARK_FILE } check_step() { local step$1 [[ ${RESUME:-0} 1 ]] || return 1 grep -q ^${step}: ok$ $MARK_FILE 2/dev/null }mark_step 的写入时机必须在模块成功返回之后不能放在模块开头。check_step 依赖 RESUME 开关只有显式传 --resume 才读取 state 文件。否则即便上一次已安装 nginx不做检查直接覆盖这也是幂等与断点续跑的区别前者通过命令探测后者通过记录状态。两种机制可以同时存在state 只负责避免重复跑耗时的编译步骤。生产环境可以给 mark_step 追加一行带模块名的哈希值check_step 时校验防止从备份恢复时把另一台机器的 state 带过来。5.2 verify 验证一遍安装结果没有任何自检的“一键”等于盲跑。verify 模块应该和安装模块一一对应装了什么就验证什么。常见做法是保存一个数组每一项用竖线分成命令和期望正则遍历执行verify_all() { local checks( nginx -v 21|nginx version: python3 -V 21|Python 3. redis-cli ping|PONG java -version 21|openjdk version ) local item cmd pattern ok1 for item in ${checks[]}; do cmd${item%%|*} pattern${item##*|} if eval $cmd 21 | grep -qE $pattern; then echo PASS: ${cmd} else echo FAIL: ${cmd} ok0 fi done return $ok }eval 在这里有执行任意命令的风险所以 checks 数组里的内容必须由脚本作者维护不能来自外部参数。花括号展开和通配符在 eval 中按预期工作这比写成多行 if 更紧凑。verify 的结果同样追加到 state 文件形成一个闭环安装完成 - mark_step - verify - 失败后从 state 继续。若要在无人值守的定时任务里跑这个脚本--resume --verify 组合会让下一次执行只补未完成的部分并把失败的验证项暴露到你自己的监控系统。本文还有配套的精品资源点击获取