简介:这套一键修复与安装脚本面向Linux运维人员与服务器管理新手,聚合系统修复、环境部署与自动化配置能力,可快速处理启动异常、服务故障、软件冲突等常见问题,并为Web、数据库、语言运行时及缓存服务提供一键安装与标准化配置流程。压缩包共19个文件,其中14个bash脚本覆盖磁盘检查、引导修复、网络配置、日志瘦身、Conda/Jupyter/Rust等环境安装等细分场景,另有2个Markdown说明与2个文本清单辅助使用,整体仅35KB,轻量便携,适用于Ubuntu、CentOS、Debian等多种Linux发行版。脚本编写遵循最小权限原则,通过sudo控制管理员操作,并内置日志记录与错误处理机制,执行异常时可给出明确提示并防止系统陷入不可控状态;同时支持批量部署与多服务器管理,大幅降低重复运维成本。已有175人学习下载,适合希望提升Linux运维效率、减少手动配置出错的工程师,也可作为Shell脚本编写与自动化运维的入门参考;使用前建议先了解脚本逻辑并做好数据备份。
1. 一键修复与安装脚本到底在解决什么问题:运维手里最该有的那份后悔药
凌晨两点,机房服务起不来,grub 引导损坏,或者新开的服务器连 Nginx 和 MySQL 都没装。这种时候最能体会到什么是后悔药:如果有一个靠谱的「一键修复与安装脚本」,把人工排查链路封装好,跑一次就能把系统拉回来,再把环境装齐。它解决两类核心问题:一类是 Linux 系统层面的修复,引导坏了、包管理器坏了、依赖坏了;另一类是服务器环境安装,LNMP、Docker、Redis 这类基础服务一条命令装好。适合三类人:被系统故障折腾过的 Linux 运维、自建服务器的开发者、想找个能练手又实用的 shell 脚本入门项目的初学者。但有一个前提必须说在前面:你要知道这个脚本每一步在干什么,而不是闭眼一键。
2. 这类脚本的骨架:从发行版识别到函数化设计
2.1 为什么必须先认发行版再干活:包管理器决定一切
Linux 不像 Windows 那样一个安装包通吃。Debian 系用 apt,CentOS/RHEL 用 yum 或 dnf,openSUSE 用 zypper,Arch 系用 pacman,Alpine 用 apk。同样的 Nginx,在 apt 里直接叫 nginx,在 CentOS 7 上必须先装 epel-release 才能找到包;MySQL 的包名更是五花八门,Ubuntu 里是 mysql-server,CentOS 里是 mysql-community-server。所以一键脚本第一件实质工作不是写安装命令,而是把当前系统认准。
最稳的识别方式是从 /etc/os-release 读取。lsb_release 不是每个最小化安装都有,uname 只能看内核版本看不出发行版,这两个都不适合作为主要依据。os-release 是 systemd 时代就有的标准文件,几乎所有发行版都带,很多桌面衍生发行版也带。区别在于 ID 字段可能不同,比如有的发行版 ID 是 linuxmint,但它的软件源、包管理和 Ubuntu 完全兼容,这时候要看 ID_LIKE 字段。
#!/usr/bin/env bash # 第一步:识别发行版,决定后续用的包管理器 detect_distro() { if [ -f /etc/os-release ]; then . /etc/os-release DISTRO_ID="$ID" DISTRO_NAME="$NAME" DISTRO_VERSION="$VERSION_ID" else echo "无法识别系统:缺少 /etc/os-release" exit 1 fi case "$DISTRO_ID" in debian|ubuntu) PM="apt" ;; centos|rhel|fedora) PM="dnf" ;; opensuse*|sles) PM="zypper" ;; arch) PM="pacman" ;; alpine) PM="apk" ;; *) # ID_LIKE=debian 的衍生发行版也能兜底接入 apt 分支 if [ "${ID_LIKE:-}" = "debian" ]; then PM="apt" else echo "暂不支持的发行版: $DISTRO_ID" exit 1 fi ;; esac echo "发行版: $DISTRO_NAME $DISTRO_VERSION 包管理器: $PM" } detect_distro逻辑说明:. /etc/os-release是把文件里的变量加载进当前 shell,之后$ID、$NAME、$VERSION_ID都能直接用;case 按 ID 字段映射包管理器。真正要留意的是 default 分支里对 ID_LIKE 的判断,很多衍生发行版 ID 不是 ubuntu,但包管理和软件源完全兼容 debian/ubuntu,只看 ID 就把它们拒之门外了。
参数说明:DISTRO_VERSION 在写软件源和判断命令差异时有用,比如 CentOS 7 只有 yum,CentOS 8 才有 dnf;ID_LIKE 是 os-release 里的可选字段,取值可能是 debian 或 rhel fedora,脚本里统一转成小写再比较更稳妥。识别完发行版,后续函数才能放心地按$PM走不同分支。
2.2 一个能直接用的脚本骨架:参数入口与统一的日志
很多一键脚本翻车,不是命令写错,而是结构太乱:几十个 .sh 文件互相 source,变量到处覆盖,在 A 机器上能用,换 B 机器就行为诡异。我倾向于单入口脚本加函数库的做法,每个功能一个函数,主流程只做参数解析和分发。脚本开头统一设置错误退出策略和日志路径,任何一步失败,日志里都能看到当时执行到哪。
#!/usr/bin/env bash set -euo pipefail LOG_FILE="/var/log/onekey_linux.log" log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE" } usage() { cat <<EOF 用法: $0 [选项] 选项: --fix-boot 修复引导 (grub) --fix-pkg 修复包管理器 --install-lnmp 安装 Nginx + MySQL + PHP --install-docker 安装 Docker 及 compose 插件 --check 只做体检,不实际改动 EOF exit 0 } [ $# -eq 0 ] && usage ACTION="$1" shift case "$ACTION" in --fix-boot) fix_boot ;; --fix-pkg) fix_pkg ;; --install-lnmp) install_lnmp ;; --install-docker) install_docker ;; --check) health_check ;; *) usage ;; esac逻辑说明:set -euo pipefail 三个开关分别是“有命令返回非零就退出”“用到未定义变量就报错”“管道中任一命令失败则整个管道失败”。这三个开关能让脚本在出问题第一时间停下来,而不是带着错误状态继续往下执行,否则后面很可能把系统弄得更乱。
参数说明:这里刻意只允许一个动作参数,不搞组合式参数,因为一键脚本最大的风险是动作叠加。先修包管理再装 LNMP,如果前者失败,后者可能在一个残缺系统上继续装。脚本一次一个动作,组合留给使用者自己决定,出错时也容易定位。log() 里用了 tee -a,既上屏又写文件,方便事后翻日志。
2.3 环境安装函数怎么写:以 Nginx 和 Docker 为例
环境安装函数有个共同套路:先探测是否已安装,再按包管理器分支安装,最后设置 systemd 自启并留日志。已装过的直接跳过,别把人家的环境重复装一遍。Nginx 在 CentOS 7 上需要 epel 源,这个细节不处理,装到一半就报 package not found。Docker 则要格外小心官方源和发行版源的差别。
install_nginx() { if command -v nginx >/dev/null 2>&1; then log "nginx 已存在,跳过: $(nginx -v 2>&1)" return 0 fi case "$PM" in apt) apt-get update -y apt-get install -y nginx ;; dnf|yum) # CentOS 7 只有 epel 源里有 nginx,RHEL 系通用做法是先装 epel if [ "$DISTRO_VERSION" = "7" ]; then yum install -y epel-release fi if command -v dnf >/dev/null 2>&1; then dnf install -y nginx else yum install -y nginx fi ;; esac systemctl enable --now nginx log "nginx 已启动" }逻辑说明:command -v 探测命令是否存在,比 which 更通用,返回状态也更可靠。CentOS 7 的 yum 和 CentOS 8 的 dnf 命令不同,先查命令存在再决定用哪个,而不是写死 dnf 或 yum。
参数说明:nginx 装好后,默认站点目录 Ubuntu 是 /var/www/html,CentOS 是 /usr/share/nginx/html,php-fpm 的 socket 路径也不同。如果脚本后面还要写默认页或配 php,这些路径必须按发行版走,否则自检阶段会一脸懵。
Docker 的安装我不推荐 curl 管道 bash 这种安装方式,原因后面避坑章会展开。优先用发行版自带仓库,装不上再考虑官方存储库。
install_docker() { if command -v docker >/dev/null 2>&1; then log "docker 已存在: $(docker --version)" return 0 fi case "$PM" in apt) apt-get update -y apt-get install -y docker.io docker-compose-v2 ;; dnf|yum) # 发行版仓库有 docker 就用,没有再用 docker-ce 仓库 if ! dnf install -y docker docker-compose-plugin 2>/dev/null; then dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin fi ;; esac systemctl enable --now docker log "docker 已安装" }逻辑说明:Ubuntu 的 apt 仓库里 docker 包名是 docker.io,CentOS 仓库里是 docker,用 ! 包一层,在发行版仓库缺包时再退回 docker 官方仓库。脚本里没有用 curl 管道 bash,而是下载 repo 文件配置,至少让使用者知道源指向哪里。
参数说明:docker compose 现在以插件形式存在,命令是 docker compose;老项目用的 docker-compose 是 python 版。脚本装完顺手检查一下 docker compose version,输出异常要单独提示,避免用户装完发现 compose 不可用。
3. 系统修复怎么做成一件事:引导、包管理器到自检
3.1 修复引导的关键在 chroot 和挂载顺序
最常见的系统起不来是引导损坏:开机进 grub 菜单,选中内核就黑屏或 kernel panic。人工修复流程是:用系统镜像的 live 环境或救援模式开机,把原系统根分区挂载到 /mnt,再 chroot 进去重新安装 grub 并生成配置。一键修复脚本正是把这条链路自动化。
chroot 之前必须先做绑定挂载,否则 chroot 里跑 grub-install 拿不到 /dev 下的设备节点,grub2-mkconfig 也会因为看不到 /proc 里的系统信息而生成空配置。顺序是 dev、dev/pts、proc、sys,用完后依次卸载,别漏。根分区挂载前最好先 lsblk 确认磁盘和分区关系,不然挂错分区,后面全白做。
TARGET="/mnt" # 救援模式下根分区挂载点 DEVICE="/dev/sda" # 目标磁盘,UEFI 模式下指 EFI 分区所在磁盘 fix_grub_chroot() { mount --bind /dev "$TARGET/dev" mount --bind /dev/pts "$TARGET/dev/pts" mount --bind /proc "$TARGET/proc" mount --bind /sys "$TARGET/sys" if [ -d "$TARGET/sys/firmware/efi" ]; then chroot "$TARGET" /bin/bash -c \ "grub-install --target=x86_64-efi && grub2-mkconfig -o /boot/grub2/grub.cfg" else chroot "$TARGET" /bin/bash -c \ "grub-install $DEVICE && grub2-mkconfig -o /boot/grub2/grub.cfg" fi umount "$TARGET/dev/pts" "$TARGET/dev" "$TARGET/proc" "$TARGET/sys" }逻辑说明:/sys/firmware/efi 目录存在说明当前是 UEFI 引导,这时 grub-install 的目标不再是磁盘 MBR,而是 EFI 系统分区,所以用 --target=x86_64-efi;不存在则按传统 BIOS 方式写入磁盘。grub2-mkconfig 的输出路径,RHEL 系是 /boot/grub2/grub.cfg,Debian 系是 /boot/grub/grub.cfg,这一步必须按发行版区分,否则生成的文件放错位置,重启照样找不到菜单。
参数说明:DEVICE 要填整块磁盘而不是分区,比如 /dev/sda 而不是 /dev/sda1,写错成分区 grub-install 会直接报错。UEFI 机器还要确认 EFI 分区已经挂载到 TARGET/boot/efi,没挂载的话得先补一步:mount /dev/sda1 "$TARGET/boot/efi"。
3.2 修复包管理器:锁文件、坏依赖、数据库重建
包管理器故障分三类:一类是进程崩溃留下的锁,一类是依赖关系破裂,一类是 rpm/dpkg 数据库损坏。看报错就能大概判断是哪类,不要上来就 rm 删锁。有进程占锁就先看进程、等它结束;只有确定没有 apt/dpkg 在跑,残留锁才能删。
fix_pkg() { case "$PM" in apt) # apt 锁被占用时先看是谁在用,别急着删除 if fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1; then echo "dpkg 锁被以下进程占用:" fuser -v /var/lib/dpkg/lock-frontend return 1 fi dpkg --configure -a apt-get --fix-broken install -y apt-get update -y ;; dnf|yum) # rpm 数据库损坏时会有 "rpmdb open failed" 的报错 rpm --rebuilddb dnf clean all dnf check ;; esac }逻辑说明:dpkg --configure -a 负责把上次中断的配置流程走完;apt-get --fix-broken install 专门修补依赖破损;最后 update 刷新索引。RHEL 系里 rpm --rebuilddb 重建数据库索引,适用于 rpmdb open failed 这类报错,之后再 clean 和 check 验证一遍。
参数说明:apt 锁有两把:/var/lib/dpkg/lock 和 /var/lib/dpkg/lock-frontend,前者是 dpkg 数据库锁,后者是 apt 前端锁。fuser 查到的是占用锁的进程号;如果是 apt 自己在跑,等它结束就行,kill 掉可能弄坏中间状态。记住一个原则:先诊断后动刀,删锁是最后手段。
3.3 修复后的验证:df、systemctl 和 grub 文件三连查
一键修复的脚本最忌讳修完就退出。修没修好,跑一遍体检函数比什么都直白。体检函数应该只读不改,放在动作执行后再跑一次,把关键服务的状态都打出来。
health_check() { echo "===== 磁盘挂载 =====" df -hT | head -20 echo "===== 关键服务状态 =====" systemctl is-active sshd nginx docker 2>/dev/null || true echo "===== 引导文件检查 =====" ls -l /boot/grub2/grub.cfg /boot/vmlinuz-* 2>/dev/null | head -5 echo "===== 端口监听 =====" ss -lntp | grep -E ':(22|80|3306)\s' || echo "没有发现 22/80/3306 端口监听" }逻辑说明:systemctl is-active 对每个服务返回一个状态,对不存在的服务会报错,后面接 || true 让检查函数继续往下跑。grub.cfg 存在只是基本项,重启起得来才算数,但至少能看出配置是否生成。端口监听比进程状态更直接。
参数说明:22 端口是 SSH,80 是 HTTP 服务,3306 是 MySQL。用 grep 把三个端口一起看,ss 在最小化系统上比 netstat 更常预装。体检函数没有任何写操作,可以安全地在动作前后各跑一次。
4. 服务器环境安装:把 LNMP 和 Docker 装成生产可用状态
4.1 流程设计的三个前置检查
环境安装和系统修复不一样,它上来就要下载和安装,所以前置检查必须做足,否则装一半发现端口被占、包冲突,留下一个半残的系统。检查顺序一般是端口占用、包冲突、自启开关。安装 LNMP 这类组合服务,80、3306、9000 都要查,9000 是 php-fpm 的默认监听端口。如果 80 上已经跑了一个服务,脚本就该停下来让用户确认,而不是直接覆盖。
port_busy() { ss -lntp 2>/dev/null | awk '{print $4}' | grep -q ":$1$" } install_lnmp() { for p in 80 3306 9000; do if port_busy "$p"; then echo "端口 $p 已被占用,安装中断。请先确认服务归属" return 1 fi done # 通过前置检查后才开始安装 }逻辑说明:ss -lntp 列出监听端口,awk 取第 4 列,也就是本地地址和端口,grep 用:$1$做精确匹配,避免 8080 误伤 80。端口检查只是第一道保险,真正的包冲突还要看 MariaDB 和 MySQL 的互斥,这两个数据库服务不能同时存在。
4.2 参数设计:环境变量加默认值
一键脚本如果只能无脑全装,生产环境没人敢用。常见做法是把可选项做成环境变量,不传就用默认值。比如 PHP 版本、MySQL root 密码、是否额外装 Redis。Debian 系安装时还要注意交互界面,apt 在配置某些包时会弹出蓝色对话框,需要设置 DEBIAN_FRONTEND=noninteractive 来跳过。
# 可选参数定义:不传时按默认值执行 PHP_VERSION="${PHP_VERSION:-8.1}" MYSQL_ROOT_PWD="${MYSQL_ROOT_PWD:-}" INSTALL_REDIS="${INSTALL_REDIS:-0}" # Debian 系安装避免交互式对话框 if [ "$PM" = "apt" ]; then export DEBIAN_FRONTEND=noninteractive fi # 如果没给 root 密码,装完生成随机密码并写入日志 if [ -z "$MYSQL_ROOT_PWD" ]; then MYSQL_ROOT_PWD="$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 16)" echo "MySQL root 密码: $MYSQL_ROOT_PWD" >> "$LOG_FILE" fi逻辑说明:环境变量带默认值的写法,让脚本既能默认跑也能定制;tr 从 /dev/urandom 取随机字符,head -c 16 截成 16 位密码,写在日志里方便首次登录找回。DEBIAN_FRONTEND=noninteractive 这个环境变量对 apt 系影响很大,不加的话安装 postfix 这类包时脚本会卡在交互界面,一键就变成了半截。
参数说明:PHP_VERSION 要和软件源里实际存在的版本对应,否则 apt 报错。MYSQL_ROOT_PWD 最好是显式传入,自动生成的密码虽然随机,但存在日志里本身就是一种暴露;内网环境还可以,公网服务器建议安装后立刻改掉。
安装时还有一个隐藏参数:是否允许脚本自动重启服务。生产环境里有些服务不能随便重启,脚本里可以增加ALLOW_RESTART="${ALLOW_RESTART:-0}"这样的开关,默认不重启,用户确认后才执行 reload 或 restart。
4.3 安装后的自检:端口响应而不是进程存在
安装完成不是终点。很多脚本装完一句 done 就退出,用户 curl 一看 502 才知道配置不对。自检阶段应该检查端口监听和 HTTP 响应码,两个一起看。
check_http() { curl -sS -o /dev/null -w "%{http_code}" http://127.0.0.1/ || echo "请求失败" } check_service() { if ss -lntp | grep -q ":$2 "; then echo "$1 端口 $2 正常" else echo "$1 端口 $2 未监听,请查看 $LOG_FILE" return 1 fi }逻辑说明:curl 的 -w 参数输出 HTTP 状态码,200 代表 nginx 默认页能访问;如果 php-fpm 没起来,nginx 会返回 502,状态码一眼能看出问题。检查监听端口比检查进程更可靠,因为进程存在但没成功 bind 端口,说明服务处于异常状态。
参数说明:curl 加 -sS 静默但保留错误信息,-o /dev/null 丢弃页面内容。企业环境里 80 根路径可能被其他应用占用,这种裸检查会误报,这时候要改成 curl 指定 Host 和路径,比如 curl -H "Host: example.com" http://127.0.0.1/healthz。
5. 一键化避坑:五个翻车现场和对应的解决方案
一键脚本方向看着简单,实际生产里翻车点很密集。下面五个是我见过或踩过的真实问题,每一条都按现象、原因、解决三个层面说清楚。
5.1 从“删锁文件”到“dpkg 整个报废”
现象:apt 报 Could not get lock /var/lib/dpkg/lock-frontend,按照网上搜到的方法直接 rm 删锁,结果后面的 dpkg 大量报错,最后整个 dpkg 状态库损坏,apt 完全不可用。
原因:锁文件本身不是问题,占用锁的 apt 进程才是。直接删锁相当于把一个正在写数据库的文件的锁删掉,dpkg 状态库自然坏。
解决:先 fuser -v 看谁占锁,ps 确认没有 apt/dpkg 进程再动手。删除前备份状态库目录:cp -a /var/lib/dpkg /var/lib/dpkg.bak。这是我最深的血泪教训之一:包管理器的锁是在保护数据,不是在跟运维作对。
5.2 从“curl 管道 bash”到“装了一堆卸载不掉”
现象:一条命令装 Docker,结果 Docker 没装上,系统多了一堆第三方软件源,还塞了几百兆不明依赖。
原因:curl 管道 bash 的问题在于内容在传输中不可审计,你根本不知道它会改哪些源、装哪些包、写哪些路径。黑匣子式的安装脚本,能不用就不用。
解决:先下载到一个目录,grep 看里面的源地址和安装逻辑,用 bash -n 做语法检查,确认没问题再执行。这个习惯也适用于你自己要发布的脚本,让别人能审计,信任度才会高。
5.3 从“不认发行版”到“nginx not found”
现象:同一份脚本在 CentOS 7 上跑,apt 显然不可用,yum 安装 nginx 直接报 package not found。
原因:脚本把包管理器或包名写死成了 Ubuntu 分支,RHEL 系需要 epel-release,CentOS 7 又只有 yum 没有 dnf。
解决:脚本里每个包安装函数都要走发行版识别分支,并且按系统版本选命令。包名映射表放在脚本头部,新增包必须同步更新,否则就是下一个坑。这也是为什么第一章要先认发行版,所有分支都从那里派生。
5.4 从“装 LNMP”到“把别人的服务顶掉了”
现象:服务器 80 端口本来跑着一个轻量服务,运行一键 LNMP 安装脚本后,旧服务直接停止,请求全部失败。
原因:安装脚本只检查了 nginx 是否已安装,没有检查 80 端口是否被其他进程占用,新 nginx 启动时发现端口被占,要么启动失败,要么配置文件被覆盖。
解决:前置检查加上端口探测,发现被占用直接中断并提示,除非用户显式传 --force。安全的“一键”应该是对环境敏感的一键,而不是无脑覆盖的一键。
5.5 从“zip 解压”到“bash 换行符报错”
现象:Windows 下打包的 zip 解压后,执行脚本报$'\r': command not found,脚本完全跑不起来。
原因:zip 不保留 Unix 执行权限,而且 Windows 行尾是 CRLF,Linux 把 CR 当成了命令的一部分。macOS 打包的 zip 还可能带 __MACOSX 目录。
解决:解压后统一处理:find . -name "*.sh" -exec chmod +x {} ;,换行符问题用 sed -i 's/\r$//' 或 dos2unix 清理。这也是 zip 格式做脚本分发时最容易被忽略的细节,发布之前一定要在干净环境里解压验证一遍。
6. 把脚本武装到能用:锁文件、备份与回滚习惯
最后聊一个容易被忽略的高级点:一键脚本不只是一堆命令的堆叠,它应该自带防重入、备份和回滚机制。
锁文件可以防止两个终端同时执行脚本互相踩踏。用 flock 比用 mkdir 占位更稳,进程退出时锁自动释放,不会留下需要手动清理的痕迹。
LOCK_FILE="/tmp/onekey_linux.lock" exec 9>"$LOCK_FILE" flock -n 9 || { echo "已有脚本实例在运行"; exit 1; }逻辑说明:exec 9> 打开一个文件描述符,flock -n 尝试加锁,拿不到就退出,不阻塞等待。这样即使有人误配了定时任务,也不会出现两个修复进程互砍的情况。
运行前留备份的习惯也很重要。改源、改 grub 配置前,先把原文件复制一份带时间戳的备份,比如 cp /etc/apt/sources.list{,.bak-$(date +%F)},这条命令用到了 bash 的大括号展开,也算 linux 常用命令里的高频技巧。备份统一放到 /root/onekey_backup,回滚时按时间戳找最近的版本就行。
我自己的教训是:测试机跑通就直接上生产,结果生产环境因为 SELinux 没关、自定义路径不同,环境安装脚本在测试机正常、生产上翻车。现在的基本要求是:先跑 --check 体检,再在测试机完整跑一遍安装并记录日志,最后才上生产执行。
这个方向值得做,工作量不大,难的是把发行版识别、参数设计、日志和回滚做扎实。希望这份经验能帮你少踩几个坑,真正把好用的东西沉淀下来,而不是变成一堆脚本的堆叠,希望帮到你。
本文还有配套的精品资源,点击获取