--- url: /sre/devops/foundation/02-production-env.md description: 理解生产环境的两个最低必要条件——7×24 与公网 IP;完成第一次 SSH + scp 手动部署,并用有向无环图(DAG)刻画发布步骤之间的依赖关系。 --- 在上一篇文章中,已经在本地用 Nginx 跑通了静态站点。然而,`http://localhost` 只有你自己的电脑能访问。如果想让网站发布到互联网,需要将它部署到 {{term:生产环境}}。 ## 什么是生产环境 从本地开发到线上生产,核心变化在于服务对象的改变。我们用一张表直观对比: | 维度 | 开发环境 (Development) | 生产环境 (Production) | | :--- | :--- | :--- | | **服务用户** | 你自己 | 互联网真实用户 | | **运行时间** | 随用随开 | 7 × 24 小时无间断 | | **网络边界** | 本地局域网 | 互联网公网可达 | | **数据故障** | 随时重来 | 线上事故 | 要让网站上线,生产环境必须跨越两个最低门槛:**7 × 24 小时运行** 与 **具备公网 IP**。对于个人而言,最快捷、低成本的达成方案就是租用一台 {{term:云服务器}}。 ## 为什么选择云服务器 云厂商对新用户扶持力度大,包年购买一台入门的 **2 核 2G(2C2G)** 轻量服务器通常只需数十元(**价格随厂商活动变动,请以官方页面为准**)。在选购云服务器时,规格选择 **2 核 2G**,系统镜像首选 **Ubuntu 22.04 或 24.04 LTS**(**截至 2026 年 8 月**),避开预装各种面板的系统,保持环境纯净。 你可以通过我的专属优惠链接直接选购: * [阿里云上云优选通道(专享优惠)](https://www.aliyun.com/daily-act/ecs/activity_selection?userCode=d1pmxxar) * [腾讯云轻量服务器通道(专享优惠)](https://curl.qcloud.com/LL4937aa) > **💡 运维小经验:2C2G 的配置为什么够用?** > Linux 生产服务器无须运行图形界面,系统本身开销通常不到 200MB。Nginx 作为静态资源服务器时也很轻量,2GB 内存足以支撑个人站点和学习环境。 ## 环境准备:SSH 登录与基建安装 ### SSH 连接服务器 购买云服务器后,你通常会拿到三类信息: | 信息 | 作用 | | --- | --- | | 公网 IP | 从互联网访问这台服务器 | | 登录用户 | 不同厂商系统可能不同,推荐使用默认用户 `ubuntu` | | 登录密码或 SSH 密钥 | 用来证明“你有权限登录” | 连接服务器有两种常见方式。 **方式一:在本地终端使用 SSH** 这是最推荐掌握的方式,因为后续部署、上传文件、排查问题都离不开命令行: ```bash ssh ubuntu@your-public-IP ``` 如果云厂商默认用户不是 `ubuntu`,就把命令里的 `ubuntu` 换成对应用户名,高权限操作通过 `sudo` 临时提权,能降低误删系统文件、误改全局配置的风险。 ```bash whoami ``` 可以查看当前登录用户。 首次 SSH 连接会提示确认主机指纹,输入 `yes`,然后输入服务器登录密码即可登录。 **方式二:使用云厂商提供的 WebUI** 阿里云、腾讯云等控制台通常都提供“远程连接”之类的浏览器入口(腾讯云侧常见为 OrcaTerm 等),不需要你本地先配好 SSH。 WebUI 适合第一次确认服务器能否登录。但它更像备用入口:后续 `scp` 上传、脚本化部署、CI 自动发布,都会更依赖本地终端里的 SSH。 ### 密码登录和 SSH 密钥有什么区别 SSH 是连接服务器的协议,密码和 SSH 密钥是两种不同的认证方式。 | 认证方式 | 怎么理解 | 优点 | 缺点 | | --- | --- | --- | --- | | 密码登录 | 输入服务器账号密码 | 简单直观,适合初学者第一次登录 | 容易被弱密码、撞库、暴力破解影响 | | SSH 密钥 | 本地保存私钥,服务器保存公钥 | 更安全,适合长期使用和自动化部署 | 初学者第一次配置稍微复杂 | 我的建议是:**初学者第一次学习可以先用 `ubuntu` 用户 + 密码登录,跑通完整流程;一旦网站真正长期在线,就切换到 SSH 密钥登录,并关闭 root 密码登录。** > 💡 **终端与 Shell**:本章命令都会在终端里跑。建议花十分钟配好 Shell(代码高亮、自动补全等),大幅提升后续工作效率。具体配置见文末"延伸阅读"。 ### 安装 Web 服务器 (Nginx) 在服务器上安装 Nginx(和本地一样的步骤): ```bash sudo apt update sudo apt install nginx ``` 验证安装: ```bash nginx -v ``` 启动 Nginx: ```bash sudo systemctl start nginx ``` > 如果浏览器访问公网 IP 加载超时,优先检查云控制台的 {{term:安全组}} 或“防火墙”是否已放通 TCP 80 端口。也可以在本地终端快速验证服务与端口: > > ```bash > curl -I http://your-public-IP > ``` > > 返回 `200 OK` 即表示服务和端口都正常——浏览器打不开多半就是安全组或本机网络的问题。 此时在浏览器访问 `http://your-public-IP`,应该能看到 Nginx 默认欢迎页面。如果不想打开浏览器,本地终端一行也能验: ```bash curl http://your-public-IP | head ``` 输出含 `Welcome to nginx` 字样即说明页面响应正常。恭喜,你的服务器已经在公网可访问了。 ## 首次发布:基于 scp 的手动发布 现在把本地的网站文件传到云服务器上。 ### 本地构建 在本地项目目录中构建生产版本: ```bash pnpm run docs:build ``` 构建产物在 `docs/.vitepress/dist/` 目录下。 ### scp 上传到服务器 使用 `scp` 命令将构建产物上传到服务器: ```bash scp -r docs/.vitepress/dist ubuntu@your-public-IP:~/ ``` 这条命令做了什么: * `scp`:基于 SSH 的文件复制命令 * `-r`:递归复制目录 * `docs/.vitepress/dist`:本地构建产物目录 * `ubuntu@your-public-IP:~/`:复制到服务器 `ubuntu` 用户的家目录 上传完成后,登录服务器,将文件复制到 Nginx 静态资源目录: ```bash sudo cp -r ~/dist/* /var/www/html/ ``` 复制完成后做两步最小验证: ```bash ls /var/www/html/ # 确认 index.html 等静态资源已就位 curl -s -o /dev/null -w "%{http_code}\n" http://your-public-IP # 应返回 200 ``` 两步都通过就不需要重启 Nginx,刷新浏览器访问 `http://your-public-IP`,看到你的网站了——这就是发布到公网。 ## 发布只是开始 至此,已经完成了一次完整的“本地 → 服务器”部署。流程是通的,但用不了多久你就会遇到这些问题。 ### 痛点一:本地和服务器环境不一致 部署不是一步到位的。第一次把网站放到服务器上,大概率不会一次成功——Nginx 配置路径不对、权限不够、文件位置不对,都需要登录服务器反复调试。这意味着你需要同时维护**本地开发环境**和**服务器生产环境**两套配置。 回顾一下:本地 Mac 上安装 Nginx 用 `brew install nginx`,服务器 Ubuntu 上用 `apt install nginx`,连安装方式都不一样。配置文件路径也不同——Mac 在 `/opt/homebrew/etc/nginx/`,Ubuntu 在 `/etc/nginx/`。 好消息是,这些差异通常只需要首次部署时排查清楚,后续就稳定了。坏消息是,**{{term:可移植性}}很差**——如果你换了一台服务器(比如从阿里云换到腾讯云),或者换了操作系统版本,之前踩过的坑可能要重新踩一遍。环境配置的细节散落在操作记忆里,没有被固化下来。 在容器与流水线出现之前,这种"散落"几乎不可避免——手动部署天然没有可复用的载体。真正可靠的做法,是把环境、交付步骤和验证方式逐步固化为可复用的配置与流程。 当前部署的是纯静态资源,跨平台传输没有问题。但涉及编译产物时,环境差异的影响会更加突出,我们在[第 03 篇](./03-server-side-deploy.md)展开。 ### 痛点二:每次发布都要手动 scp 软件交付其实分三个阶段,**节奏完全不同**: | 阶段 | 环境 | 节奏 | 在干什么 | | --- | --- | --- | --- | | **① 开发迭代** | 本地(env.local) | **循环多次** | 改代码 → 本地预览 → 直到满意 | | **构建** | 本地(env.local) | **验收后一次** | 将源码打包成可部署的产物(`dist/`) | | **② 交付** | 本地 → 云服务器 → 公网 | **通常一次** | 上传 → 公网验证 | 构建属于开发环境的工作,但不在迭代循环内——它是"满意"之后的单次操作,是开发迭代与交付之间的边界动作。 ![开发迭代 ×N、构建一次、交付到公网](https://media.xiaolin.fun/docs/img-production-env/diagram-dev-iter-delivery.png) 本地热更新很快;真正费事的是验收后那一次跨机器交付——构建在本地,上传在云服务器,验证还要看公网。步骤一多就容易漏,表面都是"页面没变化"。 不过交付链本身可以**模板化**:构建 → 上传 → 验证。这就是 {{term:流水线}}(Pipeline)的雏形——也是本系列对**生产变更**这一面建立起形式化表达的入口。 ### 生产变更的数学表达 **生产变更**的本质是一组**有序操作步骤**:开发侧提交的内容变了,对应的发布流程就要重跑一遍。 一次生产变更可以形式化为一组**步骤**——白话说,就是一张**操作清单**: $$ Y = {v\_1,, v\_2,, \ldots,, v\_n} $$ 其中,每个 $v\_i$ 是清单上的一项(构建、上传、验证……)。对于不同的开发提交 $x$,清单里的具体内容不同(如上传的文件、版本号),但清单的骨架保持不变——本篇就是固定的"构建 → 上传 → 验证"三步。 #### 步骤之间的关系:有向无环图 步骤之间不是孤立的:上传必须等构建完成,验证必须等上传完成。**$Y$ 内部是一张有向无环图(DAG,Directed Acyclic Graph)**。 发布步骤形式化为图 $G = (V,, E)$: $$ V = {v\_1,, v\_2,, \ldots,, v\_n} $$ 其中,节点 = 发布步骤。 $$ E \subseteq V \times V $$ 其中,有向边 $v\_i \to v\_j$ 表示:$v\_j$ 须等 $v\_i$ 完成后才能启动。 **有向**:依赖关系有且仅有一个方向——上传依赖构建产物,验证依赖上传完成,不能倒过来。\ **无环**:任意步骤不能(直接或间接)依赖自身,否则流水线将死锁,永远无法结束。 合法执行顺序由 **拓扑序(topological order)** 给出: $$ \tau : V \to {1, 2, \ldots, |V|} $$ 并满足: $$ \forall, (v\_i \to v\_j) \in E:; \tau(v\_i) < \tau(v\_j) $$ 每个步骤必须在其所有前驱完成后才能启动。**拓扑序存在当且仅当图无环**——这正是"无环"成为硬约束的原因:有环就不存在合法执行序,流水线在定义层面就失效了。 #### 本篇的 $Y$:最简线性 DAG 本篇的发布流程是**最简的线性 DAG**($|V|=3,;|E|=2$): $$ v\_1 \xrightarrow{} v\_2 \xrightarrow{} v\_3 $$ | 步骤 | 含义 | | --- | --- | | $v\_1$ | 构建 | | $v\_2$ | 上传 | | $v\_3$ | 验证 | 拓扑序唯一:$\tau(v\_1)=1,;\tau(v\_2)=2,;\tau(v\_3)=3$。"先构建、再上传、后验证"不是约定俗成的习惯,是 DAG 约束下的唯一合法序。 > **💡 为什么没有"重启"或"重载"步骤?** > 本篇部署的是纯静态资源——直接覆盖 `/var/www/html/` 下的文件即可。Nginx 读取的是磁盘文件,下次请求就拿到新版本,**无需重启也无需 reload**。 > > 如果变更涉及 Nginx 配置文件(`nginx.conf`、`sites-enabled/*`),上传后需要追加两步: > > 1. `sudo nginx -t`:测试新配置语法(避免写错配置直接导致 Nginx 起不来) > 2. `sudo nginx -s reload`:平滑重载——master 进程保持运行,worker 平滑切换,不中断正在处理的请求 > > 此时 DAG 变成 5 步:构建 → 上传 → 配置测试 → 平滑重载 → **黑盒验证**。`reload` 优于 `restart`:前者不中断服务,后者会断开所有连接。 > > 验证放在最后一步是有讲究的——这里的"验证"是**黑盒验证**(从外部发 HTTP 请求,确认服务行为符合预期)。它必须等所有步骤都做完才能进行:reload 没跑、新配置没生效,外部请求拿到的还是旧行为,验证就失去了意义。 本次部署的 $Y$ 是最简的线性 DAG——下一步只改文案,**这张图不变**。但当流程出现并行分支(比如同时上传到多个区域),DAG 能表达"局部并行、整体有序"的约束,单纯的"$\to$"写法做不到。把这些依赖关系明确下来,正是后续自动化交付能够稳定运行的基础。 ### 痛点三:没有版本追踪 运维领域的常规做法是:部署前先备份当前版本,再覆盖新版本。比如把旧的 `dist` 重命名为 `dist-20260713`,然后再上传新的 `dist`。这样出了问题至少还能手动回退。 但手动备份有三个绕不开的断点: **第一,备份这一步本身会被跳过。** “这次改动很小,应该没问题”、“备份目录越来越多,占空间”、“上次备份了也没用过”——这些理由每次都成立,结果就是备份时有时无,**一旦跳过就回不去**。 **第二,即使做了备份,也只是“一份旧的目录”。** `dist-20260713` 对应的是哪次代码变更?改了什么?只有日期,没有上下文。事后想排查“当时为什么挂”,找不到入口。 **第三,回退要重做一遍部署操作。** 出问题后,要手动把备份目录重命名回去,再手动重启服务。回退本身就是另一轮容易出错的手动操作——一次部署出错的概率不高,但**部署 + 回退都要靠人不出错,概率就高了**。 这三个断点叠在一起,手动备份有点像**错题本**:抄了但没看,看了又找不到,找到也用不上——形式上在做版本管理,实际上只是一套越来越繁琐的运维琐事。 更隐蔽的是**版本不一致**。本地构建的 `dist` 是基于当前代码生成的,但服务器上跑的 Nginx 配置、环境变量、依赖版本可能是上次部署时遗留的。你更新了 `dist`,却忘了同步 Nginx 配置;或者反过来,改了 Nginx 配置,但 `dist` 还是旧的。新旧版本混在一起,踩坑是常态——页面白屏、接口 404、样式错乱,排查半天才发现是前后端版本对不上。 **手动备份解决不了 {{term:版本管理}}。** 它只是把“覆盖”变成了“覆盖前先复制一份”,但“历史可追溯”、“变更可对比”、“回退可一键”这些能力,手动方式都给不了。这些不是备份的问题,是版本管理的问题——04 篇会看到,Git 正是为版本管理而设计的工具:每一次 `git commit` 都是带时间戳和说明的变更快照(解决“历史可追溯”),`git diff` 能精确比对任意两次提交之间的差异(解决“变更可对比”),`git revert` 一条命令就能回到任意历史版本(解决“回退可一键”)。 ## 小结 生产环境的两个最低必要条件是 7×24 和公网 IP,云服务器是最短路径。通过 SSH 连接服务器 + scp 上传文件,就能完成一次最朴素的手动部署。 本篇把发布流程形式化为一组有序操作步骤——本例即"构建 → 上传 → 验证"的线性 DAG。手动部署能跑通,但每一步仍靠人执行。下一步进入服务端应用部署:当部署对象从静态文件升级成需要运行时的后端进程时,构建策略、进程生命周期与进程保活都会变成新问题。 ## 思考 1. 如果你的服务器宕机了,用户访问会发生什么?生产环境的“7×24”是由谁保证的? 2. `scp` 和直接在服务器上改文件有什么区别?为什么我们选择在本地构建再上传? 3. 你在 `scp` 时输错了目标路径,覆盖了服务器上的其他文件,怎么办? ## 延伸阅读:终端怎么选 终端 App 和 Shell 插件是两层叠加:**App 负责长相,插件负责手感**。选型优先 **稳定、延迟低、习惯顺手**——花哨功能其次。 ### 终端 App | 平台 | 默认 / 推荐 | 一句话特点 | | --- | --- | --- | | Windows | **PowerShell**(系统自带) | 原生上手;命令习惯偏 Windows | | Windows | **Windows Terminal + WSL Ubuntu** | 学 Linux 运维的默认组合 | | macOS | **Terminal.app**(系统自带) | 够用,配 zsh 已现代化 | | macOS | [iTerm2](https://iterm2.com/) | 分屏、搜索、主题成熟 | | macOS | [Warp](https://www.warp.dev/) | AI 终端:补全、解释、工作流内置 | > **关于 Windows Terminal 与 WSL Ubuntu:** Windows 11 / 现代 WSL 安装时会自动把 WSL 注册为 Windows Terminal 的 profile,**两者通常无需分开显式安装**。如果你看到的是老版 Console Host 窗口,可在 Microsoft Store 单独装 Windows Terminal 后设为默认。Mac 默认 Terminal 与云服务器命令习惯完全兼容(都是 Unix 一脉),换 Warp / iTerm2 是体验升级,不是必需。 ### Shell 插件(zsh,Mac / WSL Ubuntu / Linux 都适用) | 插件 | 作用 | | --- | --- | | [oh-my-zsh](https://ohmyz.sh/) | zsh 配置框架,主题 + 插件管理 | | [git](https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/git) | 大量 `gst` / `gco` / `gp` 别名 + 提示 | | [zsh-syntax-highlighting](https://github.com/zsh-users/zsh-syntax-highlighting) | 合法命令变绿、非法变红——输错当下就知道 | | [zsh-autosuggestions](https://github.com/zsh-users/zsh-autosuggestions) | 按历史灰字提示,→ 一键采纳 | | [zsh-completions](https://github.com/zsh-users/zsh-completions) | 补全 `kubectl` / `docker` / `aws` 等额外命令 | **最小可用组合**:oh-my-zsh + git + zsh-syntax-highlighting + zsh-autosuggestions。Windows 原生 PowerShell 走自己体系(`PSReadLine`、`posh-git`),不通用。 ## 参考 1. [SSH 官方文档](https://www.openssh.com/manual.html) 2. [scp 命令使用指南](https://linux.die.net/man/1/scp) 3. [腾讯云轻量应用服务器](https://cloud.tencent.com/product/lighthouse) 4. [阿里云轻量应用服务器](https://www.aliyun.com/product/swas)