简介:面向Linux运维与服务器环境搭建的自动化脚本集合,以“一键”方式提供系统修复与安装能力。覆盖Ubuntu、CentOS、Debian等常见发行版,适用于运维人员、系统管理员和Linux初学者,可完成系统检测、包管理修复、引导加载器重置、网络配置以及Web/数据库/邮件等服务器环境部署。资源共24个文件、约44KB,主体为17个shell脚本,承担核心修复与安装逻辑;另有2个YAML配置、2个文本说明、2个Markdown笔记和1个许可证,整体轻量便于分发。已有153人学习下载。脚本内置sudo权限检查、时间同步、文件修改、日志收缩、软件包管理等模块,并提供多语言开发环境安装入口,支持Bash编写以保证跨发行版兼容,同时设计错误检测与恢复机制以降低操作风险。附带文档可帮助快速掌握各脚本用途、执行条件与排错思路,适合用于服务器初始化、故障恢复与环境迁移等场景。
1. 一键修复与安装脚本:先救活系统,再搭好环境
接手一台被折腾到起不来的 Linux 服务器,或者给新开的机器把整套运行环境装齐,这两件事的共同痛点是:步骤特别长、命令特别碎、每台机器的脾气还都不一样。一键修复与安装脚本(One-click)就是为这两类高频场景设计的——把所有 linux 系统修复和服务器环境安装的常见操作收敛到一个入口脚本里,分修复、安装两个分支去跑,跑完留下日志。它能解决的不只是“省敲几行命令”,而是把你在 linux 系统故障案例里看到的那些重复诊断动作固化成流程,让新手照跑不慌,让老手把时间省下来去处理真正疑难的问题。
2. 为什么一键脚本能修系统:模块化、幂等与日志三板斧
修复和安装脚本最怕的不是功能少,而是发生在一台“你不了解状态”的机器上。它能不能信得过,取决于脚本本身的三个设计:入口是否模块化、重复执行是否安全、出问题能不能追溯。这三点想清楚了,后面每个分支函数才有讨论的价值。
2.1 脚本骨架:一个入口、两个分支、分文件管理
我写这类脚本不会把几千行塞进一个文件。常见做法是把入口做成很薄的调度器,每个分支独立成repair_*、install_*这样的函数文件,入口只负责识别系统、转发参数、记录日志。下面是最小的入口骨架:
#!/bin/bash # 一键修复与安装脚本 - 入口 # 用法: ./one-click.sh repair [target] # ./one-click.sh install [target] set -u export LC_ALL=C LOG_FILE="/var/log/one-click-$(date +%F).log" SYS_ID="" SYS_VER="" PKG_MGR="" detect_system() { if [ -f /etc/os-release ]; then . /etc/os-release SYS_ID="${ID:-unknown}" SYS_VER="${VERSION_ID:-unknown}" else echo "$(date +%T) 未找到 /etc/os-release,无法识别发行版,退出" | tee -a "$LOG_FILE" exit 1 fi } detect_pkg_mgr() { case "$SYS_ID" in debian|ubuntu) PKG_MGR="apt" ;; centos|rhel|rocky|almalinux|fedora) PKG_MGR="dnf" ;; sles|opensuse*) PKG_MGR="zypper" ;; *) PKG_MGR="unsupported" ;; esac } main() { detect_system detect_pkg_mgr echo "$(date +%T) 系统: $SYS_ID $SYS_VER 包管理器: $PKG_MGR 动作: ${1:-}" | tee -a "$LOG_FILE" case "${1:-}" in repair) repair_main "${2:-}" ;; install) install_main "${2:-}" ;; *) echo "用法: $0 {repair|install} [target]" ;; esac } main "$@"这段代码的逻辑很简单:先识别系统,再分发动作。注意我用了set -u而不是set -e。原因在于修复场景里某个子命令失败是常态,比如grub-install在 UEFI 机器上会报错,但机器未必真的救不回来;此时直接退出反而堵死了后面 chroot 里补修的机会。用set -u只挡未定义变量,具体函数的成败交给函数内部判断,出错路径由trap或逐条日志记录来兜底。
入口里的tee -a值得一提。所有关键信息同时输出到终端和日志文件,用户在跑长任务时能看到进度,事后排查时也有时间线。日志文件名带了日期,避免一天多次运行互相覆盖。这比满屏 echo 却什么都留不下要实用得多。
2.2 系统识别与包管理器分发:别把 apt 命令丢到 yum 机器上
脚本能不能跨发行版复用,核心就在系统识别这一步。很多人习惯用uname -a判断系统,这其实是误区——它给的是内核版本,而内核和发行版没有一一对应关系。同样内核版本的机器,可能是 Ubuntu 也可能是 Rocky Linux,包管理器完全不同。
可靠的来源是/etc/os-release,它由 systemd 时代以来的发行版统一提供,字段稳定。解析之后,把包管理器的选择做成 case 分支:Debian/Ubuntu 走 apt,RHEL 系走 dnf 或 yum,SUSE 系走 zypper。判断完包管理器,后续所有动作例如“安装编译依赖”“修复依赖关系”都统一走pkg_install这类封装函数,而不是散落成裸命令:
pkg_install() { case "$PKG_MGR" in apt) apt-get update -qq && apt-get install -y --no-install-recommends "$@" ;; dnf) dnf install -y "$@" ;; zypper) zypper install -y "$@" ;; *) echo "暂不支持该包管理器,请手动安装: $*" | tee -a "$LOG_FILE" ;; esac }这个封装的价值在统一超时和日志。比如 apt 系一定要用apt-get而不是apt,apt是给交互终端用的,它带进度条和颜色转义符,在脚本非交互环境下行为不够稳定,重定向日志时还会混入控制字符。--no-install-recommends是 Debian 系装编译依赖时的常用参数,它避免把文档、可选插件等连带装进来,减少依赖面,也降低后面autoremove误删的误伤面。
如果你维护的机器里有国产发行版,比如基于 openEuler 或龙蜥的发行版,它们的/etc/os-release的ID字段可能是openeuler、anolis。策略上不必为每个名字单独写一套,而是判断“它是否兼容 dnf/yum”,把 ID 映射到包管理器族上。我一般会在 case 分支里把已知的兼容名都列出来,并留一个 unsupported 出口提示人工处理,而不是猜。
2.3 幂等设计和日志归集:重复跑不会翻车,跑挂了能查原因
“一键”听起来爽,但很多翻车现场是同一脚本被反复执行造成的。比如装 MySQL,第一次编译到一半失败,第二次从头再编译,旧目录里的残留 Makefile 和新源码混在一起,报错千奇百怪。好的修复安装脚本必须讲幂等:同一个命令跑一次和跑两次,最终状态一致,或者至少不会更坏。
我常用的两个手段是状态标记和残留清理。状态标记适合安装分支,比如编译安装完成后写一个.one-click-stamp文件,下次执行检测到标记就提示“已安装,如需重装请先 uninstall 或加 --force”。残留清理适合修复分支,比如修复包管理器前先把损坏的锁文件备份而不是直接删除,然后才重试。写入和校验放一起:
install_marker() { local marker="$1" if [ -f "$marker" ]; then echo "$(date +%T) 检测到标记文件 $marker,跳过安装。如需重装请删除后重试" | tee -a "$LOG_FILE" return 1 fi touch "$marker" } cleanup_stale_lock() { local lock_path="$1" if [ -f "$lock_path" ]; then mv "$lock_path" "${lock_path}.bak.$(date +%s)" fi }标记文件有几个注意点。不要把它和安装版本信息混在一个文件里,因为版本升级时旧标记会让脚本误判为已安装;我习惯在标记文件里写两行内容:安装时间和版本号。cleanup_stale_lock里用mv而不是rm,保留原始锁文件方便事后对照,这也算是一种后悔药。
日志归集是另一块。修复和安装过程会调用大量外部命令,给每个外部命令单独写日志不现实。我会在函数里用一个带时间戳的全局日志函数,把所有输出合并到LOG_FILE,并且用trap在脚本退出时把结束状态写进去:
log_info() { echo "$(date +%T) [INFO] $*" | tee -a "$LOG_FILE"; } log_error() { echo "$(date +%T) [ERROR] $*" | tee -a "$LOG_FILE"; } finish() { local code=$? echo "$(date +%T) [EXIT] $code" | tee -a "$LOG_FILE" exit $code } trap finish EXITtrap finish EXIT的关键效果是:即使脚本中途被kill或因为某条命令让 shell 退出,日志也会被刻上退出码。排查一键脚本问题时,最常做的一件事就是打开日志文件看最后三行——是正常结束还是被截断,截断的最后一条日志往往就是第一个出问题的点。没有这套机制,你只会在终端看到 “failed”,无从下手。
3. 系统修复分支:从引导、包管理器到网络源的救援函数
拆开讲修复分支。这里要处理的是 linux 系统故障案例里最高发的几类:引导损坏、包管理器状态错乱、网络配置丢失、动态链接库缺失。每一类都对应一到两个可以在脚本里复用的函数,我会把关键参数和适用边界说明白。
3.1 引导损坏:chroot 环境重置 GRUB
引导损坏的典型现象是开机停在黑屏或 grub rescue 提示符,进不了系统。这类问题通常不是 grub 二进制真坏了,而是/boot分区内容丢失、磁盘分区表变动,或者内核升级后 grub.cfg 没更新。修复思路是:用 Live CD 启动,把原系统的根分区挂载起来,再通过 chroot 切进原系统环境,重装 grub 并重新生成配置。
repair_bootloader() { local root_dev="$1" local boot_dev="${2:-}" local mnt="/mnt/rescue" [ -z "$root_dev" ] && log_error "修复引导需要指定根分区,例如 /dev/sda2" && return 1 mkdir -p "$mnt" mount "$root_dev" "$mnt" || { log_error "挂载根分区失败,检查分区路径"; return 1; } [ -n "$boot_dev" ] && mount "$boot_dev" "$mnt/boot" mount --bind /proc "$mnt/proc" mount --bind /sys "$mnt/sys" mount --bind /dev "$mnt/dev" chroot "$mnt" /bin/bash -c "grub-install ${GRUB_TARGET_DEV:?请设置目标磁盘} && update-grub" log_info "引导修复完成,退出 chroot 并卸载分区" umount "$mnt/dev" "$mnt/sys" "$mnt/proc" [ -n "$boot_dev" ] && umount "$mnt/boot" umount "$mnt" }函数里最容易错的是挂载顺序。必须先挂根分区,再挂 boot 和虚拟文件系统,卸载时顺序反过来。mount --bind把当前 Live 环境的/proc、/sys、/dev映射进 chroot,让原系统里的 grub 命令能正常读取设备状态。GRUB_TARGET_DEV是指 grub 要写到的磁盘设备,比如/dev/sda,这块是纯机械硬盘时写盘名,NVMe 固态则可能是/dev/nvme0,一定要传盘而不是分区,否则引导程序找不到 Stage 1。
chroot 之后建议先用df -h确认根分区空间够不够。如果之前是因为/boot写满导致 grub 更新失败,空间不足时update-grub会再次失败。空间不足就先清理再执行,别硬跑。
3.2 包管理器状态修复:dpkg 与 rpm 数据库重建
包管理器状态损坏是很隐蔽的故障:系统能正常登录,但一装软件就报 “dpkg was interrupted” 或 “rpmdb open failed”。这类问题不影响业务进程,但会让任何后续安装卡死。修复策略分发行版走。
Debian/Ubuntu 系用 dpkg 审计加配置修复:
repair_dpkg() { log_info "开始修复 dpkg 状态" mkdir -p /var/lib/dpkg/backup if [ -f /var/lib/dpkg/status ]; then cp /var/lib/dpkg/status "/var/lib/dpkg/backup/status.$(date +%s)" fi dpkg --audit 2>&1 | tee -a "$LOG_FILE" dpkg --configure -a 2>&1 | tee -a "$LOG_FILE" apt-get install -f -y 2>&1 | tee -a "$LOG_FILE" log_info "dpkg 修复完成" }dpkg --audit只列问题不修改,适合先看破坏程度。dpkg --configure -a是重跑所有未完成的配置步骤,很多中断残留在这里被补齐。apt-get install -f用于修复依赖关系,它会尝试把破损的包恢复为可用状态。备份 status 文件是修复脚本的保底操作,万一修复过程中把包状态改坏,还能拿原始状态回去对比,这对排查“修复反而修出新问题”很重要。
RHEL 系则是重建 rpm 数据库:
repair_rpmdb() { log_info "开始重建 rpm 数据库" local db_dir="/var/lib/rpm" if [ -f "$db_dir/__db.001" ]; then for f in "$db_dir"/__db.*; do mv "$f" "${f}.bak.$(date +%s)" done fi rpm --rebuilddb rpm -qa >/dev/null 2>&1 && log_info "rpm 数据库重建成功,可正常查询" || log_error "重建后 rpm -qa 仍失败,检查磁盘空间" }rpm 数据库损坏多数表现为rpm -qa直接卡死或报 “cannot open Packages database”。重建前把__db.*这些 Berkeley DB 的锁文件挪走,相当于清掉进程残留的锁状态,再执行rpm --rebuilddb重建索引。最后用rpm -qa做验证,能列出包列表才算真正修好。这一步如果验证失败,最普遍的原因是磁盘满了,索引写不进去,别反复 rebuild,先清理空间。
3.3 网卡、DNS 与软件源配置修复
服务器起不来网络,修复脚本需要覆盖三种情况:网卡没启动、DNS 配置被覆盖、软件源指向不可达地址。修网卡前先确认 NetworkManager 是否在运行:
repair_network() { log_info "开始检查网络状态" systemctl status NetworkManager --no-pager 2>&1 | head -20 | tee -a "$LOG_FILE" local active_iface="" active_iface=$(ip -br link | awk '$2!="lo" && $3=="UP" {print $1; exit}') if [ -z "$active_iface" ]; then local first_iface first_iface=$(ip -br link | awk '$2!="lo" {print $1; exit}') log_info "没有活跃网卡,尝试拉起 $first_iface" nmcli device connect "$first_iface" 2>&1 | tee -a "$LOG_FILE" fi grep -q '^nameserver' /etc/resolv.conf || echo "nameserver 223.5.5.5" > /etc/resolv.conf log_info "网络配置检查完成" }ip -br link输出简洁,适合脚本解析。这里选 223.5.5.5 只是作为能用的公共 DNS 示例,实际部署时我更建议改成公司内部 DNS 或你确认可达的地址,并且先ping验证连通性再写入。修复脚本里不要默认改动现有 DNS 配置——只做兜底,也就是当前文件里没有 nameserver 时才写,避免把用户手动配的 DNS 覆盖掉。
软件源修复相对高频,尤其是从旧系统升级或换源后出现的 404。修复脚本要做的就是先把原始源文件备份,再按发行版和版本号写入可用的源模板:
repair_apt_source() { local sources="/etc/apt/sources.list" [ -f "$sources" ] && cp "$sources" "${sources}.bak.$(date +%s)" cat > "$sources" <<EOF deb http://deb.debian.org/debian ${SYS_VER} main contrib non-free deb http://security.debian.org/debian-security ${SYS_VER}-security main contrib non-free EOF apt-get update -qq 2>&1 | tee -a "$LOG_FILE" }实际使用时,${SYS_VER}这个代号需要额外处理:Debian 的/etc/os-release里 VERSION_CODENAME 才是 “bookworm” 这样的代号,而不是 “12”。来源里的仓库路径写错,apt-get update 就会报 404。我在脚本里会单独提取VERSION_CODENAME做拼接,并且更新后强制检查一次 update 的退出码。用公共软件源只是兜底方案,内网机器建议在源文件模板里保留变量替换,方便部署时传入内网镜像地址。
3.4 系统库与动态链接器救援:ldd 和 ldconfig 的边界
这类故障的症状是命令报 “error while loading shared libraries”,比如libssl.so.1.1: cannot open shared object file。常见原因是误删了系统库、升级覆盖了旧版本,或者用户手动往/usr/local/lib装了不兼容的库。修复的第一步不是急着下载库文件,而是确认缺什么、从哪个路径加载:
diagnose_libs() { local bin="$1" [ -z "$bin" ] && log_error "用法: diagnose_libs /usr/bin/openssl" && return 1 ldd "$bin" 2>&1 | tee -a "$LOG_FILE" ldconfig -p | grep -E "$(basename "$bin")|libssl|libcrypto" | tee -a "$LOG_FILE" }ldd输出里标着not found的就是缺失项,ldconfig -p能看系统当前缓存了哪些库。很多人一看到缺库就百度文件名,找到 .so 就丢进系统目录,这种操作风险极大——库依赖的是一个完整版本链,A 库依赖 B 库的特定符号,只拷一个 .so 往往引出一串新报错。
正确的修复是优先用包管理器找回原库。比如 libssl 的库属于 openssl 包,apt-get install --reinstall openssl或者dnf reinstall openssl能把库文件恢复到和系统匹配的版本。只有确定这个库来自第三方源码安装、确实只能手动处理时,才考虑用LD_LIBRARY_PATH临时指向备份目录验证效果。ldconfig本身解决的是缓存索引问题,库文件实际不存在时执行ldconfig不会让缺失的库凭空出现,别指望它兜底。
4. 服务器环境安装分支:LNMP 与常用组件的最小可用组合
安装分支面向的场景很明确:一台刚装好系统的空白服务器,要变成能跑业务的服务器。这里我以 LNMP(Linux + Nginx + MySQL + PHP)为例展开,这套组合能覆盖大多数入门和中小业务需求。安装脚本的思路也适用于其他组合。
4.1 版本变量和路径约定:安装脚本的第一道闸门
安装脚本最忌讳“装最新版”。最新版往往意味着新特性、新依赖,也意味着社区踩坑经验最少。我在脚本顶部把版本和路径全部收敛成变量,后续所有函数只引用变量,不出现裸版本号:
NGINX_VERSION="${NGINX_VERSION:-1.24.0}" PHP_VERSION="${PHP_VERSION:-8.1.27}" MYSQL_VERSION="${MYSQL_VERSION:-8.0.36}" REDIS_VERSION="${REDIS_VERSION:-7.2.4}" INSTALL_PREFIX="${INSTALL_PREFIX:-/usr/local}" NGINX_PREFIX="$INSTALL_PREFIX/nginx" PHP_PREFIX="$INSTALL_PREFIX/php" MYSQL_PREFIX="$INSTALL_PREFIX/mysql" SRC_DIR="${SRC_DIR:-/usr/local/src}" DATA_DIR="${DATA_DIR:-/var/lib/mysql}"这些变量值是有考量的。Nginx 1.24 是当前维护稳定的主线版本,PHP 8.1 兼容绝大多数存量业务代码,MySQL 8.0 是长期支持版。版本号支持通过环境变量覆盖,意味着这个脚本在同一套基线版本上还能灵活升级。路径统一放在/usr/local下,符合 Linux 的文件系统层次标准,也便于后续卸载或迁移。编译安装前一个必须做的动作是把SRC_DIR下的同名旧目录清掉,避免源码版本和缓存冲突。
4.2 编译安装 LNMP:下载、编译参数与多线程控制
源码编译安装的通用套路是:下载源码包、校验完整性(可选)、解压、configure、make、make install。下面用 Nginx 演示完整函数,MySQL 和 PHP 只是参数不同:
install_nginx() { local src_tarball="$SRC_DIR/nginx-${NGINX_VERSION}.tar.gz" local src_dir="$SRC_DIR/nginx-${NGINX_VERSION}" [ -f "$src_tarball" ] || wget -O "$src_tarball" "https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz" [ -x "$NGINX_PREFIX/sbin/nginx" ] && log_info "nginx 已安装,跳过" && return 0 cd "$SRC_DIR" || return 1 tar xzf "$src_tarball" cd "$src_dir" || return 1 ./configure --prefix="$NGINX_PREFIX" \ --with-http_ssl_module \ --with-http_v2_module \ --with-http_realip_module \ --with-stream \ --with-http_stub_status_module \ --pid-path="$NGINX_PREFIX/logs/nginx.pid" make -j"$(nproc)" && make install log_info "nginx 安装完成,安装路径 $NGINX_PREFIX" }关键编译参数说明:--with-http_ssl_module是 HTTPS 必需,--with-http_v2_module对应 HTTP/2,--with-stream支持 TCP/UDP 代理,--with-http_stub_status_module提供/stub_status页面做健康检查。make -j$(nproc)用满全部 CPU 核数,但这在小内存机器上有风险,后面避坑章节会展开讲。configure 阶段失败时,日志里已经保留了具体缺哪个开发库,按提示补装即可。
PHP 的 configure 里通常要显式指定--with-fpm-user=www --with-fpm-group=www --with-openssl --with-zlib --enable-fpm。这个--enable-fpm特别容易漏,漏了编译出来的 PHP 没有 php-fpm 管理脚本,后面配 systemd 就缺主程序。MySQL 则用编译时间最长的 cmake 流程,核心参数是-DCMAKE_INSTALL_PREFIX和-DMYSQL_DATADIR,此外-DSYSCONFDIR=/etc决定配置文件位置,这三项务必写清楚,其它默认即可。
4.3 systemd 注册与开机自启:service 文件怎么写不翻车
编译安装不会自动注册 systemd 服务,这是“装完了但起不来”的头号原因。手动启动 Nginx 能看到进程,但重启机器后就没了。systemd 里注册服务分两步:写 unit 文件,然后daemon-reload让 systemd 读取它:
register_nginx_systemd() { cat > /etc/systemd/system/nginx.service <<EOF [Unit] Description=nginx server After=network.target [Service] Type=forking ExecStart=$NGINX_PREFIX/sbin/nginx ExecReload=$NGINX_PREFIX/sbin/nginx -s reload ExecStop=$NGINX_PREFIX/sbin/nginx -s quit PIDFile=$NGINX_PREFIX/logs/nginx.pid Restart=on-failure RestartSec=5s [Install] WantedBy=multi-user.target EOF systemctl daemon-reload systemctl enable nginx.service systemctl start nginx.service }Nginx 是典型的多进程 forking 模型:主进程启动后 fork 出 worker 进程,主进程会退出前台。所以 unit 文件必须用Type=forking,并配合PIDFile告诉 systemd 主进程的 PID 写在哪里。如果误写成Type=simple,systemd 会认为主进程一启动就退出,从而判定服务失败。Restart=on-failure表示非正常退出时才拉起,业务主动 stop 不会被误拉;RestartSec防止故障时疯狂重启打日志。这里的$NGINX_PREFIX在写文件前已被展开成具体路径,注意 cat 用的 heredoc 没加引号,这就是刻意的变量展开。
4.4 附加组件:Redis、Python 虚拟环境和 PHP 扩展
安装分支通常不止 LNMP 三件套。Redis 作为缓存组件出现频率很高,编译安装相对轻量:
install_redis() { local src_dir="$SRC_DIR/redis-${REDIS_VERSION}" [ -x "$INSTALL_PREFIX/redis/bin/redis-server" ] && log_info "redis 已安装,跳过" && return 0 cd "$src_dir" || return 1 make -j"$(nproc)" make install PREFIX="$INSTALL_PREFIX/redis" cp "$src_dir/redis.conf" "$INSTALL_PREFIX/redis/redis.conf" # 以非 root 用户运行,降低风险 useradd -r -s /sbin/nologin redis 2>/dev/null || true sed -i 's/^# supervised auto/supervised systemd/' "$INSTALL_PREFIX/redis/redis.conf" sed -i "s|^dir ./|dir $DATA_DIR|" "$INSTALL_PREFIX/redis/redis.conf" log_info "redis 安装完成,配置文件在 $INSTALL_PREFIX/redis/redis.conf" }Redis 默认配置有几个坑:默认以 root 运行、日志目录在当前目录、daemonize默认 no。编译安装后最少改两处,一是supervised systemd让 Redis 能被 systemd 管理,二是dir指向实际数据目录。useradd -r创建系统用户,把服务降权到非 root,这是生产服务器的基本习惯。Redis 的数据目录要提前建好并 chown 给 redis 用户,否则启动时写不了 RDB 文件,现象就是进程起来几秒又退出。
Python 环境在服务器上的处境比较特殊。系统自带的 Python 通常被系统工具依赖,操作系统的包管理器升级它可能弄坏系统组件。所以我装 Python 一律用源码安装加venv虚拟环境,业务代码跑在虚拟环境里,和系统 Python 隔离:
install_python() { local py_ver="3.11.8" local prefix="/usr/local/python-${py_ver}" [ -x "$prefix/bin/python3" ] && log_info "python $py_ver 已安装" && return 0 cd "$SRC_DIR" || return 1 tar xzf "Python-${py_ver}.tgz" cd "Python-${py_ver}" || return 1 ./configure --prefix="$prefix" --enable-optimizations make -j"$(nproc)" && make install "$prefix/bin/python3" -m venv "$INSTALL_PREFIX/venv/app" log_info "python 已安装,虚拟环境在 $INSTALL_PREFIX/venv/app" }--enable-optimizations会做 profile-guided optimization,编译时间长一倍,但解释器运行性能更好。对计算类业务影响明显,对纯 web IO 业务则收益有限,机器性能弱时可以去掉。venv 创建后,业务代码的依赖全部通过$INSTALL_PREFIX/venv/app/bin/pip install安装,不污染系统 site-packages,升级 Python 或迁移机器时也有干净边界。
5. 避坑:修复脚本和安装脚本最容易翻车的五个现场
脚本我写了很多轮,踩过的坑比看过的教程多。这五个现场基本把新手的坑和老手的坑都覆盖了,逐一按“现象→原因→解决”写清楚。
5.1 现象:Debian 系机器上脚本跑一半报 “package not found”
用的一键脚本在 Debian 12 上跑到安装依赖那步直接失败,提示找不到包。原因大概率是脚本里用了apt install而不是apt-get install,或者apt-get update没有先执行。apt是面向交互的命令,在脚本非交互环境下对部分参数的处理不如apt-get稳定;而包索引过期时,任何 install 都会报找不到包。
解决:脚本内统一封装包管理器命令,首次安装前先 update,install 用--no-install-recommends缩减依赖面。如果脚本是从 CentOS 那套迁移过来的,还要注意 CentOS 7 的包管理器是 yum,CentOS 8+ 变成 dnf,两者兼容但 dnf 的 repo 配置语法不同,不能直接照搬。
5.2 现象:重装 Nginx 后 systemctl start nginx 失败,端口被占用
给一台跑过业务的机器重装环境,新 Nginx 启动时报bind() to 0.0.0.0:80 failed。原因不是新装的服务配置错了,而是旧 Nginx 的 master 进程还活着,或者编译安装的旧路径/usr/local/nginx和 systemd unit 里的新路径不一致。很多重装脚本直接覆盖文件,却不管已经在跑的进程。
解决:安装前先探测端口和进程,ss -lntp | grep :80找到 PID,ps -fp <pid>确认是不是旧 nginx,是则先nginx -s quit或 kill。随后再检查 unit 文件里的PIDFile路径和ExecStart路径是否和新安装位置一致,不一致时daemon-reload也不会生效,必须修改 unit 文件。
5.3 现象:脚本里 export 了环境变量,但登录终端后 PATH 就是没变
安装脚本在最后往/etc/profile.d/写了一个env.sh,里面 export 了 PATH,脚本执行完也确实 echo 出来变量正确,可用户重新登录后却找不到新装的命令。原因是没有理解登录 shell 和非登录 shell 的读取差异。/etc/profile.d/*.sh在 login shell 里被读取,但很多运维习惯开 screen/tmux 或直接执行bash进入子 shell,这些场景读的是~/.bashrc,不读 profile。
解决:安装脚本里给当前用户写一份~/.bashrc的追加配置,并提示用户重新登录或手动source。更稳妥的是把关键可执行文件做个软链到/usr/local/bin,因为这个目录通常已经在 PATH 里,能绕开 shell 配置差异。
5.4 现象:小内存机器编译 MySQL 或 PHP 直接 OOM,报错误导排查
一台 1G 内存的机器执行一键安装,编译到一半被系统直接 kill,日志尾部是configure: error: C compiler cannot create executables,看起来像缺编译器。原因不是 gcc 坏了,而是内存耗尽后编译进程被杀,临时文件的失败间接体现在 configure 上。
解决:编译大型项目前检查物理内存,free -m低于 2G 时不要用make -j$(nproc),改为make -j1,减少并行编译进程数。还可以临时加 swap,fallocate -l 2G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile,编译完再视情况保留或关闭 swap。MySQL、LLVM 这类重编译项目特别吃内存,这是血泪经验。
5.5 现象:修复依赖时把正在用的服务给卸了
一键修复脚本跑到apt-get autoremove清理依赖,结果把 nginx 依赖的 libpcre3 给删了,重启后站点 500。原因在于 autoremove 会把“被标记为自动安装”的包全部清掉,而这个标记在手动编译安装 nginx 时不会建立,系统并不认为 libpcre3 被 nginx 依赖,于是当作孤儿包清理。
解决:修复脚本里不要用无脑autoremove,先apt-get autoremove --dry-run看清单,人工扫一眼有没有不该动的包。更稳的做法是安装前用apt-mark manual libpcre3标记核心运行库为手动安装,让 autoremove 跳过它。脚本加幂等标记之外,清理类动作永远是“先干跑、再真跑”。
6. 自检、回滚和日志审计:让一键脚本不背黑锅
脚本跑完只是开始,确认它真的把活干好了,才是收尾的关键。我的习惯是每个安装函数结束后自动跑一轮自检,把结果写入日志,而不是只有一句“安装完成”。
6.1 安装完成后的十秒钟自检命令
health_check() { log_info "===== 安装后自检 =====" $NGINX_PREFIX/sbin/nginx -t 2>&1 | tee -a "$LOG_FILE" systemctl is-active nginx || log_error "nginx 未在运行" "$MYSQL_PREFIX/bin/mysqladmin" ping -u root --silent 2>&1 | tee -a "$LOG_FILE" "$PHP_PREFIX/sbin/php-fpm" -t 2>&1 | tee -a "$LOG_FILE" redis-cli ping 2>&1 | tee -a "$LOG_FILE" }nginx -t只验证配置文件语法,和实际启动不是一回事,所以后面要跟一条systemctl is-active。mysqladmin ping返回mysqld is alive才算真活了,注意新装 MySQL 默认 root 可能只有 socket 认证,用-u root探测的是本机 socket 连接,不涉及网络权限问题。php-fpm 的-t同样只检查配置,最终以 systemd 状态为准。自检的全套输出进日志,而不是只在终端闪现,这样真出问题时日志里的自检段就是第一现场。
6.2 回滚习惯:改了什么、备份在哪、怎么撤
一键脚本的价值一半在安装效率,另一半在可回滚。我在脚本里统一维护一个操作清单文件,每次修改系统配置前先备份,备份路径记进清单。比如改/etc/nginx/nginx.conf前先复制一份到同目录.bak.时间戳,改/etc/profile.d同理。回滚时不需要脚本自动完成,因为自动回滚在全量安装场景里容易误伤新装的文件,但“备份在哪”必须随时可查。
这份清单文件通常放在/var/log/one-click-changes.log,每行记录操作时间、文件路径和备份路径。排查问题的上午、回想脚本到底动了哪些文件的深夜,这份日志就是唯一的后悔药。现在写的每个一键脚本我都强制要求自己先加好日志和备份位,再写业务逻辑——顺序反了,脚本跑得再快也不敢拿到生产环境上重复执行。希望这些思路对你手上的修复和安装脚本方案有帮助。
本文还有配套的精品资源,点击获取