# 自己写了个一键部署脚本，能动态选版本

**URL:** <https://forum.rainyun.com/t/topic/13354>\
**Category:** 云服务器 RCS\
**Created:** [2026 年8 月 3 日 13:50 UTC](https://forum.rainyun.com/t/topic/13354 "2026-08-03T13:50:44Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

作者： ![gzy](https://forum.rainyun.com/user_avatar/forum.rainyun.com/gzy/32/12796_2.png) [@gzy](https://forum.rainyun.com/u/gzy)\
发布日期： [2026 年8 月 3 日 13:50 UTC](https://forum.rainyun.com/t/topic/13354/1 "2026-08-03T13:50:45Z")

</div>

​本文来自我的个人博客：[自己写了个一键部署脚本，能动态选版本 - 叹惋博客](https://twbk.cn/355.html "自己写了个一键部署脚本，能动态选版本 - 叹惋博客")

​

### **为什么要自己写部署脚本**

买了新服务器如果要部署项目，第一件事是什么？装环境。

Nginx、MySQL、PHP、Docker、Node.js……一个一个装下来，少说十几条命令。装完之后还得配防火墙、改权限。这些事情做一次两次还好，做多了就变成纯粹的重复劳动（对于我这种喜欢经常换服务器的人来说）。最近我又去[雨云](https://www.rainyun.com/xls_)买了一台服务器，我又想到这个问题了。

网上有一键安装包，比如 oneinstack、[lnmp.org](https://lnmp.org/) 那些。但它们有个共同的问题： **你基本不知道它在你服务器上干了什么** 。装完之后文件散落在哪里、改了哪些配置、装了什么版本，全都是黑箱。一旦出了故障，你连从哪里开始排查都不知道。

另一个痛点是版本。很多一键脚本把版本号写死在代码里，比如 `nginx_version="1.18.0"`。过半年官方出了 1.26，你想装新版？要么等作者更新脚本，要么自己改代码。自己改还有个风险——改完能不能跑通是另一回事。

上周，我在[雨云](https://www.rainyun.com/xls_)购买了一台云服务器，他们家的服务器是可以预装LNMP和docker的。

 ![](https://forum.rainyun.com/uploads/default/original/2X/8/88bb2b9d5cfd41afa0f612d51fae903f8c8e4eaf.jpeg)

但是我最终还是选择安装了雨云提供的纯净镜像，自己再去装LNMP和docker，原因在于，雨云提供的预装的镜像都是有固定的系统和软件版本的，无法自己随心安装使用。

所以我想（在AI帮助下）写一个 **完全透明、能动态获取版本、结构清晰到任何人都能看懂的部署脚本** 。这就是 LXDeploy 的由来。也欢迎各位雨云和其他云服务商用户前来体验。

### **整体架构：分层设计**

整个项目分成两层：

**install.sh（安装器）** 负责一件事：把主脚本拉到用户机器上，加到 PATH 里。它不留下多余的配置文件，不污染系统目录。用户执行完 `curl | bash` 之后，得到的只是一个 `lxdeploy` 命令。

**lxdeploy.sh（主脚本）** 才是核心。它的结构是这样的：

```auto
主菜单（main_menu）
    ├── 工具函数层（颜色、日志、系统检测、IP获取）
    ├── 版本获取层（fetch_*_versions）
    ├── 版本选择层（select_version）
    ├── 幂等检查层（is_installed）
    └── 安装执行层（deploy_lnmp / install_docker / ...）

```

每一层只做一件事，层与层之间通过变量传递数据。比如版本选择层只负责把用户选的版本号存到 `SELECTED_VERSION` 里，安装执行层只负责读这个变量然后去装。这样拆开的好处是：哪天你想换一种版本获取方式（比如不从 apt-cache 抓，改从官网 API 抓），只需要改 `fetch_*` 函数，其他代码一行都不用动。

### **一、动态版本获取**

#### **1.1 一个容易被忽略的痛点**

大多数一键脚本会把版本号写死在代码里，比如：

```auto
NGINX_VERSION="1.18.0"
PHP_VERSION="7.4.33"

```

这种做法有两个问题。第一，官方发布新版本之后，用户想用新版只能等作者更新脚本；第二，用户根本没得选，装什么版本完全由脚本作者决定。

有人可能会说：“那我把版本号做成配置文件让用户自己填不就行了吗？”这确实是一个改进，但本质上还是在要求用户“自己去查版本号是什么”。用户需要先知道有哪些版本可用，然后才能决定装哪个——这又把负担推回给了用户。

LXDeploy 的做法是： **每次运行时，直接从包管理器里拉取当前所有可用版本，让用户当场选** 。

#### **1.2 apt-cache madison 和 yum --showduplicates 的原理**

在 Debian/Ubuntu 系统上，用的是 `apt-cache madison` 命令。`apt-cache` 是 APT 包管理工具集中的一个命令，它不修改系统状态，只负责查询和展示本地缓存的软件包元数据。这些元数据是之前执行 `apt update` 时从配置的软件源下载下来的。`madison` 是 `apt-cache` 的一个子命令，它的设计初衷是模仿 Debian 官方仓库管理工具 madison 的输出格式，以表格形式展示一个软件包的所有可用版本。简单说，`apt-cache madison nginx` 的输出就是当前系统配置的所有软件源里，nginx 这个包有哪些版本可以装。

在 CentOS/RHEL 系统上，对应的是 `yum --showduplicates list nginx`。`--showduplicates` 这个参数告诉 yum，在列出软件包时不要只显示最新版本，而是把仓库里所有的版本都显示出来。yum 的仓库元数据是由 `createrepo` 工具生成的，里面包含了每个 RPM 包的版本号、依赖关系等信息。加上 `--showduplicates` 之后，yum 就会把这些元数据里的所有版本都呈现出来。

获取到版本列表之后，再用 `sort -Vr` 做一次版本号排序。`-V` 是“版本号感知”的排序，能正确处理 `1.10` 和 `1.9` 这种数字比较；`-r` 是倒序，最新的排在最前面。

#### **1.3 为什么 Node.js 的获取方式不一样**

Node.js 的情况比较特殊。如果用 apt 或 yum 去装 Node.js，能获取到的版本取决于你添加的软件源——nodesource 的源通常只提供当前 LTS 版本，不会把所有历史版本都放上去。

所以 LXDeploy 换了一种思路：直接从 Node.js 官网的发布目录抓取版本列表。

```auto
curl -s https://nodejs.org/dist/ | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' | cut -c2- | sort -Vr | head -30

```

这条命令做的事情是：请求 Node.js 官方发布页的 HTML，用正则表达式匹配出所有 `vx.y.z` 格式的版本号，去掉前缀 `v`，排序，只取前 30 个。这样不管官方发布了什么版本，脚本都能感知到。而且只取前 30 个是为了避免列表太长，实际使用中 30 个版本已经远远够用了。

#### **1.4 偏移量选择：不让用户背版本号**

有了版本列表之后，怎么让用户选是个交互设计问题。最直接的做法是让用户输入版本号，比如 `1.26.0`。但这就要求用户记住版本号长什么样——`1.26.0-1~focal` 这种东西，正常人记不住。

LXDeploy 设计了一套偏移量选择：

- 输入 `0` → 选列表第一项（最新版）
- 输入 `-1` → 选列表第二项（上一个版本）
- 输入 `-2` → 选列表第三项（上上个版本）

这套设计的出发点是：用户真正需要的不是“记住版本号”，而是“选一个稳定的版本”。最新版出了问题就退一个，再有问题再退一个，很方便啊也是。

### **二、幂等性**

#### **2.1 什么是幂等性**

幂等性（Idempotence）这个词来自数学，在计算机领域的意思是： **同一个操作执行多次和执行一次，产生的效果完全相同** 。

放在部署脚本的场景里，就是： **不管用户跑一次脚本还是一百次脚本，服务器的最终状态应该是一致的** 。

为什么要强调这个？因为用户在实际使用中，几乎一定会跑第二次。第一次装完环境之后，过了一段时间想加装一个 Redis，用户不会去翻文档找“只装 Redis”的命令——他最自然的做法是重新跑一遍整个脚本。如果脚本没有幂等性，第二次跑的时候就会出问题：文件已存在、端口被占用、服务已启动……各种报错。

#### **2.2 实现方式：先查后做**

LXDeploy 实现幂等性的方式很简单：每个操作执行之前，先检查目标是否已经达成。

```auto
is_installed() {
    local pkg=$1
    if [[$PM == "apt"]]; then
        dpkg -l $pkg 2>/dev/null | grep -q "^ii"
    else
        rpm -q $pkg 2>/dev/null | grep -q "is installed"
    fi
}

```

`dpkg -l` 列出所有已安装的包，`grep "^ii"` 匹配状态为“已安装”的记录。`rpm -q` 直接查询某个包是否已安装。两个命令都加了 `2>/dev/null` 把错误输出丢掉，避免查询不存在的包时打印警告。

每个安装函数在动手之前都会调用这个检查：

```auto
if is_installed nginx && [[-z "$nginx_ver"]]; then
    echo "Nginx 已安装，跳过"
else
    # 执行安装
fi

```

这里还有一个细节：`&& [[-z "$nginx_ver"]]` 的意思是“如果用户没有指定特定版本，才跳过”。如果用户指定了某个版本，即使系统里已经装了 Nginx，脚本也会尝试安装指定版本——这给了用户“降级”或“升级”到特定版本的能力。

#### **2.3 幂等性的边界**

需要说明的是，LXDeploy 的幂等性是“安装级别”的，不是“配置级别”的。也就是说，脚本能保证“不会重复安装同一个软件包”，但不能保证“配置文件始终保持一致”。如果用户手动修改了 Nginx 的配置文件，再次运行脚本不会去覆盖它——因为脚本的设计目标是“装环境”，不是“配置管理”。

这个取舍是故意的。配置管理是 Ansible、Puppet 这类工具擅长的领域，一个 Bash 脚本强行去做配置管理，只会把事情搞复杂。

### **三、跨发行版兼容**

#### **3.1 两大包管理器家族的差异**

Linux 发行版虽然都叫 Linux，但包管理器的差异很大。Debian/Ubuntu 用 apt，CentOS/RHEL 用 yum。这两个包管理器不仅命令不同，包名也可能不同——比如 `p7zip-full` 在 Ubuntu 上叫这个名字，在 CentOS 上叫 `p7zip`。

LXDeploy 的处理方式是先检测系统类型，然后根据类型走不同的分支。

检测的依据是 `/etc/os-release` 文件。这是 systemd 引入的标准文件，几乎所有现代 Linux 发行版都有，里面定义了 `ID`（发行版标识）和 `VERSION_ID`（版本号）等字段。对于不支持 `/etc/os-release` 的旧系统（比如 CentOS 6），回退到读取 `/etc/redhat-release`。

```auto
if [[-f /etc/os-release]]; then
    . /etc/os-release
    OS=$ID
    OS_VERSION=$VERSION_ID
elif [[-f /etc/redhat-release]]; then
    OS="centos"
    OS_VERSION=$(rpm -q --qf "%{VERSION}" $(rpm -q --whatprovides redhat-release) | cut -d. -f1)
fi

```

检测到系统类型之后，设置 `PM` 变量为 `apt` 或 `yum`，后续所有包管理操作都通过这个变量来路由。

#### **3.2 包名不一致的处理**

包名不一致的问题更隐蔽。同样是安装 PHP 的 MySQL 扩展，Ubuntu 上的包名是 `php-mysql`，CentOS 上是 `php-mysqlnd`。如果写死了包名，换一个系统就装不上。

LXDeploy 的处理方式是在安装函数里根据 `PM` 变量走不同的分支：

```auto
if [[$PM == "apt"]]; then
    apt install -y php-mysql php-curl ...
else
    yum install -y php-mysqlnd php-curl ...
fi

```

这种做法虽然不够优雅（每安装一个软件都要写两套包名），但胜在清晰——用户看代码的时候能一眼看出不同系统上装的是什么包，出了问题也容易定位。

#### **3.3 为什么不直接用 Ansible**

有人可能会问：既然跨发行版兼容这么麻烦，为什么不直接用 Ansible？

Ansible 确实能优雅地解决这些问题——它有专门的 `package` 模块抽象了不同包管理器的差异，有 `ansible_facts` 自动收集系统信息。但 Ansible 的代价是：用户需要先装 Python、配 inventory、写 playbook。对于一个只想“快速搭个环境”的用户来说，这个门槛太高了。

Bash 脚本的优势就是 **零依赖** ——任何 Linux 系统都自带 bash，不需要额外安装任何东西。用户拿到脚本就能跑，没有任何前置条件。代价就是跨发行版的兼容性需要自己处理。

### **四、错误处理**

#### **4.1 错误处理的两种思路**

Bash 脚本的错误处理有两种常见的哲学。

一种是“严格模式”：在脚本开头加上 `set -euo pipefail`，让脚本在任何命令失败时立即退出。这种做法的好处是能尽早发现问题，坏处是脚本变得很脆弱——任何一个非关键命令失败都会导致整个脚本中断。

另一种是“容错模式”：每个可能失败的命令都单独检查返回值，根据情况决定是继续还是退出。这种做法的好处是灵活，坏处是代码会变得很冗长。

LXDeploy 走的是中间路线： **关键操作严格检查，非关键操作容错处理** 。

#### **4.2 关键操作 vs 非关键操作**

什么是关键操作？软件安装就是关键操作。如果 Nginx 没装上，后续的配置都没有意义。所以安装命令会检查返回值：

```auto
apt install -y nginx
if [[$? -ne 0]]; then
    echo "Nginx 安装失败，请检查网络或软件源"
    exit 1
fi

```

什么是非关键操作？防火墙放行就是非关键操作。有些系统可能根本没装防火墙，放行命令失败不代表环境有问题。所以防火墙操作会加 `2>/dev/null` 静默掉错误：

```auto
ufw allow 80/tcp 2>/dev/null

```

#### **4.3 版本指定失败的降级策略**

版本指定安装是一个比较特殊的场景。`apt install nginx=1.26.0` 这个命令有可能失败——可能是因为版本号写错了，可能是因为软件源里没有这个版本，也可能是因为版本号里包含特殊字符。

LXDeploy 的处理方式是“尝试指定版本，失败则降级到默认版本”：

```auto
apt install -y nginx=${nginx_ver} 2>/dev/null || apt install -y nginx

```

`||` 左边的命令失败时，会自动执行右边的命令。这样用户选的版本能装上最好，装不上也不至于整个脚本崩溃，而是回退到装默认版本。

这个设计的出发点是： **用户选择版本是“期望”不是“强约束”** 。用户说想装 1.26，但如果因为某些原因装不上，装 1.24 也比什么都不装强。

### **五、国内镜像源**

#### **5.1 被忽视的网络问题**

写部署脚本的人很容易忽视一个问题：很多用户的服务器在国内，从国外软件源拉取软件包的速度非常慢。一个几十 MB 的包可能要下十几分钟，整个部署流程被网络延迟拖垮。

这个问题在技术上并不复杂——把软件源换成国内的镜像就行了。但很多脚本没有这个功能，用户只能自己去改 `/etc/apt/sources.list` 或者 `/etc/yum.repos.d/` 下的文件。

#### **5.2 实现方式**

LXDeploy 的“切换国内镜像源”功能做的事情很简单：把 apt 或 yum 的源地址替换成阿里云的镜像地址。

对于 Ubuntu，把 `archive.ubuntu.com` 换成 `mirrors.aliyun.com`；对于 CentOS，把 `mirror.centos.org` 换成 `mirrors.aliyun.com`。替换之前先备份原文件，万一出了问题用户可以恢复。

这个功能的技术含量不高，但在实际使用中，它可能是用户最常用的功能之一。下载速度从几十 KB/s 提升到几 MB/s，体验差别非常大。

这也引出一个更通用的设计原则： **功能的价值不取决于技术难度，而取决于它解决了什么问题** 。一个简单的镜像源切换，可能比一个复杂的负载均衡配置对用户更有用。

### **六、总结**

其实我觉得一个脚本功能多的不一定好。OneinStack 支持 LEMP、LAMP、LNMP、LNMPA、LTMP 等好几种组合，功能确实多，但用户真的需要这么多选择吗？大部分用户只是想装一个能跑 PHP 网站的环境，给他十几个选项反而增加了决策负担。

技术复杂的不一定好。用 Ansible 写部署脚本确实更优雅，但用户需要先学 Ansible。Bash 脚本虽然原始，但用户拿到就能看懂、就能改——这才是最重要的。

设计LXDeploy前，我让deepseek给了我三个要注意的点，我自己又修改了一下，也是按照这些要点写的：

**1.让用户知道脚本在做什么。** 所有操作都有明确的输出，所有选择都有清晰的提示。用户不是面对一个黑箱，而是在和一个透明的工具交互。

**2.让用户有选择的权利。** 版本可以选，镜像源可以切，安装可以跳过已存在的组件。脚本不替用户做决定，而是帮用户做决定。

**3.让脚本能安全地反复运行。** 幂等性不是锦上添花，而是基本要求。用户不应该因为“跑了一遍脚本”就被迫重装系统。

这三个原则听起来很简单，但在实际实现中，每一个都需要在代码层面认真对待。动态版本获取需要处理不同包管理器的差异，幂等性需要在每个操作前做状态检查，透明化意味着每个步骤都要有清晰的输出。

这些工作单独看都不算什么高深的技术，但合在一起，就构成了一个“让人放心”的工具。而这，正是 LXDeploy 想要达到的目标。

### **七、使用截图**

安装脚本：

 ![](https://forum.rainyun.com/uploads/default/original/2X/5/5e4693f71e0373238fdf859e2202eea0e2a5b83e.png)

主脚本：

 ![](https://forum.rainyun.com/uploads/default/original/2X/6/616ebb40fdd5de4eb8e64c888423583df334f8cb.png)

LNMP选择版本：

 ![](https://forum.rainyun.com/uploads/default/original/2X/f/fa6e3e3d2fae3db2d72d14114e4f91ef263a045f.png)

安装完成展示：

 ![](https://forum.rainyun.com/uploads/default/original/2X/b/b4f33e1c54d680a2d93b8054625fabcbd16cd3f6.png)

* * *

**项目地址：[https://github.com/gzy318/LXDeploy](https://github.com/gzy318/LXDeploy)**
