编程导航GitHub话题讨论

GitHub

970 参与
分享

快来分享你的内容吧~

点击登录,快来和大家讨论吧~
表情
图片
话题
打卡
综合
交流
文章
问答

开源我的 Vibe Coding 工作流,已有人靠它把项目做完了

## 前言 > 如果这篇文章对你有帮助,欢迎先去给仓库点个 Star:[**project-vibe-spec**](https://github.com/dnwwdwd/project-vibe-spec),这对我是很大的鼓励,也让更多人能找到这个工具。 大家好,我是汉堡。 上一篇文章《如何从0到1 Vibe Coding 一个项目,并长期维护》里,我分享了自己踩坑之后沉淀出来的一套 Harness 体系——用文档治理、AGENTS.md、范围冻结和分阶段推进来驯服 Vibe Coding 的混乱。 文章发出去之后,有鱼友来问我:**多个 AI Agent 接力做项目,怎么让它们互相"知道"彼此做了什么?** 答案就在那篇文章里。**多 Agent 之间通信和协作,唯一的方式只有文档。** 在项目根目录维护好 Agent 的"说明书"——Codex/OpenCode 对应 `AGENTS.md`,Claude Code 对应 `CLAUDE.md`——Agent 启动时自动注入,啥也不用说就知道项目的一切。 有人照着做了,昨天来告诉我:**"牛逼,用了文章里的内容之后,AI 的产出就稳多了,现在已经把项目做完了,感谢大佬。"** 这让我很开心。所以今天这篇文章,我想介绍一个更进一步的东西——我把那套方法论直接做成了一个可以复用的 **Agent Skill**。 --- ## 为什么要做成 Skill? 上篇文章写的是**思路和方法**,但每次新建项目,你还是得自己手写 AGENTS.md、搭 docs/ 目录结构、想文档命名规范…… 重复劳动,而且容易遗漏。 所以我把这套体系沉淀成了一个开箱即用的 GitHub 仓库: > **👉 [https://github.com/dnwwdwd/project-vibe-spec](https://github.com/dnwwdwd/project-vibe-spec)** 如果这个 Skill 对你有帮助,欢迎点个 Star,这对我是很大的鼓励。 --- ## 这个 Skill 解决什么问题? 回顾一下 Vibe Coding 的几个典型困境: - **上下文膨胀**:代码越多,AI 越难理解全貌 - **耦合蔓延**:改一处牵一发而动全身 - **意图退化**:没有文档,几轮对话后你自己都忘了当初为什么这么设计 - **多 Agent 失忆**:换一个 Agent 工具,之前的上下文全部归零 这些问题都可以追溯到同一个原因——缺乏工程化的文档治理。 `project-vibe-spec` 提供了一套完整的项目规范模板,让你在开始写第一行代码之前,就把"地基"打好。 --- ## Skill 里有什么? ### 1. AGENTS.md 模板 这是整个体系的核心。AGENTS.md 干的事情只有一件:**让 AI 知道你的编码哲学和项目规范,不用每次都重复交代。** 对于 Codex/OpenCode,启动时会自动将项目级别和全局的 AGENTS.md 注入当前对话上下文。你啥也不用说,Agent 就知道: - 项目的技术栈和架构 - 代码风格和命名规范 - 禁止的行为(比如不要擅自改架构、不要顺手加功能) - 文档优先级和冲突解决规则 - 完成标准(DoD) ### 2. 文档治理体系 一套完整的文档分类规范: | 文档类型 | 命名格式 | 用途 | | --- | --- | --- | | **REQ** 需求文档 | `REQ-YYYYMMDD-XX-*.md` | 新功能或大范围改造前必写 | | **PROG** 进度日志 | `PROG-YYYYMMDD.md` | 每天一日志,记录完成了什么 | | **BUG** 缺陷记录 | `BUG-YYYYMMDD-XX-*.md` | 发现 bug 立即记录 | | **BIZ** 业务决策 | `BIZ-YYYYMMDD-XX-*.md` | 业务流程或实现策略的确认 | | **DEV** 技术方案 | `DEV-YYYYMMDD-XX-*.md` | 复杂模块拆解、阶段实施方案 | 这套体系的价值: - **上下文外挂**:AI 每次对话前先读相关文档,不会丢失上下文 - **可追溯**:三个月后回来,还能知道当初为什么这么设计 - **可交接**:换一个 AI 模型或工具,读一遍文档就能接手 ### 3. 分阶段推进模板(Phase 0 → Phase N) 大项目一口气让 AI 实现 = 灾难。必须拆阶段,每个阶段有明确的 DoD(Definition of Done): | 阶段 | 内容 | DoD | | --- | --- | --- | | **Phase 0** | 文档体系初始化 | AGENTS.md、README.md、docs/ 结构就绪 | | **Phase 1** | 后端骨架 | 服务可启动、配置可读、数据库可初始化 | | **Phase 2\~3** | 核心链路 | 端到端链路跑通 | | **Phase 4** | 业务 API | 接口字段对齐、错误响应统一 | | **Phase 5** | 前端工程化 | 拆页拆组件、接入真实 API | | **Phase 6\~7** | 收尾上线 | 链路闭环、打包部署 | 每个 Phase 结束必须达到 DoD 才能进入下一阶段。这个纪律不能破。 ### 4. 范围冻结清单 v1 要做什么、不做什么,在一开始就写死。一旦范围冻结,后续开发中 AI 想"顺手"加功能时,你就可以说:**"不在 v1 范围,先记 REQ,下个版本再说。"** --- ## 怎么用? 直接 clone 或 fork 这个仓库,把模板文件复制到你的项目根目录,按照说明填写你的项目信息即可。 ```bash git clone https://github.com/dnwwdwd/project-vibe-spec ``` 然后把 `AGENTS.md`、`docs/` 目录结构复制到你的项目里,根据你的项目实际情况填写内容。 --- ## 真实反馈 这套方法论有人真的用了。 有读者看了上篇文章之后,把这套文档治理的思路用到了自己的项目上。几天后来反馈:**AI 的产出稳定了很多,项目已经做完了。** ![读者提问:如何协调多个编程Agent接力任务](https://hejiajun-img-bucket.oss-cn-wuhan-lr.aliyuncs.com/notus/images/2026/07/90207c130230a885ad6bbc0b09b73500d150f259b977fe8d303af485d9bc4580.png)![读者反馈:用了文章内容后项目已做完](https://hejiajun-img-bucket.oss-cn-wuhan-lr.aliyuncs.com/notus/images/2026/07/d1c86d4e8fa3f3f00f64e3a90e8a249081734453c0c3af1e10d29be8c5ddc84b.png)我写这篇文章、做这个 Skill,就是想把这套工程化方法变成别人可以直接用的东西,不用每个人再从头踩一遍。 --- ## 最后 Vibe Coding 的问题不在 AI 的能力,在我们给 AI 的上下文质量。 一个没有文档、没有规范、没有阶段划分的项目,再强的模型也推不动。换上完整的 Harness 体系——文档治理、阶段划分、范围冻结——用中等模型也能稳定推进。 `project-vibe-spec` 就是帮你把这个"地基"快速搭起来的工具。 仓库地址:<https://github.com/dnwwdwd/project-vibe-spec> 如果觉得有用,可以点个 Star,或者在评论区聊聊你的使用体验。 --- ## 相关文章 - [如何从0到1 Vibe Coding 一个项目,并长期维护](https://blog.hejiajun.com) --- *我的博客:[https://blog.hejiajun.com](https://blog.hejiajun.com)*

RKit:我常用的 uTools 工具的“轻量替代”

我以前一直用 `uTools`。 说实话,它在我这儿属于那种“装机必备”级别的工具:搜东西、翻译、截图、OCR、剪贴板……一堆日常零碎事,按个热键就能搞定。 但后来 uTools 越来越臃肿,也开始限制插件数量,这我还能忍,毕竟我平时用的插件也不多,最让我绷不住的是:**开始强制登录**了。 我不是说登录就一定不好,我只是很不喜欢“一个本来用来提升效率的小工具”,慢慢变成“需要账号体系才能用”的东西 于是我就去找“uTools 平替”。 我试了 `zTools`,确实和utools差不多,但用了一段时间总觉得有些地方不太对:要么是某个流程不顺手,要么是细节不符合我的习惯。也不是不能用,就是用的时候会忍不住嘀咕一句:“要是这里能这样就好了……” 结果我一想:我每天高频用的功能就那几个,**干脆我自己做一个算了**。 于是就有了 `RKit`。 --- ## RKit 是个啥?一句话 `RKit` 就是一个 **macOS 上的命令面板**(后面也会做 windows),有点像 Spotlight: 按热键 → 弹出一个小面板 → 执行动作。 我不想做插件市场,也不想做一堆花里胡哨的功能。 我就想把我每天用的那几个能力做得**顺手、够快、够稳定**。 --- ## 它能干啥?就我常用的这几个 我现在最常用的是这些: - `截图`:区域截图 → 自动复制到剪贴板 → 顺手还能进内置编辑器改两笔 - `OCR`:对最近一次截图做文字识别(macOS 自带 `Vision`) - `翻译`:默认 Google GTX(不用 key),也可以配 Deeplx(自己搭个接口那种) - `剪贴板历史`:文本 + 图片,支持置顶/搜索,还能一键暂停采集 10 分钟 - `设置`:语言、热键录制、开机自启动、清理历史这些 你会发现,它就是“uTools 里我真正每天在用的那几个东西”。 ![file-20260717154756729.png](https://pic.code-nav.cn/post_picture/1827554952380329985/XclkGhO6EXVuAW3N.webp) --- ## 我做它最在意的点 ### 1)快:要像 Spotlight 那样“按下就出来” 默认热键是: - `Option + Space`:呼出/关闭 - `Esc`:关闭 我希望它是那种你不需要思考的动作: 手指一按,它就出现;你输入,回车,事情结束。 ### 2)别打扰:别把我从当前桌面/当前软件拽走 有些工具的面板会乱跳桌面,或者截图完又把焦点抢回去,这种我很难忍。 RKit 的目标是:**你在哪儿用,它就在哪儿出现**,尽量别干扰你的主工作流。 ### 3)本地优先:默认不联网 我个人比较敏感的一点是: 这种工具一旦开始“强制登录”,我就会下意识担心:我输入的东西、剪贴板、截图,会不会被上传、被统计、被分析? RKit 的原则很简单: - 默认本地优先 - 只有“翻译”可能要联网(你选的翻译服务决定) --- ## 怎么装?(现在是未签名 ZIP) RKit 目前走的是 **未签名 ZIP** 发布(主打一个快,先让大家用起来)。 ### 安装步骤 1. 从 GitHub Releases 下载 `RKit.app.zip` 2. 解压得到 `RKit.app` 3. 把 `RKit.app` 拖到 `/Applications` 4. 打开运行 ### 如果被 Gatekeeper 拦了(无法打开 / 提示“已损坏”) 先确认你已经把 `RKit.app` 拖到了 `/Applications`,再执行: ```bash xattr -dr com.apple.quarantine /Applications/RKit.app ``` 然后 Finder 里右键 `RKit.app` → `打开`。 --- ## 权限这块:截图一定会要“屏幕录制” 截图功能需要 macOS 的“屏幕录制”权限: `系统设置 → 隐私与安全性 → 屏幕录制 → 勾选 RKit` 这块没啥好绕的,系统规则就是这样。 我能做的就是把引导写清楚、交互做顺,不搞那些“偷偷申请一堆你用不到的权限”。 --- ## 后续计划 我不会把 RKit 做成“全能工具”,我更想把它做成一种**很顺手的日常习惯**: 有什么我高频使用的功能,我会添加进去 也会尽快开发 windows 版本 --- ## 致谢 - Deeplx(DeepLX):<https://github.com/OwO-Network/DLX> 感谢 DeepLX 开源项目:它使得在自建环境中通过本地 API 方式使用 DeepL 的免费网页翻译成为可能。 我就是使用本地部署的地址: ![image.png](https://pic.code-nav.cn/post_picture/1827554952380329985/LQSjFLAOUaU5s14B.png) --- ## 最后 做 RKit 的起点其实很简单: 我只是想要一个“不臃肿、不强制登录、只做我常用功能”的工具。 如果你也跟我一样日常使用这几个工具,欢迎来试试看。 如果遇到什么问题,欢迎随时指出。 如果你觉得项目对你有帮助,欢迎点个Star,感谢!! 项目地址:[https://github.com/Han-GR/rkit](https://github.com/Han-GR/rkit) 下载地址:[https://github.com/Han-GR/rkit/releases/download/v1.0.1/RKit.zip](https://github.com/Han-GR/rkit/releases/download/v1.0.1/RKit.zip)

开源一个能自动发现和分析 GitHub 仓库的 AI Skill 与全栈应用

大家好,我是汉堡🍔。 今天来聊一个很有意思的话题——**同一个功能,用两种完全不同的方式实现,会是什么体验?** 事情的起因是这样的:我在懒猫搬砖做副业的时候,经常需要去 GitHub 上找适合移植的开源项目。手动翻实在太累了,于是我就想——能不能写个工具自动帮我爬、帮我判断? 说干就干。但有趣的是,我前后做了**两个版本**:一个叫 **GitHub Repo Crawler**,是个 AI Agent Skill;另一个叫 **RepoRadar**,是个完整的全栈应用。 功能核心是一样的——爬 GitHub 仓库、分析能不能用、推送到飞书。但形态天差地别。下面分别聊聊它们。 --- ## 博客文章链接 https://blog.hejiajun.com/archives/kai-yuan-yi-ge-neng-zi-dong-fa-xian-he-fen-xi-github-cang-ku-de-ai-skill-yu-quan-zhan-ying-yong ## GitHub Repo Crawler:把能力装进 AI Agent 的口袋 **仓库地址**:<https://github.com/dnwwdwd/github-repo-crawler> ### 它是什么? 简单说,它是一个 **AI Agent Skill**——给 Claude Code、opencode、Codex 这类 AI 编程助手用的「插件」。 如果你用过 Claude Code 的 Skill 机制,就很好理解:它就是一个 `SKILL.md` 文件,告诉 AI「你现在有了爬取 GitHub 仓库的能力,遇到相关任务就自动调用」。 ### 怎么用? 不需要部署,不需要启动服务。你只需要把这个 Skill 装到你的 AI Agent 里,然后在对话里直接说: > 「帮我搜一下最近一周 GitHub 上 Star 增长最快的 Python 开源项目」 「看看这个仓库适不适合移植到懒猫微服」 AI 就会自动激活这个 Skill,调用 GitHub API 去搜索、分析,然后把结果吐给你。 ### 适合谁? | 场景 | 说明 | | --- | --- | | 🧑‍💻 **AI 重度用户** | 已经习惯了在 Claude Code / Codex 里干活的人,加个 Skill 就能用 | | ⚡ **临时需求** | 偶尔想搜一下 GitHub 仓库,不想专门跑个应用 | | 🔧 **轻量级** | 不需要持久化数据、不需要后台定时任务,用完即走 | **产物**:一次对话 + 即时结果。 --- ## RepoRadar:一个完整的 GitHub 仓库监测系统 **仓库地址**:<https://github.com/dnwwdwd/repo-radar> ### 它是什么? RepoRadar 是一个**完整的全栈 Web 应用**,不只是「搜一下」,而是「持续监测」。 技术栈一览: | 层级 | 技术 | | --- | --- | | **后端** | Python FastAPI | | **前端** | React + Vite + TypeScript | | **数据库** | SQLite | | **调度** | APScheduler 定时任务 | | **通知** | 飞书 SDK(即时推送 + 每日汇总) | | **AI 分析** | Agent ReAct 模式分析仓库质量 | | **部署** | 单容器 Docker,一键跑起来 | ### 它做了什么? RepoRadar 的完整工作流是这样的: ```plaintext GitHub Search 抓取 → 规则过滤 → 去重入库 → Agent 分析 → 飞书推送 ``` 并且这套流程是**自动定时跑的**,不需要人盯着。 具体来说,v1 版本冻结了以下功能: - ✅ GitHub Search 抓取(按关键词、语言、Star 数等维度搜索) - ✅ 规则过滤 + 去重 + 入库 - ✅ Token 降级与失效自动暂停 - ✅ **Agent 智能分析**:自动判断仓库质量、技术栈、移植难度 - ✅ 优先级队列 + 失败重试 - ✅ 仓库列表 + 筛选 + 详情展开(Web 界面) - ✅ 手动提交单个 GitHub URL(插队分析) - ✅ 系统状态页 + 配置中心热重载 - ✅ **飞书即时推送** + 每日汇总 - ✅ 单容器懒猫微服打包 同时,为了控制范围、避免 Vibe Coding 时「管不住手」,明确不进 v1 的功能有:Star 增速监控、批量提交、导出 CSV、多数据源、语义去重等——这些留给后续版本。 ### 适合谁? | 场景 | 说明 | | --- | --- | | 🏢 **团队使用** | 部署一个实例,全团队都能通过 Web 界面看、飞书收推送 | | 🔄 **持续监测** | 不是搜一次就完了,而是定时跑、自动分析、主动推送 | | 📊 **数据沉淀** | 仓库数据持久化存储,有历史记录、可筛选可回溯 | | 🎯 **副业/业务场景** | 懒猫移植这种有稳定需求的工作流,需要系统化工具 | **产物**:一个跑在服务器上的 Web 应用 + 飞书群里的每日推送。 --- ## 核心对比:Skill vs 全栈应用 两个项目功能一样,形态不同,本质上是**两种解决问题思路**的体现: | 维度 | GitHub Repo Crawler(Skill) | RepoRadar(全栈) | | --- | --- | --- | | **形态** | AI Agent 插件(SKILL.md) | 独立 Web 应用 | | **启动方式** | 对话中自动激活 | Docker 部署 / 手动启动 | | **交互方式** | 自然语言对话 | Web 界面 + 飞书推送 | | **数据存储** | 不持久化(对话即数据) | SQLite 持久化,历史可查 | | **定时任务** | ❌ 无(按需触发) | ✅ APScheduler 定时抓取 | | **多用户** | ❌ 单人使用 | ✅ Web 界面多人可访问 | | **通知推送** | 对话内返回结果 | 飞书即时推送 + 每日汇总 | | **Agent 分析** | AI 本身就是 Agent | 内置 ReAct Agent 自动分析 | | **上手门槛** | 极低(装 Skill 即可) | 中等(需要部署) | | **适合场景** | 个人临时使用、AI 工作流 | 团队持续使用、业务系统 | 一句话总结: > **Skill 版是「随叫随到的超级实习生」——你问它就干。RepoRadar 是「专职的后台管家」——部署好之后自己定时跑,干完了主动通知你。** --- ## 为什么做两个版本? 其实这背后有一个挺有意思的思考。 RepoRadar 是我用 **Harness 体系**(文档驱动 + 范围冻结 + 分阶段开发)认真做的全栈项目,前后分了 8 个 Phase,写了完整的 PRD、AGENTS.md、技术文档。它是一个**工程化的产物**。 但后来我发现:很多时候我只是想快速搜一下 GitHub 仓库,不想打开浏览器、不想登录系统。这时候如果能在 Claude Code 的对话里直接问一句就好了。 于是就有了 GitHub Repo Crawler——把核心能力抽出来,做成 AI Agent 的 Skill。**不需要部署、不需要界面、不需要数据库**,对话就是一切。 这两个版本不是替代关系,而是**互补关系**: - 日常快速搜索 → 用 Skill - 团队持续监测 → 用 RepoRadar --- ## 写在最后 同一个功能,两种形态,背后是两种截然不同的设计哲学: - **Skill 的思路**:把工具嵌入 AI 的工作流,让 AI 来调用,人只需要对话。 - **RepoRadar 的思路**:把工具做成独立系统,定时自动运转,人只需要看结果。 没有谁好谁坏,只有适不适合。如果你也是 AI 重度用户,强烈建议试试把常用功能做成 Skill——那种「在对话里随口一问就有答案」的体验,真的回不去了。 两个项目都是开源的,欢迎 Star 和 PR 👇 - 🔧 GitHub Repo Crawler:<https://github.com/dnwwdwd/github-repo-crawler> - 📡 RepoRadar:<https://github.com/dnwwdwd/repo-radar> --- *本文由汉堡🍔原创,首发于个人博客 [blog.hejiajun.com](https://blog.hejiajun.com/)*

GitHub 每周精选|2026 W28

GitHub 上每天都会冒出很多新项目。 大多数我都会看过就忘。 有些看起来很酷,但装完就吃灰。 还有一些,会让我真的想留下来继续折腾。 我想把这些项目记录下来。 不追求“最火”。 只记录那些: 让我真正想装下来试试的东西。 **ps:最近半个月在期末周和实习中,现在算是稳定下来,可以正常恢复更新了** --- ### 1. caveman 项目名:caveman GitHub 仓库地址: https://github.com/JuliusBrussee/caveman 你在使用 AI 的时候有没有这种感觉,就是他们有时候太爱铺垫了,明明一句 "这里单位错了" 就能说清,它非要绕一圈 "根据您的需求,我建议……" 如果是直接订阅制的 GPT 这种还好,但如果是用的 API 的方式,那 AI 输出的那些可有可无的话,就真的是在烧钱了,毕竟 token 是按照输入和输出计的嘛,如果能够让 AI 在输出端减少那些可有可无的话,也算是在省钱了 这个仓库做的就是这件事情:**让 AI coding agent 少说废话** 这个仓库的 star 数量截止到目前为止有 **88.3k**,可见大家是有多么需要这个功能 严格说,它不是两个 skill,而是同一个 `caveman` skill 里的两个常用档位:`lite` 和 `full` `lite` 比较像“正常人少废话版”,句子还完整 `full` 更短,允许片段句,更像只给你结论。不用它时,回答通常最完整,但也最容易啰嗦 我也做了个很小的对比实验:同一道 JWT 过期判断题,分别让 normal、`caveman lite`、`caveman full` 回答。三组都准确指出核心问题:JWT `exp` 是秒,`Date.now()` 是毫秒,也都给出了正确修复。区别主要在表达长度:normal 最完整,lite 最均衡,full 最短但没丢关键点 **所以我会更推荐从 `lite` 开始日常用;上下文你已经很熟、只想快速看结论时,再切 `full`** --- ### 2. 办公四件套 项目名:办公四件套 GitHub 仓库地址: https://github.com/anthropics/skills/tree/main/skills 对的,经典的办公四件套,也就是 docx、xlsx、pdf、pptx 我是这周实习时一位老师和我说,ta 在使用 codex 时识别和修改 Word 文档效果不好,才知晓 ta 没有安装办公四件套对应的 skill 的,于是就把这几个 skill 推荐给 ta 简单来说一下这几个 skill 的功能,其实从名字也能看出来了,所以就简单带过一下 有了它,就能让 AI Agent 来**读取和修改**上面提到的办公四件套类型的文件了,如果不装的话,其实也是可以读取的,但是效果是没有安上好的,**建议所有职场办公的小伙伴们安装一下**,star 数量 160k,恐怖如斯 ![image.png](https://pic.code-nav.cn/post_picture/1835174163661012994/rcRkfZeQh6EN8vOO.webp) --- ### 3. CodexBar 项目名:CodexBar GitHub 仓库地址: https://github.com/steipete/CodexBar 你是不是也遇到过这种情况: 正用 AI 写代码,思路刚顺起来,结果突然弹出一句“额度用尽” **如果你只用一个 AI 编程工具,额度用完了就等它恢复,问题不大** 真正麻烦的是同时用好几个工具的人:Codex、Claude、Cursor、Copilot、Gemini,每个工具都有自己的额度、重置时间和使用窗口。信息都能查到,但散在不同地方 **CodexBar 做的就是把这些信息收拢到一起** CodexBar 是一个 macOS 菜单栏工具。装好之后,它会把你常用 AI 编程服务的用量、剩余额度、重置时间放到菜单栏里。你不用每次打开官网,也不用临时跑命令查状态,抬眼看一下菜单栏,大概就知道现在还能用多少 它有点像 AI 编程工具的“油量表” 不过这里有几个点要先说清楚 第一,CodexBar 目前的菜单栏 App 只支持 macOS 14+,**也就是主要面向 Mac 用户**。项目里有 CLI 相关构建,但如果你想要的是菜单栏里的完整体验,那目前就是 Mac。 第二,**它统计的是官方账号、官方 API、官方 CLI、本地配置或浏览器登录态能拿到的数据**。也就是说,如果你是通过普通中转 API、第三方代理站、共享 Key 之类的方式在用 OpenAI 或 Claude,它通常没法帮你统计官方账号里的真实额度和重置窗口。除非那个中转服务本身也提供了 CodexBar 支持的用量接口,否则这里看不到。 第三,它不是让你把账号密码交给它。CodexBar 主要复用你本机已有的登录状态、CLI 凭据、Cookie、OAuth、API Key 或本地文件。不同服务的数据来源不一样,有些需要浏览器 Cookie,有些需要 API Key,有些读本地 CLI 配置。 目前 CodexBar 支持的服务非常多,不只是 Codex、Claude、Cursor 这几个。按项目里的注册列表看,已经覆盖了 58 个 provider: ![image.png](https://pic.code-nav.cn/post_picture/1835174163661012994/9a5epjZERzMOBZUZ.webp) 所以,如果你每天都在用 AI 写代码,经常在 Codex、Claude、Cursor、Gemini、Copilot 之间切换,关心今天还剩多少额度、下次什么时候重置,那 CodexBar 就很值得装一下 ![image.png](https://pic.code-nav.cn/post_picture/1835174163661012994/5MqP57RVWYCQ1azE.webp) --- ### 4. feishu-CLI 项目名:feishu-CLI GitHub 仓库地址: https://github.com/larksuite/cli 如果你所在公司的主要内容都在飞书当中,那么 feishu-cli 一定会给你惊喜的,通过 codex、claude code 就能操作飞书当中的各种内容 如果你目前还没有上班或者公司的主平台不在飞书,但要做数据搜集之类的事情,比如我自己的 GitHub-Star-Top 所抓取的数据,所抓取的爆款封面的数据,以及现在实习期间搭建的内容都是放在飞书多维表格当中的 配合飞书机器人也能实现各种操作的自动化,比如我是实现了开源仓库初筛和选题池入池的自动化、各个平台数据抓取的自动化等等 所以如果你做数据抓取,那么 feishu-cli 很值得你去试一试,爱飞书~ ![image.png](https://pic.code-nav.cn/post_picture/1835174163661012994/S0N1ocFK4BLqMuRT.webp) --- ### 5. frontend-design & UI UX Pro Max 项目 1:frontend-design 项目 2:UI UX Pro Max 项目 1 仓库地址: https://github.com/anthropics/skills/tree/main/skills/frontend-design 项目 2 仓库地址: https://github.com/nextlevelbuilder/ui-ux-pro-max-skill AI 编程几乎是一定会需要做前端页面的,那么关于 UI 的审美问题,如果在不给 AI 任何提示的情况下,**一般都会做出来经典的蓝紫色配色的页面,丑的一批...** 这两个仓库就是来解决 AI 在做前端页面的审美问题的,那可能有小伙伴有疑问: **这两个仓库都是解决前端的审美问题的,那我应该如何选择呢?** 好问题,在测这两个仓库的时候,我用同一个主题进行了测试了,下面是我的一些个人感受,供大家参考 这轮测试里,主题均是 "睡眠" 的手机 APP 产品介绍页 frontend-design 它**只做了一轮轻量需求澄清,就直接进入执行**,产物更有品牌感和审美记忆点 ui-ux-pro-max 更像设计系统顾问,它会在设计对齐、产品类型、UX 约束和页面结构上花更多时间;如果用户只是一路说“继续”,它可能会产出更稳但不够惊艳的方案(我是一路 "继续吧" 跑下来的),**但我之前也是用 ui-ux-pro-max 做出过很漂亮的页面的,只是在这次的测试当中表现确实不好** ui-ux-pro-max 会倾向于反复给反馈、精修方向、调整风格、配色、字体和交互约束时,能更系统地把 UI 打磨起来 所以我的判断如下: - 快速 Demo:优先 `frontend-design` - 想要第一眼好看、有风格、有惊喜:优先 `frontend-design` - 想做设计系统、UX 规则、跨页面一致性:优先 `ui-ux-pro-max` - 愿意多轮对齐、精雕细琢:`ui-ux-pro-max` 更值得用 - 普通用户不想回答太多问题:`frontend-design` 体验更轻 - 有明确产品方向、愿意参与设计决策:`ui-ux-pro-max` 上限可能更高 用 frontend-design 跑的一个 demo ![image.png](https://pic.code-nav.cn/post_picture/1835174163661012994/exgT4dE92KafCg0G.webp) --- 最后: 后面肯定还会继续遇到: 让我真正想装下来试试的项目。 这个系列也会继续更新下去。

手把手教你:GitHub CI + PyPI 自动发包,从此告别手动 twine upload

> 最近写了个 Python 练手项目,本地跑得挺顺,盘算着发到 PyPI 上,让别人也能一行 `pip install` 就用起来。本来以为 `poetry build` + `twine upload` 就完事了,结果一脚踩进 GitHub Actions 的世界,从 CI 配置到 Trusted Publisher 认证,从 Poetry 依赖解析到版本号踩坑,一路上“惊吓”不断。折腾了一晚上,才把整条链路跑通。谨以此文,纪念熬的又一个夜——不算什么高深教程,但每个坑都是实打实踩过的,希望能帮后来的同学少绕几个弯。 --- ## 先搞清楚几个名词 在开始之前,得先弄明白三个东西,不然 YAML 文件抄都抄不明白。 ### CI 是啥? CI = Continuous Integration,翻译过来就是"持续集成"。听着高大上,其实就是:**你每次提 PR,GitHub 帮你自动跑测试**。 ``` 你 push 代码 → GitHub 开一台虚拟机 → 跑 pytest → 绿了 ✅ 或者 红了 ❌ ``` 说白了就是一个比你更勤快的同事,每次你改了代码都帮你检查一遍。 ### GitHub Actions 又是啥? 就是 GitHub 内置的"自动化引擎"。你在仓库的 `.github/workflows/` 目录下扔一个 YAML 文件,GitHub 就会在云端给你开一台机器,按你说的干活。 ```yaml on: push jobs: say-hello: runs-on: ubuntu-latest steps: - run: echo "hello world" ``` 就这么简单。每次 push 代码,GitHub 就帮你打印一个 hello world。 ### Release 呢? Release 就是 GitHub 上的"版本快照"。你觉得代码写得差不多了,就打个 Release,绑一个 Git tag(比如 `v0.1.0`),写几句变更说明。它本身不干啥,但它是触发 PyPI 自动发包的"开关"。 ### 三者的关系 简单画个流程图: ``` 你提 PR 到 main │ ▼ GitHub Actions 自动跑测试 │ ├── 红了 ❌ → 回去改 bug │ ▼ 绿了 ✅ 合并到 main │ ▼ 在 GitHub 上打个 Release │ ▼ GitHub Actions 自动构建 + 上传 PyPI │ ▼ 别人可以 pip install 你的包了 🎉 ``` --- ## 第一步:把项目结构搞对 项目的目录长这样: ``` my-project/ ├── .github/ │ └── workflows/ │ ├── ci.yml # 自动测试 │ └── release.yml # 自动发包 ├── my_project/ # 你的 Python 代码 │ ├── __init__.py │ └── ... ├── pyproject.toml # 项目的"身份证" └── ... ``` 重点是 `pyproject.toml`,这玩意儿是整个发布流程的核心。我用的是 Poetry,配置长这样: ```toml [project] name = "my-project" version = "0.1.0" description = "一个示例项目" authors = [{name = "Your Name", email = "you@example.com"}] license = {text = "MIT"} readme = "README.md" requires-python = ">=3.12,<4.0" dependencies = [ "requests>=2.31", ] [project.scripts] my-cli = "my_project.cli:main" [tool.poetry] packages = [{include = "my_project"}] [tool.poetry.group.dev.dependencies] pytest = ">=7.0" [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api" ``` 这里面有几个坑,我后面会专门讲。先照着抄,别自己发挥。 --- ## 第二步:搭 CI(自动测试) 创建 `.github/workflows/ci.yml`: ```yaml name: CI on: pull_request: branches: [main] jobs: test: runs-on: ubuntu-latest strategy: matrix: python-version: ["3.12", "3.13"] steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install Poetry run: | pip install poetry poetry config virtualenvs.create false - name: Install dependencies run: poetry install --with dev - name: Run tests run: pytest -v || [ $? -eq 5 ] ``` 几个值得注意的地方: **`matrix`**:同时在 Python 3.12 和 3.13 上跑测试。GitHub 会开两台机器并行,确保两个版本都兼容。 **`virtualenvs.create false`**:GitHub Actions 的 runner 每次都是全新的,不需要再搞个虚拟环境,直接装到系统 Python 就行。 **`pytest -v || [ $? -eq 5 ]`**:pytest 退出码 5 表示"没有收集到任何测试"。项目刚开始没测试文件的时候,CI 不会因为这个红掉。等你写了真正的测试,该红还是会红。 提 PR 到 main 之后,去 Actions tab 就能看到结果了。绿了就 merge,红了就改。 --- ## 第三步:配置 PyPI Trusted Publisher ### 什么是 Trusted Publisher? 以前发 PyPI 要手动生成 API Token,然后存到 GitHub Secrets 里。Trusted Publisher 是 PyPI 推出的新方式:**用 OIDC 认证,不需要管 Token**。 原理说人话就是:GitHub Actions 运行的时候,可以向 GitHub 证明"我确实是这个仓库的这个 Workflow",然后 PyPI 验证这个证明是否和你之前配置的一致。一致就放行。 ### 怎么配? **PyPI 那边**:去 [https://pypi.org/manage/account/publishing/](https://pypi.org/manage/account/publishing/),在「添加新的待定发布者」里填: | 字段 | 填啥 | | ----------------- | --------------------------- | | PyPI project name | 你的包名,比如 `my-project` | | Owner | GitHub 用户名 | | Repository name | GitHub 仓库名 | | Workflow name | `release.yml` | | Environment name | `pypi` | **GitHub 那边**:去仓库 → Settings → Environments → New environment,名字填 `pypi`,直接保存。 两边的名字必须**一模一样**,大小写都不能差。我当时 Environment name 填了 `Pypi`,结果报了个 `invalid-publisher`,排查花费了2.5根头发。 --- ## 第四步:配置自动发包 创建 `.github/workflows/release.yml`: ```yaml name: Release to PyPI on: release: types: [published] jobs: build: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.12" - name: Install Poetry run: pip install poetry - name: Build package run: poetry build - name: Upload build artifacts uses: actions/upload-artifact@v4 with: name: dist path: dist/ publish-pypi: needs: build runs-on: ubuntu-latest environment: pypi permissions: id-token: write steps: - name: Download build artifacts uses: actions/download-artifact@v4 with: name: dist path: dist/ - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 ``` `environment: pypi` 和 `id-token: write` 是 OIDC 认证的关键,少了哪个都发不上去。 流程是这样的:你在 GitHub 上点"Create Release" → Actions 自动构建 `.whl` 和 `.tar.gz` → 用 OIDC 认证上传到 PyPI → 别人就能 `pip install` 了。 --- ## 发版的完整流程 日常开发和发版,就这三步: ```bash # 1. 改版本号(pyproject.toml 里的 version) # version = "0.2.0" # 2. 提交推送 git add pyproject.toml git commit -m "chore: bump version to 0.2.0" git push origin main # 3. 去 GitHub 打 Release(tag 填 v0.2.0,点 Publish) ``` 然后就不用管了。Actions 会自动帮你构建、上传。去 PyPI 搜一下你的包名,新版本就在那了。 版本号推荐用语义化版本(SemVer): ``` v 主版本 . 次版本 . 补丁版本 │ │ │ │ │ └─ 修了个 bug │ └─────────── 加了个新功能 └───────────────────── 改了 API,不兼容旧版 ``` **注意**:PyPI 不让重复上传同一个版本号。你要是忘了改 version 就打 Release,Actions 会报 `400 Bad Request`。别问我怎么知道的。 --- ## 踩坑实录(血泪教训) ### 坑 1:`Group(s) not found: dev` CI 跑到 `poetry install --with dev` 就挂了,报错说找不到 dev 组。 原因是 dev 依赖写错了位置。Poetry 只认 `[tool.poetry.group.dev.dependencies]`,不认 `[project.optional-dependencies]`。 ```toml # ❌ 这样写 Poetry 不认 [project.optional-dependencies] dev = ["pytest>=7.0"] # ✅ 要这样写 [tool.poetry.group.dev.dependencies] pytest = ">=7.0" ``` 这俩长得差不多,但 Poetry 就是不认前者。属于"看起来对但就是不行"的那种坑。 ### 坑 2:`No file/folder found for package` 把 PyPI 包名从 `mochi-agent` 改成了 `mochi-assistant`,结果构建时报错说找不到包。 原因是 Poetry 默认按包名找目录。包名 `mochi-assistant`,它就找 `mochi_assistant/` 目录。但实际目录叫 `mochi_agent/`。 解决办法:要么改目录名(我选了这个),要么在 `pyproject.toml` 里显式指定: ```toml [tool.poetry] packages = [{include = "mochi_agent"}] ``` ### 坑 3:Poetry lock 报 Python 版本不兼容 `requires-python = ">=3.12"` 写得挺好,结果 `poetry lock` 报了一堆版本冲突。 原因:没写上界,Poetry 认为你的包支持 Python 4.0+。但 `langchain-core` 这些库声明了 `python < 4.0`,Poetry 发现"你的范围比它的大",就觉得不兼容。 加个上界就好了: ```toml # ❌ 没上界 requires-python = ">=3.12" # ✅ 加上界 requires-python = ">=3.12,<4.0" ``` ### 坑 4:Trusted Publisher 认证失败 报错 `invalid-publisher: valid token, but no corresponding publisher`。 意思是:GitHub Actions 确实拿到了一个 OIDC token,但 PyPI 那边找不到和它匹配的配置。 排查方法:看 Actions 日志里的 claims,逐项和 PyPI 上的配置对比: ``` sub: repo:Owner/Repo:environment:pypi ← Environment 要对 repository: Owner/Repo ← 仓库名要对 workflow_ref: .../release.yml@refs/tags/v0.1.0 ← Workflow 名要对 environment: pypi ← 大小写要对 ``` 我当时的问题是 GitHub 上没创建 Environment。光在 PyPI 配了 Trusted Publisher,GitHub 那边也要建一个同名的 Environment 才行。 ### 坑 5:pip install 时疯狂下载历史版本 装我的包时,pip 把 `langgraph` 从 1.2.7 一路下载到 0.6.x,装了十几分钟。 原因是 `pyproject.toml` 里写了 `langgraph>=0.1.0`,pip 的依赖解析器从最新版开始试,发现和已安装的 `langchain` 版本不兼容,就一个一个往回试。 解决办法:收紧依赖下限。当前用的是 1.2.7,就写 `>=1.2.0`,别写 `>=0.1.0`。 --- ## 不用 Trusted Publisher 的话 如果你不想配 Trusted Publisher(或者要发到 TestPyPI),可以用 API Token: 1. 去 [https://pypi.org/manage/account/token/](https://pypi.org/manage/account/token/) 创建 token 2. 去 GitHub 仓库 → Settings → Secrets → Actions → New secret - Name: `PYPI_API_TOKEN` - Value: `pypi-` 开头的那串 3. `release.yml` 的 publish 步骤改成: ```yaml - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: password: ${{ secrets.PYPI_API_TOKEN }} ``` 不过还是推荐 Trusted Publisher,不用管 token 过期的问题,配一次就行。 --- ## 最后 整套流程搞下来,发现其实不复杂,就是 YAML 文件 + PyPI 配置 + GitHub Environment 三件套。难的是第一次配,各种小坑会把你绊住。 配好之后就很舒服了:写代码 → 提 PR → CI 自动测 → merge → 打 Release → 自动发包。全程不用碰 `twine`,也不用记密码。 希望这篇文章能帮你少踩几个坑。祝发包顺利 🚀

GitHub 每周精选|2026 W26

GitHub 上每天都会冒出很多新项目。 大多数我都会看过就忘。 有些看起来很酷,但装完就吃灰。 还有一些,会让我真的想留下来继续折腾。 我想把这些项目记录下来。 不追求“最火”。 只记录那些: 让我真正想装下来试试的东西。 --- ### 1. loop-engineering-orange-book 项目名:loop-engineering-orange-book GitHub 仓库地址: https://github.com/alchaincyf/loop-engineering-orange-book 继 harness-engineering 之后,最近 AI 圈的一个新概念——loop-engineering,至此 AI 的发展已经经历了这几个阶段了 - 提示词工程 - 上下文工程 - 缰绳工程 - 循环工程 如果你已经在用 Claude Code、Codex 或 Cursor,还处于前面的三个阶段,那么这本由花叔整理的《Loop Engineering》橙皮书值得快速扫一遍 这本橙皮书介绍了一套能落到工程里的方法: **怎么让系统自己去发现任务、把任务交给 agent、用独立的评判机制去验证结果、把状态持久化到磁盘上,再通过调度让这一整套流程持续跑下去** 读完会更清楚一件事,真正的分水岭不在于你把 prompt 写得多漂亮,而在于你有没有能力把自己从执行环里挪出来,去设计一个会自动运转、也知 道什么时候该停下的 loop ![image.png](https://pic.code-nav.cn/post_picture/1835174163661012994/ksZIbqjINkQiEbUz.webp) --- ### 2. system-prompts-and-mo 项目名:system-prompts-and-mo GitHub 仓库地址: https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools 你有没有好奇过,Cursor、Claude Code 这些火爆的 AI 编程工具,到底是怎么一步一步把模型“调”成现在这个样子的? 这个仓库不只是把一堆 system prompt 摆出来,而是顺着这些线索,把一款 AI 工具背后的控制面拆给你看 它收录的内容,**表面上看像是提示词合集,实际上更像一个 AI 工具行为档案库**:你能看到 system prompt,也能看到 agent prompt、工具定义、行为规则,甚至一些模型和配置线索 换句话说,它不是只告诉你“AI 说了什么”,而是让你看到“AI 是怎么被约束、被引导、被编排出来的” 这也是它和普通提示词仓库最大的区别。普通 prompt 库更像素材库,适合直接拿来抄作业;而这个仓库更像拆机图,适合你理解一款 AI 工具为什么会这样工作 对于想做 AI 应用、研究 agent 设计,或者单纯想看懂这些工具底层逻辑的人来说,它的价值都不只是“有一段咒语”,而是能顺手把产品设计思路也一起看明白 如果说普通用户最先看到的是提示词,那真正拉开差距的,是它把提示词放回了一个更完整的结构里去看 **你会开始意识到,一款 AI 工具并不是靠一段 prompt 就变聪明的,而是 prompt、工具调用、上下文注入、运行约束这些层一起起作用**。这个仓库恰好把这些层的痕迹都摆在了台面上 ![image.png](https://pic.code-nav.cn/post_picture/1835174163661012994/EWg31zjGSscSxRef.webp) --- ### 3. SkillSpector 项目名:SkillSpector GitHub 仓库地址: https://github.com/NVIDIA/SkillSpector skill,相信大家对于这个词已经不陌生了,但你有没有想过你安装的 skill 是否真的安全呢? 当然,相信大家安装的多数 skill 是经过多数人验证的没有问题的,但总是会有一些比较小众的 skill,这时做一个 "安检" 还是挺有必要的 SkillSpector 这个仓库做的就是这件事情:在你把 skill 装进去之前,先帮你做一次“安全体检” 它会扫描GitHub 仓库、压缩包,甚至单个文件,去找提示词注入、数据泄露、危险脚本这类问题 对经常用 Claude Code、Codex CLI 这类工具的人来说,这一步很实用,因为 skill 看起来只是一个小插件,实际却可能偷偷读取环境变量,或者把数据发到外部地址 ![image.png](https://pic.code-nav.cn/post_picture/1835174163661012994/uzd3Oyz4Yno9MY3H.webp) --- ### 4. opencv 项目名:opencv GitHub 仓库地址: https://github.com/opencv/opencv 如果你想找一个仓库,快速把 “视觉能力” 从想法变成能跑的东西,OpenCV 会是一个比较好的选择 我先做的是最基础的图片版 demo:二维码识别和人脸检测。二维码这部分很顺,生成测试图之后,能直接识别出内容,也能把框的位置给出来 人脸检测也比较稳,摄像头打开之后,脸能被实时框出来,分数也会跟着显示。至少在“把一条视觉链路跑通”这件事上,OpenCV 还是非常扎实的 后面我又继续往前走了一步,没有停在“画个框就算识别”的层面,而是把它扩成了一个简单的人脸验证 demo:先用一张照片录入 reference,再打开摄像头实时做 1:1 比对 这个测试很重要,因为它比“能不能检测到脸”更接近真实需求。实际跑下来,它确实能工作,也确实能给出相似度分数。但也正因为亲测了一遍,反而更能看清它的边界 比如正常情况下,匹配分数大概在百分之六十多,这说明它已经不是玩具级别的“瞎跑”,但也远远没有到可以直接拿去做严肃身份认证的程度 它非常适合做视觉项目的底座,尤其适合原型验证、工程打样和本地 demo。你要做二维码识别、图像处理、摄像头接入、人脸检测、ONNX 模型推理、基础的人脸验证链路,这套东西都能很快搭起来 ![image.png](https://pic.code-nav.cn/post_picture/1835174163661012994/TmndTPPrcQDWhWNr.webp) ### 5. turbovec 项目名:turbovec GitHub 仓库地址: https://github.com/RyanCodrai/turbovec turbovec 是一个面向 RAG 检索层的本地向量索引工具,核心价值在于把知识库召回这一步做得更轻、更省内存 它用 TurboQuant 做向量压缩,在我的测试里,同样一批文档,索引体积能明显缩小;在带权限、租户或条件过滤的检索场景里,也能把候选范围先收紧再做向量搜索,避免“先全库搜一遍,再把不该看的结果丢掉”这种低效流程 对正在搭建知识库问答、语义搜索、企业内部 RAG 的开发者来说,它更像是一个能直接降低部署成本、提升检索稳定性的底层组件 ![image.png](https://pic.code-nav.cn/post_picture/1835174163661012994/2iHkezhgWEwk9AAi.webp) --- 最后: 后面肯定还会继续遇到: 让我真正想装下来试试的项目。 这个系列也会继续更新下去。

GitHub 每周精选|2026 W25

GitHub 上每天都会冒出很多新项目。 大多数我都会看过就忘。 有些看起来很酷,但装完就吃灰。 还有一些,会让我真的想留下来继续折腾。 我想把这些项目记录下来。 不追求“最火”。 只记录那些: 让我真正想装下来试试的东西。 --- ### 1. 人味 skill 项目名:renwei-writing GitHub 仓库地址: https://github.com/orange2ai/renwei-writing **这是继 web-access 之后,我基本上天天会使用的 skill,目前还不到 1k 的 star,暂时算是不温不火的状态** 从这个仓库名称也基本可以猜到这个仓库的作用到底是什么了,没错就是输出的时候更有人味 这里我没有单独去做一个用和不用这个 skill 输出的文本的实验了,因为自从用了这个 skill 之后,就基本没有在创作的时候不用这个 skill 了 如果你是一名创作者的话,这个 skill 大抵会让你爱不释手的 **虽然这个 skill 的效果确实很不错,但对于输出的内容并不能做到真正的全部使用,还是需要人的创作指导的,要不人味依然不会很高** ### 2. andrej-karpathy-skills 项目名:andrej-karpathy-skills GitHub 仓库地址: https://github.com/multica-ai/andrej-karpathy-skills Andrej Karpathy 在 26 年的 1 月发了一条推特,来聊了聊过去几周大量使用Claude编程的一些零散想法,**有 770 万次浏览量**,推特链接如下: https://x.com/karpathy/status/2015883857489522876 这里也简单介绍一下 Andrej Karpathy 的背景 Andrej Karpathy 是 AI 研究者与工程实践者,**OpenAI 创始团队成员之一**,后担任 Tesla Autopilot 视觉方向负责人 --- 在这条推特当中,Andrej Karpathy 聊到了现在 AI 智能体在编码时的一些问题,这里我还是觉得直接放原文会好一些,相信这也是大家在用 AI 智能体编码时多多少少会遇到的一个问题 ![image.png](https://pic.code-nav.cn/post_picture/1835174163661012994/L7zLYkf8fIpYJxPS.webp) 为了解决上面的问题,就有这个仓库,ndrej-karpathy-skills 做的事情,就是把以上问题压缩成 4 条规则(**以下不是完整的 skill 内容**): 1. 编码前先思考:不要默默假设,有歧义就说出来 2. 简洁优先:能 50 行解决,就不要写成 200 行 3. 精准修改:只动和当前任务有关的地方,不顺手重构 4. 目标驱动执行:不要只说“修一下”,而是定义可验证的成功标准 优先是很轻,可以直接将这个 skill 安装到 AI Agent 当中,或者直接写进 CLAUDE.md 或者 AGENTS.md 都是可以的,可以一定程度上解决上面的问题 **缺点也是比较明显的,它不是强约束**,它不会像类型系统、测试、lint 那样硬性拦住错误,能减少犯错的概率,但并不能杜绝错误的发生 比如关于 "编码前先思考" 这点,用 superpower 的效果会更好 --- 至于使用人群的话,如果你打算用 AI 认真写代码,那么值得一试,它做了一件很朴素的事情:**AI coding 的下一步,不只是让模型更会写代码,也要让模型少乱写代码** ![image.png](https://pic.code-nav.cn/post_picture/1835174163661012994/KJ6sA9x6gOPzz5qt.webp) ### 3. guizang-social-card-skill 项目名:guizang-social-card-skill GitHub 仓库地址: https://github.com/op7418/guizang-social-card-skill 藏师傅的新作品,之前也有推荐过藏师傅的 PPT skill,感兴趣的话可以点击下面的链接去看一下 https://zhuanlan.zhihu.com/p/2040526006285509057 优点有如下几个: 第一:延续上次 guizang-ppt-skill 的两种风格,审美起点更高 不是让你从零调字体、颜色、间距,而是先给你一套有约束的视觉语言。这个约束反而是好事,因为大多数内容图做丑,不是因为自由不够,而是因为自由太多 第二:适合长文拆图 说成大白话就是为文章配图,将一片文章拆成 5 到 9 张小红书卡片,它能从结构、标题、重点句、截图排布这些地方一起处理,不只是做一张封面 第三:修改成本低 最初的产物是单文件 HTML,再用 Playwright 渲染成 PNG。HTML 和 CSS 都能继续改,出问题也能查,不像很多在线设计工具,最后只剩一个不可控的导出结果 当然我也需要说一说局限性,原配的 skill 主要是生成小红书和公众号内容的,这两个平台的图片比例为 3:4 和 21:9,**对于 4:3 和 16:9 比例的支持稍微差一些**,我第一次在生成 16:9 的配图时出现了配图模糊的情况,后续在原配的基础上进行一些改进,顺利的解决了这个问题,这一点有必要告诉大家 ![image.png](https://pic.code-nav.cn/post_picture/1835174163661012994/WZvw1fN4V4Cs4CuE.webp) ### 4. agent-skills 项目名:agent-skills GitHub 仓库地址: https://github.com/addyosmani/agent-skills 区别于很多 "角色大全" 式的 skill 合集,产品、运营、设计、销售、法务、数据分析都来一点,看起来很全 agent-skills 的重心收得很窄,基本围绕软件开发这条线展开:**从需求澄清、写规格、拆任务,到编码、测试、调试、代码审查、安全、性能、CI/CD、发布、监控和迁移废弃** 所以它更像是把一个资深工程团队的日常习惯拆开,写成 AI coding agent 可以照着执行的工作流 里面不只是“你是一个前端工程师”这种角色设定,还会告诉agent:什么时候该写 spec,什么时候该停下来补测试,什么时候该做安全检查,什么时候该考虑回滚和可观测性等等 ![image.png](https://pic.code-nav.cn/post_picture/1835174163661012994/hapkzymCDCEOzIAa.webp) ### 5. whisper 项目名:whisper GitHub 仓库地址: https://github.com/openai/whisper 如果单说功能的话,这个仓库的功能是极其简单的: 将视频 / 音频转换成文字稿 而且这个文字稿也不是完美的。**它不会顺手帮你清理语气词,不会自动帮你删除重复表达,对一些专业名词、人名、品牌名的识别也不总是稳定。**你如果拿它的结果直接发出去,往往还是要自己再校对一遍 对于做字幕,没有剪映方便 对于视频会议的转写也没有腾讯会议、飞书会议方便 对于只是偶尔转一段采访或者播客,现在市面上也有很多现成工具能做,比如 TurboScribe、Riverside、VEED、Otter、Notta,中文场景里还有飞书妙记、通义听悟这类产品,很多都比它更省事 --- 那我为什么还想要来推荐这个仓库呢? 第一点是,**它足够便宜**,准确点说,是几乎没有使用门槛上的持续成本 很多在线转写产品表面上能免费试,但真正想长期用,要么限制分钟数,要么限制文件大小,要么导出字幕和全文稿时开始收费 Whisper 不一样。环境装好、模型下载好之后,它就是一个可以一直放在你电脑上的本地工具。你不用反复算时长,也不用担心哪天平台把免费额度收紧 第二点是,**它是本地可控的** 你的视频、录音、采访、会议材料,不需要上传到第三方网站。这个差别平时不明显,但一旦素材涉及客户、内部沟通、未发布内容、个人隐私,你就会知道“本地处理”这四个字有多值钱。很多产品更方便,但方便的代价就是文件先出去;Whisper 不是。 第三点是,它虽然简单,但它简单得很像一个基座 很多成品工具解决的是“给你一个结果”,Whisper 解决的是“把音频转文字这一步,变成你自己手里的一项能力” 你可以拿它输出 .txt、.srt、.vtt、.json,然后继续接自己的工作流:字幕、归档、摘要、检索、内容拆条、AI 总结,后面怎么接都行 这也是它和剪映、飞书会议、腾讯会议这类工具最大的差异点。那些工具是成品,目标是让普通用户少折腾;Whisper 是底层能力,目标是让你自己决定后面怎么用 它的优点说白了就三个:免费、本地、通用 它的缺点也很明确:不够傻瓜、不够省心、结果不够干净、后处理要靠自己 --- 所以它并不是一个适合所有人的仓库 如果你只是想偶尔给视频加字幕,剪映更合适 如果你主要是开会并且想自动生成纪要,腾讯会议、飞书会议更合适。 如果你不想碰命令行,也不想自己管模型和环境,那各种在线转写网站也更合适 --- 但如果你属于下面这几类人,Whisper 就很值得看一眼: 你经常要处理录音、播客、口播、采访、课程、会议素材; 你在意隐私,不想把文件上传到第三方平台; 你想把“音频转文字”这一步沉淀成一个长期可用的本地能力; 或者你是开发者,后面还想接字幕、摘要、检索、自动化流程 对这些人来说,Whisper 的价值不在于它做得比所有产品都更好,而在于它把最基础、也最关键的一步,稳定地交回到了你自己手里。 如果要给它一句比较准确的定位,我会更愿意这么说: **Whisper 不是最好用的视频转文字产品,但它是很值得拥有的本地转写底座** ![image.png](https://pic.code-nav.cn/post_picture/1835174163661012994/tsi2MIFfqI5J4ePF.webp) --- 最后: 后面肯定还会继续遇到: 让我真正想装下来试试的项目。 这个系列也会继续更新下去。

当 Loop 不再是内核:Coding Agent 真正难的地方

两三年前,做一个 agent 的"内核"是一件能说清楚的事:ReAct 循环、任务拆分、memory、plan、tool calling,拼好就能跑。今天这套东西已经商品化了。它不是过时了,是它**迁移了位置**也就是说它沉进了模型本身。 理解这次迁移,比追逐任何一个新框架都重要。 ## 能力的边界,由"可验证性"画出来的 前沿实验室现在用 RLVR(基于可验证奖励的强化学习)把 agentic 行为直接训进模型:多轮工具调用、自我纠错、中途重新规划。**一种能力能不能被训进模型,取决于它的结果能不能被廉价地验证。** 强化学习需要一个 verifier 来判断“这一步做得好不好”。凡是结果可以被一个程序自动判定对错的领域,模型就能在上面做大规模自我博弈,把能力内化。如果结果模糊、需要人来判断好坏的领域,这条路就很难了。 coding agent 先成熟并不是偶然。编译器和测试套件,就是一个免费的、客观的、永不疲倦的 verifier。代码的对错恰好落在机器可判定。我觉得 agent 的扩散顺序很清楚了:任何"数字动作 + 可校验结果"的领域会被依次攻克,而反馈一旦变模糊(比如 browser agent、研究类任务),难度就很大。 大模型擅长的,往往是那些容易验证对错的领域。 ## Harness 变薄,不是因为事情变少了 把推理和规划交给模型之后,外层的 harness 肉眼可见地变薄了。其实去看 Claude Code 这类工具的内部,会发现它的 loop 简单得近乎朴素。很容易得出一个错误结论:做 agent 这件事正在变简单。 实则不然。这里可以说是一种**复杂度守恒**:复杂度没有被消灭,它从循环逻辑里流了出来,重新沉淀到三个地方,system prompt、工具设计、context 管理。loop 的代码行数或许下降了,可是把一个 agent 做好所需要的判断力,一点没少。 如果继续在 loop 上发力,把 plan 模块打磨得更精巧、把状态机写得更工整,这些收益其实在递减,因为那块地正在被模型层吞掉。真正的杠杆,在模型**接管不了**的地方。 ## 模型唯一无法替你做的事:决定它能看见什么 一个语言模型,无论多强,有一个根本的局限:它只能对它上下文里的东西做出反应。它没有眼睛 没有记忆,没有对外部世界的独立访问。它的整个"世界",就是我们喂进上下文的那些 token。 **塑造模型的"世界",它每做一个动作之后,看见的是什么。** SWE-agent 那篇论文提出过一个概念,叫 ACI(agent-computer interface),agent 的智能表现,不只取决于模型本身,同样取决于它的动作会得到什么样的反馈。一个工具报错时,它返回的不是"日志",而是模型的下一条 prompt,是给它一段无意义的栈,还是给它一条能据以纠正的线索,直接决定了它接下来会不会卡死。**设计工具,其实是在设计模型所能感知的那个现实。**这比设计循环要根本得多。 **对抗"遗忘",管理那段会随时间腐烂的记忆。** 上下文一长,中间的信息就会塌陷(所谓 lost-in-the-middle),会话会"腐烂"。context 工程的本质,不是把更多东西塞进去,是**对一段有限且会衰减的注意力做主动的策展**:什么该留、什么该压缩成摘要、什么该隔离到别处去处理、哪些"失败的痕迹"反而要刻意保留下来好让模型自我纠正。这是一项复杂的工程,而不是 prompt 技巧的集合。Manus 团队和 Anthropic 各有一篇 context engineering 的复盘,写得很好。 做 agent,正在从"编写控制逻辑"变成"为一个无法完全控制的智能体,设计它的感知与记忆"。编写控制逻辑是确定性编程,设计它的感知与记忆是更难、也是更有意思的那部分。 ## 开始构建你自己的 Agent 吧 我做了一个开源的命令行 coding agent:[**codewright**](https://github.com/Caldalis/codewright)。 github 地址:https://github.com/Caldalis/codewright 它在设计上刻意押注"薄 loop、厚感知与记忆":引擎核心是一个朴素的流式循环,真正下功夫的地方都在外面——把工具的反馈设计成能引导模型恢复的形态、上下文逼近窗口时自动压缩、用只读的子 agent 隔离探索类任务以免污染主上下文、把验证过的经验蒸馏成可复用的 skill。模型层是 OpenAI-compatible 的,换个 `base_url` 就能在 GPT、DeepSeek、Qwen、本地 Ollama 之间切换。 欢迎大家去翻代码、提 issue 或 fork,也希望大家能点点 star: **https://github.com/Caldalis/codewright**

全栈项目部署实战指南:Java / Python / Vue / React 一站式搞定

# 全栈项目部署实战指南:Java / Python / Vue / React 一站式搞定 > 从本地开发到生产上线,覆盖 Spring Boot、FastAPI/Flask、Vue、React 四大技术栈的完整部署方案,结合 Docker 容器化 + Nginx 反向代理 + CI/CD 自动化,帮你打通项目上线的"最后一公里"。 > > **本文中的所有部署方式,均来自我日常工作中维护的多个生产项目,以及个人开源项目的实战经验。如果对你有帮助,希望给我的开源项目点赞支持一下吧,你们的点赞和收藏都是我的动力! https://github.com/DevYangJC** **🧪 实战推荐:** 如果你想找一个前后端分离的真实项目来练手部署,推荐试试我的开源项目 —— **[DataLoom](https://github.com/DevYangJC/DataLoom)**,把在线表格(类似 Excel)像零件一样装进你的项目里。它的架构和本文完全对应:**前端是独立 SPA(Vue 3 + Luckysheet)、后端是独立微服务(Spring Boot)、前后端之间只有 REST API 通信**。读完本文后,用它来实操一遍"前端 Nginx 托管 + 后端脚本部署 + Nginx 反向代理"的完整流程,体会会更深刻。 --- ## 目录 - [一、部署前置:服务器环境准备](#一部署前置服务器环境准备) - [1.1 服务器选型建议](#11-服务器选型建议) - [1.2 基础环境安装](#12-基础环境安装) - [1.3 部署前检查清单](#13-部署前检查清单) - [1.4 防火墙与安全组](#14-防火墙与安全组) - [二、Java(Spring Boot)项目部署](#二javaspring-boot项目部署) - [三、Python(FastAPI / Flask)项目部署](#三pythonfastapi--flask项目部署) - [四、前端(Vue / React)项目部署](#四前端vue--react项目部署) - [五、Nginx 反向代理与域名配置](#五nginx-反向代理与域名配置) - [六、Docker Compose 一键编排](#六docker-compose-一键编排) - [七、CI/CD 自动化部署](#七cicd-自动化部署) - [八、生产环境最佳实践](#八生产环境最佳实践) - [九、常见问题排查](#九常见问题排查) --- ![slide_01.png](https://pic.code-nav.cn/post_picture/1609766978631761921/Z1VvJKiiEjcVqeY1.webp) ## 一、部署前置:服务器环境准备 ### 1.1 服务器选型建议 | 场景 | 推荐配置 | 适用项目 | |------|----------|----------| | 个人项目/测试 | 2C4G | 单体应用、静态前端 | | 中小型生产 | 4C8G | Spring Boot + 前端 | | 中大型生产 | 8C16G+ | 微服务、多容器编排 | > **云服务商选择**:国内推荐阿里云、腾讯云、华为云;海外推荐 AWS、DigitalOcean。学生和开发者可关注各厂商的优惠活动,通常首年价格极低。 ### 1.2 基础环境安装 在开始安装之前,先想清楚一个问题:**你部署的项目用到了哪些技术?** 不同的项目需要不同的运行环境,没必要一股脑全装上。下面这张表帮你快速判断: | 你要部署什么 | 必装环境 | 可选环境 | |-------------|---------|---------| | Spring Boot 项目 | JDK 17+ | Maven(服务器构建时需要) | | FastAPI / Flask 项目 | Python 3.10+、pip | Nginx(生产环境反向代理) | | Vue / React 前端项目 | Node.js 20+(构建时需要) | Nginx(托管静态文件) | | 使用 Docker 部署 | Docker、Docker Compose | — | | 需要数据库 | MySQL / PostgreSQL | Redis(缓存) | > **安装顺序建议**:先装基础工具(curl、git 等)→ 再装运行环境(JDK、Python、Node)→ 最后装基础设施(Docker、Nginx、数据库)。因为后面的软件可能依赖前面的工具来下载和安装。 以 CentOS 7/8 为例,下面按步骤安装: ```bash # ======================================== # 第一步:系统更新(装任何东西之前先更新) # ======================================== # CentOS 7 sudo yum update -y # CentOS 8 / Rocky Linux / AlmaLinux sudo dnf update -y # ======================================== # 第二步:基础工具(必须装,后面都会用到) # ======================================== sudo yum install -y curl wget git vim unzip net-tools lsof # curl - 下载文件用(装 Docker、Node 都靠它) # wget - 另一个下载工具,某些脚本只支持 wget # git - 拉代码用 # vim - 编辑配置文件 # unzip - 解压 zip 包 # net-tools - netstat 等网络排查工具 # lsof - 查看端口占用(服务启动失败时排查用) # ======================================== # 第三步:Docker 和 Docker Compose # ======================================== # 安装 Docker curl -fsSL https://get.docker.com | sh sudo systemctl start docker sudo systemctl enable docker # 把当前用户加入 docker 组,这样不用每次都 sudo sudo usermod -aG docker $USER # ⚠️ 执行上面这条后需要重新登录 SSH 才能生效 # 安装 Docker Compose sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" \ -o /usr/local/bin/docker-compose sudo chmod +x /usr/local/bin/docker-compose # 验证 docker --version docker-compose --version # ======================================== # 第四步:Nginx(前端托管 + 反向代理) # ======================================== # CentOS 必须先装 epel-release 源,不然找不到 nginx 包 sudo yum install -y epel-release sudo yum install -y nginx sudo systemctl start nginx sudo systemctl enable nginx # ======================================== # 第五步:JDK 17(Java 项目必装) # ======================================== sudo yum install -y java-17-openjdk java-17-openjdk-devel # 验证 java -version # javac -version # 验证开发工具包(服务器构建时需要) # 如果你的项目还在用 JDK 8 或 11,装对应版本: # sudo yum install -y java-11-openjdk # JDK 11 # sudo yum install -y java-1.8.0-openjdk # JDK 8 # 多版本 JDK 共存时,用 alternatives 切换默认版本: # sudo alternatives --config java # ======================================== # 第六步:Python 3(FastAPI / Flask 项目必装) # ======================================== # CentOS 7(系统自带 Python 2,需要额外装 Python 3) sudo yum install -y https://repo.ius.io/ius-release-el7.rpm sudo yum install -y python3u python3u-pip python3u-venv # CentOS 8+ 自带 Python 3 sudo dnf install -y python3 python3-pip # 验证 python3 --version pip3 --version # 设置 pip 国内镜像(加速下载,默认源在国外很慢) pip3 config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple # ======================================== # 第七步:Node.js 20+(前端构建时需要) # ======================================== # 注意:Node.js 只在"服务器上直接构建"时才需要 # 如果你在本地构建好 dist/ 再上传服务器,可以不装 curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash - sudo yum install -y nodejs # 验证 node --version npm --version # 设置 npm 国内镜像(加速下载) npm config set registry https://registry.npmmirror.com # ======================================== # 第八步:Git(如果需要在服务器上拉代码) # ======================================== sudo yum install -y git # 配置 Git 用户信息(首次使用需要) git config --global user.name "你的名字" git config --global user.email "your@email.com" # 如果需要从私有仓库拉代码,配置 SSH 密钥: # ssh-keygen -t ed25519 -C "your@email.com" # 然后把 ~/.ssh/id_ed25519.pub 的内容添加到 Git 平台的 SSH Keys 设置中 ``` > **安装后验证清单**:装完跑一遍,确保每个工具都正常工作。如果某个命令报 `command not found`,说明没装上或者环境变量没配好: > > ```bash > java -version # ✅ 应该输出 openjdk version "17.x.x" > python3 --version # ✅ 应该输出 Python 3.x.x > node --version # ✅ 应该输出 v20.x.x > docker --version # ✅ 应该输出 Docker 2x.x.x > nginx -v # ✅ 应该输出 nginx version: nginx/1.x.x > git --version # ✅ 应该输出 git version 2.x.x > ``` ### 1.3 部署前检查清单 ![slide_02.png](https://pic.code-nav.cn/post_picture/1609766978631761921/kAmwFgvRuBccrDIP.webp) 很多新手拿到一台服务器就急着装软件、传代码、启动服务,结果到处报错——端口被占了、数据库连不上、文件权限不对、配置文件少写了逗号……这些问题如果在部署前花 10 分钟检查一遍,能省下好几个小时的排错时间。 部署前的检查就像出门旅行前的"伸手要钱"(身份证、手机、钥匙、钱包)——看似啰嗦,但少一样都走不了。 ```mermaid graph TD A["📋 部署前检查"] --> B["1️⃣ 代码与版本"] A --> C["2️⃣ 配置与依赖"] A --> D["3️⃣ 权限与密钥"] A --> E["4️⃣ 网络与端口"] A --> F["5️⃣ 磁盘与资源"] B --> G["✅ 全部通过 → 开始部署"] C --> G D --> G E --> G F --> G B -.->|"❌ 不通过"| H["🔧 修复后重新检查"] C -.->|"❌ 不通过"| H D -.->|"❌ 不通过"| H E -.->|"❌ 不通过"| H F -.->|"❌ 不通过"| H H --> A ``` #### 1️⃣ 代码与版本检查 | 检查项 | 怎么检查 | 为什么重要 | |--------|---------|-----------| | 代码是否已提交并推送到远程 | `git status` 看有没有未提交的改动 | 你部署的应该是远程仓库的代码,而不是本地改了一半的版本 | | 部署的版本号是否正确 | `git tag` 或 `git log --oneline -5` 确认当前版本 | 避免部署了错误的分支或旧的提交 | | 分支是否正确 | `git branch` 确认是 release/main 分支 | 部署了开发分支的代码上生产,后果你懂的 | | JAR 包 / 构建产物是否最新 | 对比构建时间和代码提交时间 | 避免用了缓存的旧包,新代码没打进去 | ```bash # 在本地或 CI 环境中执行 git status # 确认没有未提交的改动 git log --oneline -5 # 确认最新提交是你期望的版本 git tag -l "v*" # 查看所有版本标签 # 打一个版本标签(发布时建议打 tag) git tag -a v1.2.0 -m "发布 v1.2.0" git push origin v1.2.0 ``` #### 2️⃣ 配置与依赖检查 | 检查项 | 怎么检查 | 为什么重要 | |--------|---------|-----------| | 配置文件是否齐全 | 确认 `application-prod.yml` / `.env` 等文件存在 | 缺了配置文件,服务启动后连不上数据库、读不到参数 | | 数据库连接信息是否正确 | 检查 host、port、用户名、数据库名 | 配置写错了或者指向了测试库,生产数据就乱套了 | | 第三方服务密钥是否配置 | 检查短信、支付、OSS 等的 API Key | 缺了密钥,相关功能全部不可用 | | 依赖版本是否匹配 | 对比开发环境和生产环境的 JDK/Python/Node 版本 | "在我电脑上能跑"最常见的原因就是版本不一致 | | 环境变量是否设置 | 检查 `.env` 文件或 `export` 的变量 | 密码、密钥等敏感配置通常通过环境变量注入 | ```bash # 检查配置文件是否存在且内容完整 ls -la /opt/app/user-service/config/ cat /opt/app/user-service/config/application-prod.yml # 检查关键配置项 grep -E "datasource|redis|port" /opt/app/user-service/config/application-prod.yml # 测试数据库是否可连接(在服务器上执行) mysql -h 10.0.0.100 -u appuser -p -e "SELECT 1" # MySQL # python3 -c "import redis; r=redis.Redis(host='10.0.0.100',port=6379,password='xxx'); print(r.ping())" # Redis # 检查 Python 依赖是否安装 pip3 list | grep -E "fastapi|flask|gunicorn|uvicorn" # 检查 Node.js 依赖是否安装(前端项目) ls node_modules/ | head # 确认依赖已安装 ``` #### 3️⃣ 权限与密钥检查 | 检查项 | 怎么检查 | 为什么重要 | |--------|---------|-----------| | SSH 密钥是否配置 | `ssh -T git@github.com` 测试连通性 | 没有密钥就没法从私有仓库拉代码 | | 文件目录权限是否正确 | `ls -la /opt/app/` 查看权限 | 权限不对会导致应用无法读取配置或写入日志 | | 应用是否以非 root 用户运行 | 检查启动脚本中的用户身份 | root 运行应用一旦被攻破,整个服务器都沦陷 | | SSL 证书是否有效 | `openssl x509 -enddate` 检查过期时间 | 证书过期用户访问会提示"不安全",直接流失用户 | | 数据库用户权限是否最小化 | 检查应用用的数据库账号是否只有必要的权限 | 给应用 root 权限 = 给小偷配了万能钥匙 | ```bash # ---- SSH 密钥检查 ---- # 测试 GitHub SSH 连通性 ssh -T git@github.com # ✅ 看到 "Hi xxx! You've successfully authenticated" 说明密钥配置正确 # 生成新的 SSH 密钥(如果没有的话) ssh-keygen -t ed25519 -C "deploy@myapp" # 把公钥添加到 Git 平台:cat ~/.ssh/id_ed25519.pub 复制内容 # ---- 文件权限检查 ---- # 应用目录权限:所有者可读写执行,组和其他用户只读 ls -la /opt/app/user-service/ # ✅ 正确:drwxr-xr-x appuser appgroup ... # ❌ 危险:drwxrwxrwx (777,任何人都能改) # 修正权限 chown -R appuser:appgroup /opt/app/user-service/ chmod 755 /opt/app/user-service/bin/ chmod 600 /opt/app/user-service/config/application-prod.yml # 配置文件只有所有者可读写 # ---- SSL 证书有效期检查 ---- # Let's Encrypt 证书有效期 90 天,务必设置自动续期 openssl x509 -enddate -noout -in /etc/letsencrypt/live/example.com/fullchain.pem # 检查自动续期定时任务 sudo crontab -l | grep certbot # 如果没有,添加一个: # echo "0 3 * * * certbot renew --quiet" | sudo crontab - ``` #### 4️⃣ 网络与端口检查 | 检查项 | 怎么检查 | 为什么重要 | |--------|---------|-----------| | 应用端口是否被占用 | `lsof -i :8080` 或 `netstat -tlnp \| grep 8080` | 端口被别的程序占了,你的服务启动就报 "Address already in use" | | 服务器能否访问外部网络 | `curl -I https://www.baidu.com` | 服务器出不了外网,npm install / pip install 就会失败 | | 数据库服务器是否可达 | `telnet 10.0.0.100 3306` 或 `nc -zv 10.0.0.100 3306` | 连不上数据库,应用启动就报错 | | 防火墙是否放通了必要端口 | `sudo firewall-cmd --list-ports` | 端口没开,外部请求根本进不来 | | DNS 解析是否正常 | `nslookup your-domain.com` 或 `dig your-domain.com` | 域名没解析到服务器 IP,用户访问不了 | ```bash # 检查端口占用(部署前务必检查!) lsof -i :8080 # 检查 8080 端口 lsof -i :80 # 检查 80 端口 netstat -tlnp | grep -E "8080|80|443|3306" # 如果端口被占用,找到并停掉占用的进程 kill -15 <PID> # 先礼貌退出 # kill -9 <PID> # 实在不停再强制杀 # 测试服务器网络连通性 curl -I https://www.baidu.com # 测试外网 ping 10.0.0.100 # 测试内网数据库服务器 nc -zv 10.0.0.100 3306 # 测试数据库端口是否可连 nc -zv 10.0.0.100 6379 # 测试 Redis 端口是否可连 # DNS 解析检查 nslookup your-domain.com # 如果域名还没解析,可以先在本地 hosts 文件中临时配置: # echo "你的服务器IP your-domain.com" | sudo tee -a /etc/hosts ``` #### 5️⃣ 磁盘与资源检查 | 检查项 | 怎么检查 | 为什么重要 | |--------|---------|-----------| | 磁盘剩余空间 | `df -h` | 磁盘满了日志写不进去、数据库崩溃,问题一个比一个大 | | 内存是否充足 | `free -h` | 内存不够 Java 直接 OOM 崩掉 | | CPU 负载是否正常 | `top -bn1 \| head -5` | CPU 已经跑满了再加服务,新服务也起不来 | | 日志清理策略是否设置 | `du -sh /var/log/` 检查日志大小 | 日志无限增长是磁盘爆满的头号原因 | ```bash # 磁盘空间(使用率超过 80% 就要警惕了) df -h # Filesystem Size Used Avail Use% Mounted on # /dev/vda1 40G 15G 23G 40% / ← ✅ 正常 # /dev/vda1 40G 35G 3G 93% / ← ⚠️ 危险!赶紧清理 # 内存(关注 available 列,这是实际可用内存) free -h # total used free available # Mem: 7.6G 3.2G 1.1G 4.0G ← ✅ 充裕 # Mem: 7.6G 6.8G 128M 500M ← ⚠️ 偏紧,考虑加内存或减少 JVM 分配 # CPU 负载(load average 三个数分别代表 1/5/15 分钟的平均负载) # 数值超过 CPU 核心数说明过载了 top -bn1 | head -5 # 大文件排查(找出占空间最多的目录) du -sh /* | sort -rh | head -10 ``` > **一键检查脚本**:如果你觉得上面逐项检查太麻烦,可以把所有检查项写成一个脚本,部署前跑一次就行。把下面的脚本保存为 `pre-deploy-check.sh`,每次部署前执行 `bash pre-deploy-check.sh`: > > ```bash > #!/bin/bash > # 部署前一键检查脚本 > # 用法: bash pre-deploy-check.sh > > PASS=0; FAIL=0 > check() { > local desc="$1" cmd="$2" > echo -n "[$(echo $desc | cut -c1-20)] ... " > if eval "$cmd" &>/dev/null; then > echo "✅ 通过"; ((PASS++)) > else > echo "❌ 未通过"; ((FAIL++)) > fi > } > > echo "========== 部署前检查 ==========" > echo "" > echo "--- 环境 ---" > check "Java 安装" "java -version" > check "Python 安装" "python3 --version" > check "Node.js 安装" "node --version" > check "Docker 安装" "docker --version" > check "Nginx 安装" "nginx -v" > check "Git 安装" "git --version" > > echo "" > echo "--- 网络 ---" > check "外网连通" "curl -sI https://www.baidu.com" > check "DNS 解析" "nslookup baidu.com" > > echo "" > echo "--- 资源 ---" > check "磁盘 > 20%" "test $(df / | tail -1 | awk '{print $5}' | tr -d '%') -lt 80" > check "内存 > 500M" "test $(free -m | awk '/Mem/{print $7}') -gt 500" > > echo "" > echo "========== 结果: ✅ ${PASS} 通过, ❌ ${FAIL} 未通过 ==========" > if [ $FAIL -gt 0 ]; then > echo "⚠️ 请先修复未通过的项目再进行部署!" > exit 1 > fi > ``` ### 1.4 防火墙与安全组 ```bash # CentOS 7/8 使用 firewalld sudo systemctl start firewalld sudo systemctl enable firewalld # 开放常用端口 sudo firewall-cmd --permanent --add-port=22/tcp # SSH sudo firewall-cmd --permanent --add-port=80/tcp # HTTP sudo firewall-cmd --permanent --add-port=443/tcp # HTTPS sudo firewall-cmd --permanent --add-port=8080/tcp # Spring Boot 默认端口 # sudo firewall-cmd --permanent --add-port=3306/tcp # MySQL(仅内网访问,不建议公网开放) # 重载防火墙使规则生效 sudo firewall-cmd --reload # 查看已开放端口 sudo firewall-cmd --list-ports ``` > **安全提醒**:数据库端口(3306、5432、6379 等)务必不要直接暴露在公网,应通过安全组限制为内网访问。 --- ## 二、Java(Spring Boot)项目部署 ![slide_03.png](https://pic.code-nav.cn/post_picture/1609766978631761921/jweY4l0J3rHoLaDW.webp) ### 2.1 项目打包 Spring Boot 项目内嵌 Tomcat,直接打成可执行 JAR 即可: ```bash # Maven 项目 mvn clean package -DskipTests # Gradle 项目 gradle clean build -x test ``` 构建产物在 `target/` 目录下,类似 `user-service-1.0.0.jar`。 ### 2.2 方式一:脚本化部署(推荐,多服务运维首选) #### 为什么要用脚本部署? 你可能想问:Java 项目直接一条 `java -jar xxx.jar` 不就能跑了吗?确实能跑,但问题来了——你的服务器重启了怎么办?服务挂了怎么办?你要更新 JAR 包怎么办?每次都手动敲命令,时间长了自己都忘了怎么操作,换个人来接手更是两眼一抹黑。 脚本化部署就是把日常操作"录"下来,变成一条命令就能搞定的事情。就像微波炉的"一键热牛奶"按钮,你不需要知道微波炉内部怎么运作,按一下就行。同样的道理: - **服务没启动**,运行脚本 → 自动启动 - **服务已经在跑**,运行同一个脚本 → 自动重启(先停再启) - **要更新版本**,运行备份脚本 → 旧版本安全保存,出问题随时回退 这种方式最大的好处就是**简单可靠**——不需要学 Docker、不需要懂容器网络、不需要额外的运维工具,一台装了 Java 的 Linux 服务器就能跑。对于大多数中小项目来说,这就够了。 ```mermaid graph TD A["运行 ./restart.sh"] --> B{"服务是否在运行?"} B -->|没在跑| C["启动服务<br/>nohup java -jar xxx.jar &"] B -->|已经在跑| D["先停掉旧进程<br/>kill 旧PID"] D --> E["等待进程完全退出"] E --> C C --> F["记录新PID<br/>写入 pid 文件"] F --> G["✅ 服务已就绪"] ``` #### 目录结构设计 在开始之前,先说清楚文件放在哪里。一个好的目录结构就像一个整理好的工具箱——扳手在哪、螺丝刀在哪,一目了然,不用每次都翻箱倒柜。 我们规定所有服务都放在 `/opt/app/` 下面,每个服务一个独立的文件夹,里面的子目录各有各的用处: ``` /opt/app/ ├── user-service/ # 用户服务 │ ├── app/ │ │ └── user-service.jar # JAR 包(应用本体) │ ├── config/ │ │ └── application-prod.yml # 外部配置文件 │ ├── logs/ # 运行日志 │ ├── tmp/ # 临时文件(存放 PID 文件等) │ └── bin/ │ ├── restart.sh # 重启/启动脚本(核心,就这一个) │ └── backup.sh # 备份脚本 │ ├── api-gateway/ # API 网关 │ ├── app/ │ │ └── api-gateway.jar │ ├── config/ │ │ └── application-prod.yml │ ├── logs/ │ ├── tmp/ │ └── bin/ │ ├── restart.sh │ └── backup.sh │ └── backup/ # 发布备份(回滚用) ├── user-service/ └── api-gateway/ ``` 这些目录都是什么用?用大白话给你解释一下: | 目录 | 里面放什么 | 为什么要单独放 | |------|-----------|---------------| | `app/` | JAR 包,你的应用本体 | 和配置文件分开,更新时只需替换这里面的 JAR 包,配置不受影响 | | `config/` | `application-prod.yml` 外部配置 | 改数据库密码、调端口号,直接编辑这个文件就行,不用重新打包 | | `logs/` | 运行产生的日志文件 | 日志会越来越大,单独放方便定期清理,不会撑爆磁盘 | | `tmp/` | PID 文件(记录进程号) | 脚本靠这个文件判断服务是否在运行,就像门上挂的"有人/无人"牌子 | | `bin/` | 管理脚本 | 所有运维操作都在这里,一条命令搞定 | | `backup/` | 旧版本 JAR 包备份 | 更新前自动备份,万一新版本有问题,把旧的换回来就行 | #### 一键初始化目录结构 每新增一个服务都要手动建这么多文件夹?当然不用。下面这个初始化脚本帮你一键创建好所有目录,新项目部署时跑一次就行: ```bash #!/bin/bash #=================================================== # 初始化服务目录结构 # 用法: ./init-structure.sh <服务名> # 示例: ./init-structure.sh user-service #=================================================== APP_HOME="/opt/app" SVC_NAME=$1 # 检查参数 if [ -z "$SVC_NAME" ]; then echo "❌ 请提供服务名称!" echo "用法: $0 <服务名>" echo "示例: $0 user-service" exit 1 fi SVC_DIR="$APP_HOME/$SVC_NAME" # 检查是否已存在 if [ -d "$SVC_DIR" ]; then echo "⚠️ 目录已存在: $SVC_DIR" read -p "是否继续?(会跳过已存在的目录)[y/N] " confirm if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then echo "已取消" exit 0 fi fi echo "🔧 正在初始化服务目录: $SVC_DIR" # 创建各子目录 mkdir -p "$SVC_DIR/app" mkdir -p "$SVC_DIR/config" mkdir -p "$SVC_DIR/logs" mkdir -p "$SVC_DIR/tmp" mkdir -p "$SVC_DIR/bin" mkdir -p "$APP_HOME/backup/$SVC_NAME" # 创建重启脚本 cat > "$SVC_DIR/bin/restart.sh" << 'SCRIPT' #!/bin/bash #=================================================== # 服务重启/启动脚本 # 逻辑:服务没启动 → 启动;已经在跑 → 重启 # 用法: ./restart.sh [prod|dev] #=================================================== # ---- 基础配置(按实际项目修改这三行)---- APP_NAME="这里改成你的服务名" JAR_NAME="这里改成你的jar包名.jar" ACTIVE_PROFILE=${1:-prod} # ---- 以下不用改 ---- APP_DIR=$(cd "$(dirname "$0")/.." && pwd) JAR_PATH="$APP_DIR/app/$JAR_NAME" CONFIG_DIR="$APP_DIR/config" LOG_DIR="$APP_DIR/logs" PID_FILE="$APP_DIR/tmp/app.pid" # JVM 参数 JVM_OPTS="-Xms512m -Xmx1024m" JVM_OPTS="$JVM_OPTS -XX:+UseG1GC" JVM_OPTS="$JVM_OPTS -XX:+HeapDumpOnOutOfMemoryError" JVM_OPTS="$JVM_OPTS -XX:HeapDumpPath=$LOG_DIR/heapdump.hprof" JVM_OPTS="$JVM_OPTS -Djava.security.egd=file:/dev/./urandom" JVM_OPTS="$JVM_OPTS -Dfile.encoding=UTF-8" # 颜色输出 RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m' info() { echo -e "${GREEN}[INFO]${NC} $1"; } warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } error() { echo -e "${RED}[ERROR]${NC} $1"; } # 获取 PID get_pid() { if [ -f "$PID_FILE" ]; then cat "$PID_FILE" 2>/dev/null fi } # 检查是否在运行 is_running() { local pid=$(get_pid) [ -z "$pid" ] && return 1 if ps -p "$pid" > /dev/null 2>&1; then return 0 else rm -f "$PID_FILE" return 1 fi } # 停止服务 do_stop() { if ! is_running; then warn "$APP_NAME 未在运行" return 0 fi local pid=$(get_pid) info "正在停止 $APP_NAME (PID: $pid) ..." # 先发 SIGTERM,让应用优雅关闭(保存数据、释放连接) kill -15 "$pid" # 等最多 30 秒 for i in $(seq 1 30); do if ! ps -p "$pid" > /dev/null 2>&1; then info "$APP_NAME 已优雅停止" rm -f "$PID_FILE" return 0 fi sleep 1 done # 超时了还没停,强制杀掉 warn "优雅关闭超时,强制终止 ..." kill -9 "$pid" rm -f "$PID_FILE" info "$APP_NAME 已强制停止" } # 启动服务 do_start() { if is_running; then warn "$APP_NAME 已在运行 (PID: $(get_pid)),将重启 ..." do_stop sleep 2 fi if [ ! -f "$JAR_PATH" ]; then error "找不到 JAR 文件: $JAR_PATH" exit 1 fi mkdir -p "$LOG_DIR" "$APP_DIR/tmp" info "正在启动 $APP_NAME ..." info " 环境 : $ACTIVE_PROFILE" info " JAR : $JAR_PATH" info " 配置 : $CONFIG_DIR/" nohup java $JVM_OPTS \ -jar "$JAR_PATH" \ --spring.profiles.active="$ACTIVE_PROFILE" \ --spring.config.location="$CONFIG_DIR/" \ >> "$LOG_DIR/console-$(date '+%Y%m%d%H%M').log" 2>&1 & echo $! > "$PID_FILE" sleep 5 if is_running; then info "✅ $APP_NAME 启动成功 (PID: $(get_pid))" else error "❌ $APP_NAME 启动失败!请查看日志: $LOG_DIR/" exit 1 fi } # 主逻辑:运行就是重启/启动 do_start SCRIPT # 创建备份脚本 cat > "$SVC_DIR/bin/backup.sh" << 'SCRIPT' #!/bin/bash #=================================================== # 服务备份脚本 # 用法: ./backup.sh # 功能: 把当前 JAR 包复制到 backup 目录,带时间戳 #=================================================== APP_NAME="这里改成你的服务名" JAR_NAME="这里改成你的jar包名.jar" APP_DIR=$(cd "$(dirname "$0")/.." && pwd) JAR_PATH="$APP_DIR/app/$JAR_NAME" BACKUP_DIR="/opt/app/backup/$APP_NAME" RED='\033[0;31m'; GREEN='\033[0;32m'; NC='\033[0m' info() { echo -e "${GREEN}[INFO]${NC} $1"; } error() { echo -e "${RED}[ERROR]${NC} $1"; } if [ ! -f "$JAR_PATH" ]; then error "找不到 JAR 文件: $JAR_PATH" exit 1 fi mkdir -p "$BACKUP_DIR" # 备份文件名加上时间戳,方便识别 BACKUP_NAME="${JAR_NAME}.bak_$(date '+%Y%m%d%H%M%S')" cp "$JAR_PATH" "$BACKUP_DIR/$BACKUP_NAME" info "✅ 备份完成: $BACKUP_DIR/$BACKUP_NAME" # 只保留最近 5 个备份,旧的自动清理 ls -t "$BACKUP_DIR"/*.bak_* 2>/dev/null | tail -n +6 | xargs -r rm -f info "已清理旧备份(保留最近 5 个)" SCRIPT # 设置执行权限 chmod +x "$SVC_DIR/bin/restart.sh" chmod +x "$SVC_DIR/bin/backup.sh" echo "" echo "✅ 目录结构初始化完成!" echo "" echo "📁 $SVC_DIR/" echo " ├── app/ ← 把 JAR 包放这里" echo " ├── config/ ← 把 application-prod.yml 放这里" echo " ├── logs/ ← 日志会自动写到这里" echo " ├── tmp/ ← PID 文件自动管理" echo " └── bin/" echo " ├── restart.sh ← 启动/重启脚本" echo " └── backup.sh ← 备份脚本" echo "" echo "⚡ 下一步:" echo " 1. 把 JAR 包上传到 $SVC_DIR/app/" echo " 2. 把配置文件放到 $SVC_DIR/config/" echo " 3. 修改 restart.sh 和 backup.sh 顶部的 APP_NAME 和 JAR_NAME" echo " 4. 运行 $SVC_DIR/bin/restart.sh 启动服务" ``` 使用方式非常简单: ```bash # 初始化一个新服务的目录结构 ./init-structure.sh user-service # 再初始化另一个服务 ./init-structure.sh api-gateway ``` #### 核心脚本详解 初始化完成后,每个服务的 `bin/` 目录下会有两个脚本。这两个脚本就是日常运维的全部武器,不需要记复杂的命令,会这两个就够了。 **脚本一:restart.sh —— 启动/重启服务** 这是最核心的脚本,日常 90% 的操作都用它。它的工作逻辑很简单:**服务没启动就启动,已经在跑就先停再启**。你不需要关心服务当前是什么状态,运行就完了。 ```mermaid graph TD A["运行 ./restart.sh"] --> B{"检查 PID 文件<br/>服务是否在运行?"} B -->|"没有 PID 文件<br/>或进程已死"| C["直接启动"] B -->|"有 PID 且进程活着"| D["kill -15 发送停止信号<br/>给应用 30 秒优雅关闭"] D --> E{"30 秒内停了吗?"} E -->|是| F["确认已停"] E -->|否| G["kill -9 强制杀掉<br/>防止无限卡住"] G --> F F --> H["等待 2 秒<br/>确保端口释放"] H --> C C --> I["nohup java -jar xxx.jar &<br/>后台启动服务"] I --> J["记录 PID 到 tmp/app.pid"] J --> K["等待 5 秒检查<br/>确认启动成功"] K --> L["✅ 服务已就绪"] ``` 几个你可能好奇的设计细节,用大白话解释一下: **为什么要 PID 文件?** PID 文件就是记录"当前运行的进程号"的小文件,放在 `tmp/app.pid` 里。脚本靠它判断服务是否在运行——有 PID 文件且对应进程还活着,说明服务在跑;没有 PID 文件或进程已经死了,说明服务停了。这比每次都 `ps -ef | grep xxx` 去查要快得多、准得多。 **为什么先 kill -15 再 kill -9?** `kill -15`(SIGTERM)相当于"礼貌地请应用退出",Spring Boot 收到这个信号后会执行清理工作——关闭数据库连接、保存缓存、写完日志——然后优雅退出。`kill -9`(SIGKILL)相当于"直接拔电源",进程立刻消失,什么清理都来不及做。所以我们的策略是:先礼貌请退,等 30 秒,如果还不走再强制拖走。 **为什么用 nohup?** `nohup` 的意思是"不要因为终端关闭就停掉程序"。如果没有 nohup,你 SSH 登录服务器启动 Java 进程,关掉 SSH 窗口后进程就跟着死了。加了 nohup,即使你关掉终端、断开 SSH,服务依然在后台运行。 **为什么 sleep 5 秒再检查?** Java 应用启动需要时间——加载类、初始化 Spring 容器、连接数据库……这些不是瞬间完成的。等 5 秒再检查进程是否还活着,可以过滤掉"刚启动就崩了"的情况。如果 5 秒后进程还活着,基本可以认为启动成功了。 **脚本二:backup.sh —— 备份当前版本** 更新版本之前先备份,这是运维的基本素养。万一新版本有 bug,把备份的旧 JAR 包换回来就能恢复,比重新构建快得多。 ```mermaid graph LR A["运行 ./backup.sh"] --> B["把当前 JAR 复制到<br/>/opt/app/backup/服务名/"] B --> C["文件名加上时间戳<br/>user-service.jar.bak_202606161030"] C --> D["检查备份数量"] D --> E{"超过 5 个?"} E -->|是| F["自动删除最老的<br/>只保留最近 5 个"] E -->|否| G["✅ 备份完成"] F --> G ``` 备份文件名带时间戳,这样你能清楚地知道每个备份是什么时候做的。自动保留最近 5 个备份,太老的自动清理,防止备份目录无限膨胀。 #### 日常运维——就这两条命令 | 场景 | 命令 | 效果 | |------|------|------| | 首次启动服务 | `./restart.sh` | 检测到没在运行,直接启动 | | 重启服务 | `./restart.sh` | 检测到在运行,先停再启 | | 切换到开发环境 | `./restart.sh dev` | 以 dev 环境启动(默认是 prod) | | 更新版本前备份 | `./backup.sh` | 当前 JAR 包安全保存到 backup 目录 | 就这些,没有复杂的参数,没有多余的子命令。 #### 发布更新完整流程 下面这张图展示了从"开发完成"到"上线运行"的完整操作流程,每一步都配有对应的命令: ```mermaid graph TD A["1️⃣ 备份当前版本"] --> B["2️⃣ 停止并重启服务<br/>(restart.sh 会自动停旧的启新的)"] B --> C["3️⃣ 上传新 JAR 包<br/>覆盖 app/ 下的旧文件"] C --> D["4️⃣ 再次运行 restart.sh<br/>用新 JAR 启动服务"] D --> E{"5️⃣ 检查日志<br/>服务是否正常?"} E -->|"正常"| F["✅ 发布完成"] E -->|"异常"| G["6️⃣ 从 backup 恢复旧版本<br/>cp backup/xxx.jar app/"] G --> H["再次 restart.sh"] H --> F ``` 对应的命令操作: ```bash # 1. 先备份(防止翻车) cd /opt/app/user-service/bin ./backup.sh # 2. 上传新 JAR 包到 app 目录(从你本地电脑上传到服务器) scp target/user-service-1.0.1.jar user@server:/opt/app/user-service/app/user-service.jar # 3. 运行 restart.sh,自动停旧版本、启新版本 ./restart.sh # 4. 看一眼日志,确认启动正常 tail -f ../logs/console-*.log # 5. 万一新版本有问题,从备份恢复 cp /opt/app/backup/user-service/user-service.jar.bak_20260616103000 \ /opt/app/user-service/app/user-service.jar ./restart.sh ``` #### 外部配置文件说明 你可能注意到,启动命令里有一行 `--spring.config.location=../config/`。这是什么意思? Spring Boot 项目打包时,配置文件(`application.yml`)会被打进 JAR 包里。但生产环境的数据库地址、密码跟开发环境肯定不一样,你不可能为每台服务器单独打一个 JAR 包。`--spring.config.location` 就是告诉 Spring Boot:"别用 JAR 包里面的配置了,用我指定目录下的配置文件。" 这样 JAR 包只打一次,不同服务器放不同的 `application-prod.yml` 就行。 ```yaml # /opt/app/user-service/config/application-prod.yml server: port: 8081 spring: datasource: url: jdbc:mysql://10.0.0.100:3306/user_db?useSSL=true username: ${DB_USER} # 也可通过环境变量注入,避免密码明文写在文件里 password: ${DB_PASSWORD} hikari: maximum-pool-size: 20 logging: file: name: logs/application.log logback: rollingpolicy: max-file-size: 50MB max-history: 30 ``` 这种方式的好处用三句话总结: 1. **改配置不需要重新打包** — 直接编辑 yml 文件,运行 `./restart.sh` 就生效 2. **不同服务器可以有不同配置** — JAR 包保持一致,配置各管各的 3. **敏感信息不入代码仓库** — 数据库密码、API 密钥只存在于服务器上,不会泄露到 Git ### 2.3 方式二:systemd 服务管理(适合需要开机自启的场景) 如果你的服务需要**开机自启**或与系统服务统一管理,可以将脚本注册为 systemd 服务: ```ini # /etc/systemd/system/user-service.service [Unit] Description=User Service (Spring Boot) After=network.target [Service] Type=forking User=deploy WorkingDirectory=/opt/app/user-service ExecStart=/opt/app/user-service/bin/app.sh start ExecStop=/opt/app/user-service/bin/app.sh stop ExecReload=/opt/app/user-service/bin/app.sh restart PIDFile=/opt/app/user-service/tmp/app.pid Restart=on-failure RestartSec=10 [Install] WantedBy=multi-user.target ``` ```bash # 注册并启动 sudo systemctl daemon-reload sudo systemctl start user-service sudo systemctl enable user-service # 开机自启 # 查看状态和日志 sudo systemctl status user-service sudo journalctl -u user-service -f ``` > **提示**:使用 `Type=forking` 配合脚本中的后台启动(nohup &),systemd 能通过 PIDFile 正确追踪进程状态。 ### 2.4 方式三:Docker 容器化部署 #### 什么是 Docker?为什么要用 Docker? 先说一个很多人都会遇到的痛点:**"在我电脑上明明能跑啊!"**——你本地开发得好好的,一部署到服务器就各种报错,JDK 版本不对、系统依赖缺失、文件路径写死……这些问题说到底都是"环境不一致"造成的。 Docker 就是来解决这个问题的。它的核心思路很简单:**把你的应用和它需要的所有依赖(JDK、系统库、配置文件)一起打包成一个"镜像"**,这个镜像就像一个密封的集装箱——不管你把它搬到哪台机器上,只要装了 Docker,打开就能跑,环境一模一样,不会再有"在我电脑上能跑"的尴尬。 用大白话来打几个比方: - **镜像(Image)** = 装好软件的系统盘。比如 `mysql:8.0` 这个镜像,就是一个已经装好了 MySQL 8.0 的"系统盘",你拿过来直接用就行,不用自己安装配置。 - **容器(Container)** = 用系统盘装好的、正在运行的电脑。一个镜像可以"装"出多个容器,就像一张系统盘可以装多台电脑。 - **Dockerfile** = 安装说明书。告诉 Docker 怎么一步步打包你的应用——先装 JDK,再复制代码,再编译打包……每一步都写清楚。 - **Docker Compose** = 批量启动器。你的项目通常不只是一个应用,还有数据库、缓存、Nginx……Docker Compose 让你用一个 YAML 文件定义所有服务,一条命令全部拉起来。 ```mermaid graph LR A["Dockerfile<br/>安装说明书"] -->|docker build| B["镜像 Image<br/>打包好的系统盘"] B -->|docker run| C["容器 Container<br/>正在运行的服务"] B -->|docker run| D["容器 Container<br/>另一个实例"] B -->|docker run| E["容器 Container<br/>又一个实例"] F["docker-compose.yml<br/>批量编排文件"] -->|docker-compose up| G["一键启动<br/>所有服务"] ``` > **Docker vs 传统部署的直观对比**:传统部署就像自己买砖、买水泥、雇工人盖房子,每换一块地就得重新来;Docker 部署就像买了一个移动板房,里面水电齐全,搬到哪里都能直接住。 本节将分两部分讲解:第一部分是**测试环境单节点部署**(入门友好,快速上手),第二部分是**生产级多节点集群部署**(正式上线用,高可用保障)。 --- #### 第一部分:测试环境单节点部署 ##### 什么是单节点部署? 单节点部署就是把所有服务——应用、数据库、缓存、Nginx——都装在同一台服务器上,各自跑在独立的 Docker 容器里。就像把厨房、卧室、客厅都放在一个房间,虽然挤了点,但一个人住完全够用。 **适合场景**:自己开发测试、功能验证、给客户做演示。 **不适合场景**:正式对外提供服务。因为一旦这台机器宕机(断电、硬件故障、网络中断),所有服务全挂,用户完全无法访问。生产环境必须用多节点,后面会讲。 ##### 部署架构 下面这张图展示了单节点部署时,各容器之间的关系和调用链路。用户的请求先到 Nginx(门面),Nginx 根据请求路径把请求转发给对应的应用容器,应用容器再去连接数据库和缓存。 ```mermaid graph TB subgraph "单台服务器 192.168.1.100" subgraph "Docker 环境" nginx["Nginx 容器<br/>反向代理 / 流量分发<br/>端口: 80 / 443"] app1["应用容器 1<br/>Spring Boot 服务<br/>端口: 8080"] app2["应用容器 2<br/>Spring Boot 服务<br/>端口: 8081"] redis["Redis 容器<br/>缓存服务<br/>端口: 6379"] mysql["MySQL 容器<br/>数据库<br/>端口: 3306"] end end user["用户请求"] --> nginx nginx -->|"转发 /api/"| app1 nginx -->|"转发 /api/"| app2 app1 --> redis app1 --> mysql app2 --> redis app2 --> mysql ``` **数据流解读**:用户访问网站 → 请求先到 Nginx(统一入口)→ Nginx 把 API 请求转发给 Spring Boot 容器 → Spring Boot 从 Redis 读缓存,缓存没有就去 MySQL 查 → 查到后返回给用户,同时写入 Redis 缓存。 ##### 前置准备 在开始部署之前,需要先在服务器上安装 Docker 和 Docker Compose。就像做饭之前要先把灶台和锅碗瓢盆准备好一样,这些是后续所有操作的基础。 | 步骤 | 命令 | 大白话解释 | |------|------|----------| | 安装 Docker | `curl -fsSL https://get.docker.com \| bash` | 从 Docker 官网拉一个安装脚本,自动帮你装好 Docker 引擎。就像让专业师傅上门装灶台,一条命令搞定 | | 安装 Docker Compose | `sudo yum install docker-compose -y` | Docker Compose 是 Docker 的"批量管家",让你用一个配置文件同时管理多个容器。没有它你就得一个一个手动启动 | | 验证安装 | `docker --version` | 看一眼版本号,确认安装成功了。如果报 `command not found`,说明没装上,回头检查 | | 启动 Docker 服务 | `sudo systemctl start docker` | Docker 装好了但还没运行,这条命令相当于"开灶" | | 设置开机自启 | `sudo systemctl enable docker` | 服务器重启后 Docker 自动启动,不用你每次手动开。生产环境**必须设置**,不然机器重启后服务就断了 | ##### Docker Compose 配置文件详解 这是整个部署的核心——`docker-compose.yml`。它就像一张"施工图纸",告诉 Docker 要启动哪些服务、每个服务怎么配置、服务之间怎么关联。下面逐段讲解,每一行都配上大白话解释。 ```yaml # version 表示 Compose 文件格式的版本号 # 不同版本支持的功能不同,3.8 是目前最常用的稳定版本 version: '3.8' # services 下面定义所有需要运行的容器 # 每一个 service 就是一个独立的容器,可以理解为"一台虚拟小电脑" services: # ---- MySQL 数据库服务 ---- # 数据库是整个应用的数据仓库,所有业务数据都存在这里 mysql: # image 指定使用哪个"系统盘"来创建容器 # mysql:8.0 就是一个已经装好 MySQL 8.0 的系统盘,拿来即用 image: mysql:8.0 # container_name 给容器起个名字,方便后续管理 # 不指定的话 Docker 会自动生成一个随机名字(如 mystifying_tesla),不好记 container_name: test-mysql # restart: always —— 容器挂了自动重启 # 比如数据库进程崩了,Docker 会自动把它拉起来,不用你半夜爬起来手动重启 restart: always # environment 设置容器内部的环境变量 # 这些变量在 MySQL 首次启动时生效,帮你自动完成初始化配置 environment: MYSQL_ROOT_PASSWORD: root123456 # root 超级用户的密码,权力最大 MYSQL_DATABASE: myapp # 首次启动自动创建这个数据库,省得你手动建 MYSQL_USER: appuser # 创建一个普通用户,日常操作用这个,不用 root MYSQL_PASSWORD: apppass123 # 普通用户的密码 # ports 端口映射:把容器内部的端口"暴露"到宿主机 # 格式是 "宿主机端口:容器端口" # "3306:3306" 意思是:访问服务器的 3306 端口,就等于访问容器内的 3306 端口 ports: - "3306:3306" # volumes 目录挂载:把宿主机的目录"绑定"到容器内部 # 这是数据持久化的关键——默认情况下,容器删除后内部数据就没了 # 挂载后,数据实际存在宿主机上,容器删了重建,数据还在 volumes: # 数据库文件存到宿主机 ./data/mysql 目录 # 容器内 /var/lib/mysql 是 MySQL 存数据的地方,映射出来就不怕丢了 - ./data/mysql:/var/lib/mysql # 把初始化 SQL 脚本挂载进去 # MySQL 首次启动时会自动执行这个目录下的 .sql 文件,帮你建表、灌初始数据 - ./init.sql:/docker-entrypoint-initdb.d/init.sql # ---- Redis 缓存服务 ---- # Redis 是内存数据库,用来缓存热点数据,减轻数据库压力 # 就像你把常用文件放在桌面,不用每次都去文件柜翻 redis: image: redis:7-alpine # alpine 版本基于 Alpine Linux,体积只有正常版的 1/5 container_name: test-redis restart: always ports: - "6379:6379" # command 覆盖容器默认的启动命令 # 默认 Redis 启动是不设密码的,这里加上密码防止被人白嫖 command: redis-server --requirepass redis123 volumes: - ./data/redis:/data # 把 Redis 的持久化文件也挂出来,防止丢失 # ---- 应用服务(Spring Boot 后端)---- # 这是你的核心业务应用,处理用户的请求、执行业务逻辑 app: # build 表示不从仓库拉镜像,而是根据 Dockerfile 自己构建 # 就像不买现成的家具,而是按图纸自己打 build: context: . # Dockerfile 在当前目录下 dockerfile: Dockerfile # 指定 Dockerfile 文件名 container_name: test-app restart: always ports: - "8080:8080" # depends_on 定义启动顺序 # 意思是:先启动 mysql 和 redis,再启动 app # 不然应用启动时连不上数据库就报错了 # 注意:depends_on 只保证启动顺序,不保证 mysql 已经"准备好接受连接" # 如果应用启动太快连不上数据库,可以在应用里加重试逻辑 depends_on: - mysql - redis # environment 注入环境变量 # 这些变量会覆盖 Spring Boot 配置文件中的对应项 # 好处:不用改代码和配置文件,只需改环境变量就能切换数据库连接等配置 environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/myapp?useSSL=false&serverTimezone=Asia/Shanghai # 注意这里的 host 写的是 "mysql" 而不是 IP # 因为 Docker Compose 会自动创建一个内部网络,服务名就是域名 # 应用容器访问 "mysql:3306" 就能连到 MySQL 容器,不用管 IP SPRING_DATASOURCE_USERNAME: appuser SPRING_DATASOURCE_PASSWORD: apppass123 SPRING_REDIS_HOST: redis # 同理,"redis" 就是 Redis 容器的域名 SPRING_REDIS_PASSWORD: redis123 # ---- Nginx 反向代理 ---- # Nginx 是整个系统的"前台门面",用户只跟它打交道 # 它负责:1. 接收用户请求 2. 转发给后端应用 3. 返回结果给用户 # 这样用户只需要访问 80 端口,不用记住后端各种端口号 nginx: image: nginx:alpine container_name: test-nginx restart: always ports: - "80:80" # HTTP 端口,用户访问的就是这个 - "443:443" # HTTPS 端口,配置 SSL 后使用 volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro # :ro = read-only 只读挂载 # Nginx 配置文件挂进来,修改后 nginx -s reload 即可生效 # :ro 防止容器内的进程意外修改配置文件 depends_on: - app # 等应用启动后再启动 Nginx ``` > **关键概念说明**: > - **端口映射**(ports):容器是一个封闭的小世界,外部默认访问不到。端口映射就是在容器上"开一扇窗",让外部流量能进来。`"8080:8080"` 就是把容器的 8080 端口暴露到宿主机的 8080 端口。 > - **目录挂载**(volumes):容器内部的数据默认是临时的,容器一删就没了。挂载就是把宿主机的目录"绑定"到容器内,这样数据实际存在宿主机上,容器重建后数据还在。 > - **服务名即域名**:Docker Compose 自动创建内部网络,服务名就是域名。比如 `mysql:3306` 就能访问 MySQL 容器,不用写 IP。 ##### 应用 Dockerfile 详解 Dockerfile 是打包应用的"菜谱",告诉 Docker 怎么把你的源代码变成一个可运行的镜像。这里用的是**多阶段构建**——先在一个"厨房"里编译代码,再把编译好的"成品菜"端到另一个干净的"盘子"里。这样做的好处是最终镜像很小,不会把编译工具链也带进去。 ```dockerfile # ============ 阶段1:构建 ============ # 这个阶段就像一个"专用厨房",里面有 Maven 和 JDK,专门用来编译代码 # 构建完成后这个"厨房"就被丢弃了,不会出现在最终镜像里 FROM maven:3.9-eclipse-temurin-17 AS builder WORKDIR /app # 先复制 pom.xml 并下载依赖 # 这一步单独写,是为了利用 Docker 的缓存机制: # 只要 pom.xml 没变,下次构建就会跳过依赖下载,节省大量时间 COPY pom.xml . RUN mvn dependency:go-offline -B # 把所有依赖下载到本地缓存 # 再复制源代码并编译 COPY src ./src RUN mvn clean package -DskipTests -B # 打包成 JAR,跳过测试(测试在 CI 阶段已经跑过了) # ============ 阶段2:运行 ============ # 这是最终镜像,只包含运行应用所需的最小环境 # 用 JRE 而不是 JDK,因为运行时不需要编译器,体积小很多 FROM eclipse-temurin:17-jre-alpine WORKDIR /app # 安全最佳实践:在容器内创建一个普通用户来运行应用 # 默认容器以 root 用户运行,一旦被攻破,攻击者就拥有了 root 权限 # 用普通用户运行,即使被攻破,危害也有限 RUN addgroup -S appgroup && adduser -S appuser -G appgroup USER appuser # 从 builder 阶段把编译好的 JAR 包复制过来 # --from=builder 表示从名为 builder 的阶段复制 COPY --from=builder /app/target/*.jar app.jar # 健康检查:Docker 每 30 秒访问一次 /actuator/health 端点 # 连续 3 次失败就认为容器不健康,触发重启策略 # 就像定时给容器"量体温",发烧了就送医院 HEALTHCHECK --interval=30s --timeout=3s --retries=3 \ CMD wget -qO- http://localhost:8080/actuator/health || exit 1 # EXPOSE 声明容器对外提供服务的端口 # 这只是一个"文档说明",并不会实际发布端口,真正的端口映射在 docker-compose.yml 中 EXPOSE 8080 # ENTRYPOINT 容器启动时执行的命令 # 这里启动 Java 应用,配置了基本 JVM 参数 ENTRYPOINT ["java", \ "-Xms512m", "-Xmx1024m", \ # 初始/最大堆内存 "-Djava.security.egd=file:/dev/./urandom", \ # 加速 SecureRandom 初始化 "-jar", "app.jar"] ``` ##### 启动与运维 万事俱备,只欠东风。配置文件写好后,按照下面的流程图操作即可: ```mermaid graph TD A["编写 docker-compose.yml"] --> B["docker-compose up -d<br/>后台启动所有服务"] B --> C["docker-compose ps<br/>查看各容器运行状态"] C --> D{"服务是否正常?"} D -->|正常| E["docker-compose logs -f app<br/>实时查看应用日志"] D -->|异常| F["docker-compose logs mysql<br/>排查数据库日志"] F --> G["修改配置后<br/>docker-compose restart"] G --> C E --> H["测试完成<br/>docker-compose down<br/>停止并删除所有容器"] ``` | 命令 | 大白话解释 | |------|----------| | `docker-compose up -d` | "一键开火"——根据配置文件启动所有容器。`-d` 是后台运行的意思,不加的话当前终端会被占满,关掉终端服务就停了 | | `docker-compose ps` | "点名"——看看哪些容器在跑、哪些挂了,以及各自的端口映射 | | `docker-compose logs -f 服务名` | "监听"——实时查看某个服务的日志输出,`-f` 表示持续跟踪(跟 `tail -f` 一个意思),按 Ctrl+C 退出 | | `docker-compose restart 服务名` | "重启"——某个服务改了配置文件后,重启让新配置生效 | | `docker-compose down` | "收工"——停掉并删除所有容器和网络。**注意**:数据卷(volumes 挂载的目录)不会删,数据库数据还在 | | `docker-compose down -v` | "连根拔起"——连数据卷一起删!数据库数据永久丢失,慎用! | | `docker exec -it test-mysql bash` | "进入容器内部"——像远程登录一样进入 MySQL 容器的命令行,可以直接执行 SQL | --- #### 第二部分:生产级多节点集群部署 ##### 为什么需要多节点? 单节点部署有一个致命问题:**单点故障**。那台机器一旦宕机——不管是断电、硬盘坏了、还是网络中断——你的整个系统就瘫痪了。用户访问不了,订单下不了,数据查不到,损失可能按分钟计算。 生产环境必须做到**高可用**:多台机器协同工作,任何一台挂了,其他机器自动接管,用户完全感知不到。这就好比你开了一家店,只有一个收银员,他请假了店就得关门;但如果你有三个收银员,谁请假都不影响营业。 多节点集群要解决的问题有三个: 1. **应用层高可用**:部署多个应用实例,一个挂了其他还在 2. **数据库高可用**:主库挂了,从库自动升级为主库,数据不丢 3. **入口层高可用**:负载均衡器也要做双机热备,不能它自己成了单点 ##### 多节点集群架构 下面这张图展示了生产环境的完整集群架构。从上往下看:用户请求先到负载均衡层,再分发到应用集群,应用集群再去访问缓存和数据库。每一层都做了高可用设计,不存在单点故障。 ```mermaid graph TB subgraph "负载均衡层 — 系统大门" lb1["Keepalived + Nginx<br/>主负载均衡器<br/>192.168.1.10"] lb2["Keepalived + Nginx<br/>备负载均衡器<br/>192.168.1.11"] vip["虚拟 IP VIP<br/>192.168.1.100<br/>用户只访问这个 IP"] end subgraph "应用服务集群 — 干活的" app1["Node-1<br/>应用容器<br/>192.168.1.20"] app2["Node-2<br/>应用容器<br/>192.168.1.21"] app3["Node-3<br/>应用容器<br/>192.168.1.22"] end subgraph "缓存集群 — 记性好的" r1["Redis 主节点<br/>读写<br/>192.168.1.30"] r2["Redis 从节点<br/>只读备份<br/>192.168.1.31"] r3["Redis 从节点<br/>只读备份<br/>192.168.1.32"] sentinel["Redis Sentinel<br/>哨兵:监控主节点健康<br/>主节点挂了自动提拔从节点"] end subgraph "数据库集群 — 存档案的" m1["MySQL 主库<br/>读写<br/>192.168.1.40"] m2["MySQL 从库<br/>只读<br/>192.168.1.41"] m3["MySQL 从库<br/>只读<br/>192.168.1.42"] end subgraph "管理与监控 — 后勤保障" swarm["Swarm Manager<br/>集群总指挥"] monitor["Prometheus + Grafana<br/>监控面板 + 告警"] end user["用户请求"] --> vip vip -.->|"VIP 漂移"| lb1 vip -.->|"主挂了才切"| lb2 lb1 <-->|"VRRP 心跳检测<br/>互相确认对方还活着"| lb2 lb1 --> app1 lb1 --> app2 lb1 --> app3 app1 --> r1 app2 --> r1 app3 --> r1 r1 -->|"数据同步"| r2 r1 -->|"数据同步"| r3 sentinel -.->|"监控 & 故障转移"| r1 sentinel -.->|"监控"| r2 sentinel -.->|"监控"| r3 app1 --> m1 app2 --> m1 app3 --> m1 m1 -->|"主从复制"| m2 m1 -->|"主从复制"| m3 swarm -->|"调度管理"| app1 swarm -->|"调度管理"| app2 swarm -->|"调度管理"| app3 monitor -.->|"采集指标"| app1 monitor -.->|"采集指标"| app2 ``` ##### 集群组件职责说明(大白话版) | 组件 | 官方说法 | 大白话理解 | |------|----------|----------| | **Docker Swarm** | 容器集群编排工具 | 工地的**总调度**:决定哪个工人(节点)干什么活,有人请假自动找人顶上 | | **Keepalived + VIP** | 负载均衡高可用方案 | 公司的**总机号码**:对外只有一个号码(VIP),背后两台机器一主一备。主机正常时它接电话,主机挂了备机无缝接替,客户完全不知道换人了 | | **Nginx 集群** | 七层反向代理与负载均衡 | **前台接待员**:把来访的客户请求均匀分配给后台的工作人员(应用实例),谁闲就分给谁 | | **Redis 主从 + Sentinel** | 缓存高可用与自动故障转移 | **记性好的团队**:主节点负责记东西(读写),从节点抄一份备份。Sentinel 是"监工",盯梢主节点,一发现它倒下了立马提拔一个从节点上位 | | **MySQL 主从复制** | 数据库高可用与读写分离 | **档案室**:主库存原件(写),从库存复印件(只读)。写操作只找主库,读操作分摊给从库,既安全又快 | | **Prometheus + Grafana** | 指标采集、可视化与告警 | **体检中心**:Prometheus 定时给每台机器量体温、测血压(采集指标),Grafana 把数据画成图表,体温超标自动发短信告警 | ##### 第一步:初始化 Docker Swarm 集群 Docker Swarm 是 Docker 自带的集群管理工具,装了 Docker 就自带,不用额外安装。它的工作方式很简单:选一台机器当"管理者"(Manager),其他机器当"打工人"(Worker),管理者负责分配任务,打工人负责执行。 下面的时序图展示了从零搭建集群的完整过程: ```mermaid sequenceDiagram participant ops as 运维人员 participant mgr as Manager 节点<br/>192.168.1.10 participant w1 as Worker 节点1<br/>192.168.1.20 participant w2 as Worker 节点2<br/>192.168.1.21 ops->>mgr: docker swarm init --advertise-addr 192.168.1.10<br/>初始化集群,我当老大 mgr-->>ops: 返回加入命令和 Token<br/>(相当于入队通行证) ops->>w1: docker swarm join --token SWMTKN-xxx 192.168.1.10:2377<br/>拿着通行证加入集群 w1-->>mgr: 注册成功,我来干活了 ops->>w2: docker swarm join --token SWMTKN-xxx 192.168.1.10:2377<br/>你也加入 w2-->>mgr: 注册成功,我也来了 ops->>mgr: docker node ls<br/>看看队里都有谁 mgr-->>ops: 显示节点列表<br/>Manager: Ready<br/>Worker1: Ready<br/>Worker2: Ready ``` **命令详解(每条都讲清楚执行时机和效果):** | 命令 | 在哪台机器执行 | 大白话解释 | |------|----------|----------| | `docker swarm init --advertise-addr 192.168.1.10` | Manager 节点 | "我来当老大"——初始化集群。`--advertise-addr` 告诉其他节点"来找我报到"。执行后会输出一个 `docker swarm join` 命令,里面带着 Token,复制下来给其他机器用 | | `docker swarm join --token SWMTKN-xxx 192.168.1.10:2377` | Worker 节点 | "我来打工"——拿着通行证加入集群。2377 是 Swarm 管理通信的端口,Token 是安全凭证,防止随便什么机器都来加入 | | `docker node ls` | Manager 节点 | "点名"——查看所有节点的状态。能看到每个节点是 Manager 还是 Worker、是正常(Ready)还是掉线(Down) | | `docker node promote 节点名` | Manager 节点 | "提拔"——把 Worker 提升为 Manager,增加管理节点的冗余度。推荐至少 3 个 Manager 节点 | | `docker node update --availability drain 节点名` | Manager 节点 | "休假"——把节点标记为"排空"模式,上面的容器会自动迁移到其他节点。常用于给机器做维护升级 | ##### 第二步:部署服务栈 集群搭好了,接下来要把应用部署上去。这里用 Docker Compose 文件配合 Swarm 的 `deploy` 配置来实现多节点编排。和单节点的 `docker-compose.yml` 相比,最大的区别是多了 `deploy` 块——它告诉 Swarm 怎么在多台机器上分配容器。 ```yaml version: '3.8' services: app: image: registry.example.com/myapp:latest # 从私有镜像仓库拉取镜像 # deploy 块是 Swarm 模式专用配置 # 在普通 docker-compose up 中不生效,必须用 docker stack deploy 部署 deploy: # replicas 副本数量:同时运行几个应用实例 # 3 个副本分布在 3 台机器上,任何一台挂了,另外两台还能继续服务 replicas: 3 # update_config 滚动更新策略:怎么安全地更新到新版本 update_config: parallelism: 1 # 每次只更新 1 个副本,不会一口气全换 delay: 10s # 更新一个后等 10 秒,确认没问题再更新下一个 failure_action: rollback # 如果更新失败(新版本起不来),自动回滚到旧版本 # restart_policy 重启策略:容器挂了怎么处理 restart_policy: condition: on-failure # 只有异常退出才重启(正常退出不重启) max_attempts: 3 # 最多重试 3 次,避免无限重启(可能代码本身有问题) # placement 部署约束:容器应该放在哪种节点上 placement: constraints: - node.role == worker # 应用容器只部署在 Worker 节点上 # Manager 节点专注于管理工作,不跑业务,避免影响集群稳定性 environment: SPRING_PROFILES_ACTIVE: prod networks: - app-network nginx: image: nginx:alpine deploy: replicas: 2 # 2 个 Nginx 实例,互为备份 placement: constraints: - node.role == manager # Nginx 部署在 Manager 节点,作为统一入口 ports: - "80:80" - "443:443" configs: - source: nginx_config target: /etc/nginx/nginx.conf # configs 是 Swarm 专用的配置管理方式 # 比 volumes 更适合存储配置文件:可以版本化、可以滚动更新 configs: nginx_config: file: ./nginx-prod.conf # overlay 网络:允许不同机器上的容器互相通信 # 就像给分布在各楼层的员工配了对讲机,不用走公网 networks: app-network: driver: overlay ``` ##### 第三步:服务部署与运维命令 | 命令 | 大白话解释 | |------|----------| | `docker stack deploy -c docker-compose.yml myapp` | "全员上阵"——把配置文件中定义的所有服务部署到集群上。Swarm 会自动把容器分配到各个节点。`myapp` 是这个技术栈的名字,后续操作都靠它 | | `docker stack ls` | "看看部署了什么"——列出集群中所有的技术栈 | | `docker stack services myapp` | "看看各服务状态"——显示每个服务需要几个副本、当前跑了几个。如果 `REPLICAS` 显示 `3/3` 说明全在跑,`2/3` 说明有一个挂了 | | `docker stack ps myapp` | "细看每个副本"——显示每个副本具体跑在哪台机器上、运行了多久、是否正常 | | `docker service logs -f myapp_app` | "看聚合日志"——把分布在多台机器上的应用日志汇总显示,不用一台一台登录看 | | `docker service scale myapp_app=5` | "紧急加人"——把应用副本从 3 个扩到 5 个,应对流量突增。Swarm 自动在可用节点上启动新容器 | | `docker service update --image registry.example.com/myapp:v2.0 myapp_app` | "升级装备"——把应用镜像更新到 v2.0 版本。Swarm 会按 `update_config` 中的策略逐个替换,保证服务不中断 | | `docker stack rm myapp` | "收队"——移除整个技术栈,所有容器停止并删除 | ##### 第四步:多节点日常运维 集群部署不是一锤子买卖,日常巡检和及时处理问题同样重要。下面这张图展示了一个典型的日常运维流程: ```mermaid graph TD A["每日巡检<br/>docker node ls<br/>检查所有节点状态"] --> B{"所有节点正常?"} B -->|是| C["查看服务状态<br/>docker stack services myapp"] B -->|否| D["排查故障节点<br/>docker node inspect 节点名"] D --> E["必要时驱逐节点<br/>docker node update --availability drain<br/>让容器迁走,安心修机器"] E --> F["修复后重新加入<br/>docker node update --availability active"] F --> A C --> G["查看 Grafana 监控面板"] G --> H{"资源使用率超阈值?"} H -->|是| I["扩容<br/>docker service scale myapp_app=N<br/>增加副本来分担压力"] H -->|否| J["记录巡检日志<br/>一切正常,收工"] ``` **关键概念深入讲解**: **节点故障自动恢复**:当某个 Worker 节点宕机,Swarm 不需要你做任何事——它会自动发现该节点失联,然后在该节点上运行的容器迁移到其他健康节点。整个过程通常在几十秒内完成。这得益于 Swarm 的"期望状态协调"机制:你声明"我要 3 个应用副本",Swarm 会持续监控,发现只有 2 个在跑,就自动补上 1 个。就像你跟管家说"家里要常备 3 瓶牛奶",管家发现只剩 2 瓶了就自动去超市补货。 **滚动更新的原理**:更新时绝对不能把旧版本全停了再启动新版本(那叫"停机更新",用户会看到 502 错误)。滚动更新是逐个替换:先启动 1 个新版本容器 → 等健康检查通过 → 停掉 1 个旧版本容器 → 再启动第 2 个新版本容器……如此循环,直到全部替换完毕。用户在这个过程中完全感知不到服务中断。 **日志聚合**:在多节点集群中,同一个服务的 3 个副本可能分布在 3 台不同的机器上。`docker service logs` 会自动把所有副本的日志汇总到一起显示,不用你逐台 SSH 登录去看。 **监控告警**:推荐用 Prometheus 定时采集各节点的 CPU、内存、磁盘、网络指标,Grafana 以漂亮的图表展示。当某个指标超过阈值(比如内存使用率超过 90%),Alertmanager 会自动通过钉钉、邮件或短信发送告警。运维人员不需要一直盯着屏幕,有问题系统会主动通知你。 --- #### 测试环境 vs 生产环境对比总结 | 维度 | 测试环境单节点 | 生产环境多节点集群 | |------|---------------|-------------------| | 服务器数量 | 1 台 | 至少 3 台(推荐 5 台以上) | | 容器编排 | Docker Compose | Docker Swarm | | 高可用 | 无,单点故障 | 多副本自动故障转移 | | 数据持久化 | 本地目录挂载 | NFS/分布式存储 | | 负载均衡 | 无或单 Nginx | Nginx + Keepalived 双活 | | 数据库 | 单实例 | 主从复制 + 读写分离 | | 缓存 | 单 Redis | Redis 主从 + Sentinel | | 监控告警 | 手动查看日志 | Prometheus + Grafana 自动告警 | | 日志管理 | docker logs | ELK/Loki 集中日志平台 | | 适用场景 | 开发调试、功能验证 | 正式对外提供服务 | > **选型建议**:如果你的项目是内部系统、用户量不大,单节点就够用了,别过度设计。等流量上来了、业务重要了,再升级到多节点也不迟。架构是演出来的,不是一步到位的。 ### 2.5 JVM 调优参考 | 参数 | 说明 | 推荐值 | |------|------|--------| | `-Xms` | 初始堆内存 | 物理内存的 1/4 | | `-Xmx` | 最大堆内存 | 物理内存的 1/2,不超过 4G | | `-XX:+UseG1GC` | 使用 G1 垃圾回收器 | JDK 9+ 默认,低延迟场景推荐 | | `-XX:MaxRAMPercentage=75.0` | 容器内按比例分配堆内存 | Docker 环境推荐 | | `-XX:+HeapDumpOnOutOfMemoryError` | OOM 时自动 dump 堆 | **生产必开**,排查内存问题 | | `-XX:HeapDumpPath` | dump 文件路径 | 指向 logs 目录 | ```bash # 脚本部署中的推荐 JVM 参数(已在 app.sh 中配置) JVM_OPTS="-Xms512m -Xmx1024m" JVM_OPTS="$JVM_OPTS -XX:+UseG1GC" JVM_OPTS="$JVM_OPTS -XX:+HeapDumpOnOutOfMemoryError" JVM_OPTS="$JVM_OPTS -XX:HeapDumpPath=$LOG_DIR/heapdump.hprof" ``` ```bash # Docker 环境下的推荐启动参数 java -XX:MaxRAMPercentage=75.0 \ -XX:+UseG1GC \ -XX:+HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath=/app/logs/heapdump.hprof \ -jar app.jar ``` --- ## 三、Python(FastAPI / Flask)项目部署 ![slide_04.png](https://pic.code-nav.cn/post_picture/1609766978631761921/08Tcql7cu7WamiVn.webp) ### 先说大白话:Python 项目部署和 Java 有什么不同? 很多学 Java 出身的同学第一次部署 Python 项目会懵:**"Python 没有 `main` 方法、没有内置 Web 服务器,我到底在跑什么?"** 先把这个困惑讲清楚 —— **Java Spring Boot 自带 Tomcat**,打个 JAR 包直接 `java -jar` 就能跑;但 **Python 不是这样**,它是解释型语言,没有"自带服务器"这个概念。要跑一个 HTTP API 服务,你需要自己搭一个 Web 服务器来"托管"你的 Python 代码。 这就引出了一整套工具和概念,我们用一张图来讲清楚: ```mermaid graph LR A["用户请求"] --> B["Nginx<br/>反向代理"] B --> C["Gunicorn / Uvicorn<br/>WSGI / ASGI 服务器"] C --> D["FastAPI / Flask<br/>你的 Python 应用代码"] D --> E["数据库 / Redis"] ``` **各层大白话解释**: | 层级 | 是什么 | 大白话理解 | 为什么需要 | |------|--------|------------|----------| | **Nginx** | HTTP 反向代理服务器 | "前台接待员"——负责接客、分发请求、返回结果 | 直接把 Python 服务暴露到公网不安全,Nginx 做了一层缓冲和统一入口 | | **Gunicorn** | WSGI HTTP 服务器 | "包工头"——负责启动和管理多个 Python 工作进程(Worker) | Python 代码自己跑不稳,需要个"包工头"来管理进程,挂了自动重启 | | **Uvicorn** | ASGI 服务器(FastAPI 专用)| "技术工"——真正执行你的 async/await 代码的服务器 | FastAPI 基于 async/await,必须用支持 ASGI 的服务器才能发挥性能 | | **FastAPI/Flask** | 你的业务代码 | "干活的程序员"——写接口、写业务逻辑的地方 | 这是你真正要部署的东西 | > **一句话总结**:部署 Python 项目 = 让你的代码跑在一个"包工头(Gunicorn)"管理下的"技术工(Uvicorn/Flask)"进程里,前面再挡一个"前台(Nginx)"。 --- ### 3.1 项目结构示例 一个标准的 Python API 项目长这样,每个目录和文件都有明确分工: ``` my-python-api/ ├── app/ │ ├── __init__.py # 包初始化文件(Python 包必须有这个) │ ├── main.py # 应用入口:FastAPI() 或 Flask() 实例创建的地方 │ ├── routers/ # 路由模块(接口定义放这里) │ │ └── user.py │ ├── models/ # 数据模型(Pydantic / SQLAlchemy 模型) │ │ └── user.py │ └── dependencies.py # 依赖注入(认证、数据库会话等) ├── requirements.txt # 依赖清单(pip install -r 安装的包列表) ├── gunicorn.conf.py # Gunicorn 配置文件(Worker 数、超时等) ├── Dockerfile # Docker 构建文件 └── .env # 环境变量(数据库密码、API Key 等,不要提交到 Git!) ``` **为什么要这样组织?** - `app/` 是一个 Python 包,所有业务代码都在里面 - `requirements.txt` 是 Python 项目的"购物清单"——列出所有需要安装的第三方库(类似 Java 的 `pom.xml`) - `gunicorn.conf.py` 控制 Gunicorn 怎么跑你的应用:几个进程、超时多久、日志在哪 - `.env` 存敏感信息,通过 `python-dotenv` 库加载,**切记加入 `.gitignore`**,不要提交到代码仓库 --- ### 3.2 方式一:虚拟环境直接部署 "虚拟环境"是 Python 的生态特色——它解决了"全局 Python 被不同项目互相污染"的问题。比如项目 A 需要 `requests==2.28`,项目 B 需要 `requests==2.31`,装在一起会冲突。虚拟环境就是给每个项目建一个**独立的 Python 小房间**,互不干扰。 #### 部署步骤详解 跟着下面这张流程图操作,每一步都有大白话解释: ```mermaid graph TD A["第一步:上传代码<br/>scp / git clone"] --> B["第二步:创建虚拟环境<br/>python3 -m venv venv"] B --> C["第三步:激活虚拟环境<br/>source venv/bin/activate"] C --> D["第四步:安装依赖<br/>pip install -r requirements.txt"] D --> E["第五步:测试启动<br/>看有没有报错"] E --> F{"启动成功?"} F -->|是| G["第六步:用 Gunicorn 启动<br/>正式跑起来"] F -->|否| H["排查错误<br/>看报错信息"] H --> D G --> I["第七步:配置 systemd<br/>开机自启 + 崩溃重启"] ``` **第一步:上传代码到服务器** ```bash # 方式 A:从 Git 仓库拉取(推荐,版本可控) git clone https://github.com/yourname/my-python-api.git /opt/myapi cd /opt/myapi git checkout prod # 切换到生产分支 # 方式 B:用 scp 从本地上传(适合没用 Git 的项目) # 在本地机器执行: scp -r ./my-python-api user@your-server:/opt/myapi ``` **第二步:创建虚拟环境** ```bash cd /opt/myapi # venv 是 Python 内置的虚拟环境工具,会在当前目录创建一个 venv/ 文件夹 # 里面是一个独立的 Python 解释器 + pip,跟系统 Python 完全隔离 python3 -m venv venv # 验证:激活后命令行提示符前面会出现 (venv) 字样 source venv/bin/activate which python # 应该显示 /opt/myapi/venv/bin/python(不是 /usr/bin/python) ``` > **大白话**:`venv` 就像给这个项目单独配了一台"虚拟机",里面的 Python 是独立的,跟系统 Python 各装各的包,互不干扰。 **第三步:安装依赖** ```bash # 激活虚拟环境后,pip 会自动把包装到 venv/ 里(不是装到系统) source venv/bin/activate pip install -r requirements.txt # 安装完成后,确认关键包都在 pip list | grep -E "fastapi|flask|gunicorn|uvicorn" ``` > **注意**:FastAPI 生产环境需要额外装 `gunicorn` 和 `uvicorn[standard]` > ```bash > pip install gunicorn "uvicorn[standard]" > ``` **第四步:先测试启动,确认能跑起来** ```bash # ---------- FastAPI 项目 ---------- # uvicorn 是 FastAPI 的开发服务器,先用它测一下能不能跑 # --reload 表示代码改了自动重启(仅开发用,生产不要用!) uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload # 如果看到 "Uvicorn running on http://0.0.0.0:8000" 说明成功了 # 按 Ctrl+C 停掉,准备用 Gunicorn 正式跑 # ---------- Flask 项目 ---------- # Flask 内置的开发服务器(app.run)不能用于生产!只用来测试 flask --app app.main run --host 0.0.0.0 --port 8000 ``` > **为什么开发服务器不能用于生产?** 因为 `uvicorn --reload` 或 `flask run` 是单线程、单进程的,只能同时处理一个请求,并发能力几乎为零,而且没有进程守护,挂了就是真挂了。 **第五步:用 Gunicorn 正式启动(生产方式)** 现在要用 Gunicorn("包工头")来管理你的应用进程了: ```bash # ========== FastAPI 项目 ========== gunicorn app.main:app \ -w 4 \ # 启动 4 个 Worker 进程(具体几个看下面说明) -k uvicorn.workers.UvicornWorker \ # Worker 类型:FastAPI 必须用 UvicornWorker --bind 0.0.0.0:8000 \ # 监听所有网卡的 8000 端口 --timeout 120 \ # Worker 处理请求超过 120 秒就被强制杀掉重启 --access-logfile - \ # 访问日志输出到 stdout(Docker 场景推荐) --error-logfile - # 错误日志输出到 stdout # ========== Flask 项目 ========== gunicorn app.main:app \ -w 4 \ # 4 个 Worker 进程 -k gthread \ # Worker 类型:Flask 推荐用线程模式 --bind 0.0.0.0:8000 \ --timeout 120 ``` **`-w 4` 是什么意思?Worker 数量怎么定?** 这是最容易困惑的地方,用大白话讲:`gunicorn` 启动后是这样的: ``` Gunicorn(主进程,包工头) ├── Worker 1(干活的程序员) ├── Worker 2(干活的程序员) ├── Worker 3(干活的程序员) └── Worker 4(干活的程序员) ``` 每个 Worker 是一个独立的 Python 进程,能同时处理请求。**Worker 越多 = 并发能力越强**,但也不是越多越好(内存有限)。 **推荐公式**:`Worker 数 = (2 × CPU核心数) + 1` 比如你的服务器是 2 核 CPU,就设 `2 × 2 + 1 = 5` 个 Worker。4 核就设 9 个。 #### Gunicorn 配置文件(推荐用文件代替命令行) 每次启动敲一大串参数太麻烦,也容易出错。推荐把配置写入 `gunicorn.conf.py` 文件: ```python # gunicorn.conf.py # Gunicorn 会自动读取当前目录下的这个文件,不需要在命令行指定 -c import multiprocessing import os # ==================== 核心配置 ==================== # Worker 数量:公式 (2 × CPU核心数) + 1 # multiprocessing.cpu_count() 自动获取 CPU 核心数,不用硬编码 workers = multiprocessing.cpu_count() * 2 + 1 # 每个 Worker 的线程数(Flask 的 gthread 模式会用到,FastAPI 一般设 1) # FastAPI 是 async 的,一个进程就能处理大量并发,不需要多线程 # Flask 是同步的,一个请求占用一个线程,所以线程数可以设大一点(如 4) threads = 1 # FastAPI 用 1;Flask 用 4 # Worker 类型(非常重要!决定了你的应用怎么运行) # FastAPI → 必须用 "uvicorn.workers.UvicornWorker"(支持 async/await) # Flask → 推荐用 "gthread"(多线程模式,能更好利用 CPU) worker_class = "uvicorn.workers.UvicornWorker" # FastAPI 用这行 # worker_class = "gthread" # Flask 用这行(取消注释,注释掉上一行) # ==================== 网络配置 ==================== # 绑定地址:0.0.0.0 表示监听所有网卡(外网能访问) # 127.0.0.1 表示只监听本机(外网访问不了,一般不用) bind = "0.0.0.0:8000" # ==================== 超时配置 ==================== # Worker 处理请求的超时时间(秒) # 超过这个时间 Worker 还没处理完,Gunicorn 会强制 KILL 掉这个 Worker 并重启 # 设置太短:正常请求被误杀;设置太长:慢请求卡住 Worker 不释放 # 一般 API 接口 30-60 秒,有文件处理/导出的可以设 120-300 秒 timeout = 120 # 优雅重启超时(秒) # 当你 reload Gunicorn 时,旧 Worker 有这么多秒时间处理完手上的请求再退出 # 设为 0 表示立即强制退出(可能丢请求);设为 30 表示给 30 秒优雅收尾 graceful_timeout = 30 # ==================== 内存保护 ==================== # 每个 Worker 处理满 N 个请求后,自动重启自己 # 目的:防止代码里内存泄漏累积(Python 的 GC 不是万能的) # 比如设为 5000:每处理 5000 个请求,这个 Worker 就"自杀"重启,释放内存 max_requests = 5000 max_requests_jitter = 500 # 加一点随机抖动,避免所有 Worker 同时重启 # ==================== 日志配置 ==================== # 访问日志:记录每个请求的 URL、状态码、耗时 # "-" 表示输出到 stdout(Docker / systemd 场景推荐,日志由 Docker/systemd 统一管理) # 也可以写成文件路径:"/var/log/myapi/access.log" accesslog = "-" # 错误日志:记录异常、报错信息 errorlog = "-" # 日志级别:debug / info / warning / error / critical # 生产环境推荐 info 或 warning;debug 会打印太多日志影响性能 loglevel = "info" # ==================== 应用配置 ==================== # 预加载应用(True = 在 Worker fork 之前就把应用代码加载好) # 好处:多个 Worker 共享同一份代码内存,节省内存 # 坏处:代码里的全局变量在 fork 后不共享(每个 Worker 有自己的副本) # 一般设为 True;如果你的应用有全局状态且依赖共享内存,设为 False preload_app = True # 进程名称(方便用 ps 命令识别) proc_name = "myapi-gunicorn" ``` #### 配置 systemd 服务(开机自启 + 崩溃重启) 直接 `gunicorn ...` 启动的进程,服务器重启后就停了,而且进程挂了不会自动重启。用 `systemd` 来管理就能解决这两个问题: ```ini # /etc/systemd/system/myapi.service # systemd 是 Linux 的系统服务管理器,用来管理开机自启和进程守护 [Unit] Description=My Python API Service # After=network.target 表示:等网络服务就绪后再启动本服务(不然端口绑定会失败) After=network.target [Service] Type=notify # User / Group:用普通用户运行,不要用 root(安全!) User=www-data Group=www-data # WorkingDirectory:进程的工作目录(gunicorn.conf.py 要放在这个目录下) WorkingDirectory=/opt/myapi # Environment:设置环境变量,重点是让 systemd 知道去哪找 venv 里的 python/gunicorn Environment="PATH=/opt/myapi/venv/bin" # 如果有 .env 文件,可以通过 EnvironmentFile 加载: # EnvironmentFile=/opt/myapi/.env # ExecStart:启动命令(不需要 -c gunicorn.conf.py,因为 Gunicorn 会自动找同目录下的) ExecStart=/opt/myapi/venv/bin/gunicorn app.main:app # Restart=always:进程挂了就自动重启(always = 任何原因退出都重启) # RestartSec=5:重启前等 5 秒(避免频繁重启刷日志) Restart=always RestartSec=5 # 限制资源(防止内存泄漏拖垮整台服务器) # MemoryMax=1G:这个服务最多用 1GB 内存,超了会被系统杀掉 MemoryMax=1G [Install] # WantedBy=multi-user.target:多用户模式下自动启动(即正常的服务器运行级别) WantedBy=multi-user.target ``` 启用并启动服务: ```bash # 重新加载 systemd 配置(每次改了 .service 文件都要执行) sudo systemctl daemon-reload # 启动服务 sudo systemctl start myapi # 设置开机自启 sudo systemctl enable myapi # 查看状态(看是否 Active (running)) sudo systemctl status myapi # 查看日志(代替 tail -f /var/log/xxx.log) sudo journalctl -u myapi -f ``` --- ### 3.3 方式二:Docker 容器化部署(推荐) 如果你的团队已经用 Docker 管理 Java/前端项目,Python 项目也用 Docker 部署是最省心的——环境完全一致,不会再有"我本地能跑"的问题。 #### FastAPI 项目的 Dockerfile(逐行大白话注释) ```dockerfile # ==================== 阶段 1:安装依赖("厨房"阶段)==================== # 用 python:3.12-slim 作为构建环境 # slim 版本比 full 版本体积小很多(约 50MB vs 800MB),生产推荐 FROM python:3.12-slim AS builder # WORKDIR:设置工作目录为 /app(类似 cd /app,后面的命令都在这个目录下执行) WORKDIR /app # 先只复制 requirements.txt,再执行 pip install # 目的:利用 Docker 的缓存机制 # requirements.txt 不常变,这样依赖装好后,下次构建会直接用缓存,不用重新下载 COPY requirements.txt . # --no-cache-dir:不缓存下载的安装包(减小镜像体积) # --prefix=/install:把包装到 /install 目录下(方便下一阶段复制) RUN pip install --no-cache-dir --prefix=/install -r requirements.txt # ==================== 阶段 2:运行("上菜"阶段)==================== # 这个阶段是最终镜像,只保留运行所需的最小文件 FROM python:3.12-slim WORKDIR /app # 安全:创建非 root 用户来运行应用 # 默认用 root 跑容器,一旦被攻破,攻击者就有 root 权限,很危险 RUN groupadd -r appgroup && useradd -r -g appgroup appuser # 从 builder 阶段把安装好的依赖复制过来 # /install 目录下的所有文件会被复制到新镜像的 /usr/local 下 COPY --from=builder /install /usr/local # 复制应用代码到容器 COPY . . # 切换到普通用户(安全最佳实践) USER appuser # 健康检查:Docker 每隔一段时间访问一次 /health 接口 # 连续失败 3 次,Docker 就认为这个容器"不健康",触发重启策略 HEALTHCHECK --interval=30s --timeout=3s --retries=3 \ CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" # 声明容器对外暴露 8000 端口(文档作用,实际映射在 docker run -p 时指定) EXPOSE 8000 # 容器启动命令:用 Gunicorn 跑应用 # -w 4:4 个 Worker(容器内 CPU 核心数就是容器被分配的核数,可以用 os.cpu_count() 获取) # --bind 0.0.0.0:监听所有网卡(容器内必须写 0.0.0.0,写 127.0.0.1 外网访问不了) CMD ["gunicorn", "app.main:app", \ "-k", "uvicorn.workers.UvicornWorker", \ "-w", "4", \ "--bind", "0.0.0.0:8000", \ "--timeout", "120"] ``` #### Flask 项目的 Dockerfile(只改 CMD) Flask 项目唯一区别是 Worker 类型不同,Gunicorn 用 `gthread` 模式: ```dockerfile # ... 前面的 FROM / WORKDIR / COPY 都和 FastAPI 完全一样 ... CMD ["gunicorn", "app.main:app", \ "-w", "4", \ "--bind", "0.0.0.0:8000", \ "--timeout", "120"] # 注意:Flask 不需要 -k 参数,默认就是 sync worker # 如果想用多线程模式,加:-k gthread -t 4 ``` #### 构建与运行 ```bash # 构建镜像(在项目根目录执行,确保 Dockerfile 在当前目录) docker build -t myapi:latest . # 运行容器 docker run -d \ --name myapi \ -p 8000:8000 \ # 注入环境变量(覆盖代码中的配置,比改代码灵活) -e DATABASE_URL=postgresql://user:pass@db:5432/mydb \ -e REDIS_URL=redis://redis:6379/0 \ -e TZ=Asia/Shanghai \ # 挂载 .env 文件(适合不方便用 -e 注入的大量环境变量) -v /opt/myapi/.env:/app/.env:ro \ # 自动重启策略:除非手动停止,否则一直重启 --restart unless-stopped \ myapi:latest # 查看日志 docker logs -f myapi # 进入容器内部调试 docker exec -it myapi bash ``` --- ## 四、前端(Vue / React)项目部署 ![slide_05.png](https://pic.code-nav.cn/post_picture/1609766978631761921/UzFMDQj1VsCjduC8.webp) ### 4.1 核心认知 > **前端部署的本质**:把源码构建成静态文件(HTML/CSS/JS),然后交给 Web 服务器(Nginx)对外提供服务。前端项目本身不需要运行时环境,Nginx 托管静态文件即可。 ### 4.2 项目构建 ```bash # ---- Vue 项目(Vite 构建)---- npm install npm run build # 产物在 dist/ 目录 # ---- React 项目(Vite / CRA)---- npm install npm run build # 产物在 build/ 或 dist/ 目录 ``` 构建完成后,核心产物是 `index.html` + JS/CSS 静态资源文件。 ### 4.3 方式一:Nginx 直接托管(最常用) #### 前端打包后到底生成了什么? 很多小白搞不清楚"构建"到底做了什么。用大白话来说: > **构建 = 把你能看懂的 Vue/React 源码,翻译成浏览器能直接运行的 HTML + CSS + JS 文件** 构建完成后,`dist/` 文件夹里就是这些"翻译好"的文件: ``` dist/ ├── index.html ← 入口页面,浏览器第一个加载的文件 ├── assets/ │ ├── index-abc123.js ← 你的业务代码(登录、下单等功能),带 hash 防缓存 │ ├── vendor-def.js ← 第三方库(Vue、React 等) │ └── style-xyz789.css ← 样式文件 └── favicon.ico ``` **核心认知**:Nginx 的工作就是"把这些文件发给浏览器",它不需要懂 Vue 或 React,它只负责"递文件"。 --- #### 第一步:把 dist 上传到服务器,放在哪里? 推荐放在 `/var/www/项目名/` 或 `/opt/项目名/frontend/`,两个方案对比: | 方案 | 路径示例 | 适用场景 | 优点 | |------|---------|---------|------| | `/var/www/` | `/var/www/myapp/` | 纯前端项目,Nginx 直接托管 | 符合 Linux 规范,权限清晰 | | `/opt/项目名/` | `/opt/myapp/frontend/dist/` | 前后端在同一台机器 | 前后端文件在一起,方便管理 | **推荐用 `/var/www/` 方案**,命令如下: ```bash # 在本地构建(你的电脑上) npm run build # 构建完成后,dist/ 目录就生成了 # 把 dist/ 里的所有文件上传到服务器的 /var/www/myapp/ # scp 是 SSH 文件传输命令,把本地文件复制到远程服务器 scp -r dist/* user@your-server-ip:/var/www/myapp/ # ---- 或者,在服务器上直接构建(推荐) ---- ssh user@your-server-ip cd /var/www/myapp git pull origin main # 拉最新代码 npm install # 安装依赖 npm run build # 构建,产物在 dist/ ``` > **小贴士**:如果 `npm run build` 时提示 `npm: command not found`,说明服务器上还没装 Node.js,参考"一、部署前置"的 1.2 节安装。 --- #### 第二步:Nginx 配置托管——逐行大白话解释 Nginx 要做的只有一件事:**当用户访问你的网站时,把 `dist/` 里的文件发给浏览器**。 下面是完整配置,每一行都配有"大白话解释": ```nginx # /etc/nginx/conf.d/myapp.conf # 这是 Nginx 的"虚拟主机"配置文件 # 一个文件对应一个网站,可以有多个网站同时跑在 80 端口 server { # listen 80:监听 80 端口(HTTP 默认端口) # 用户访问 http://你的域名 或 http://服务器IP,Nginx 就会收到请求 listen 80; # server_name:这个配置响应哪个域名的请求 # 比如 server_name www.example.com,只有访问 www.example.com 才会走这个配置 # 如果写 _(下划线),表示"匹配所有域名",适合临时测试 server_name www.example.com; # root:指定"网站根目录" # Nginx 收到请求后,会去这个目录下找文件 # 比如用户访问 /logo.png,Nginx 就去 /var/www/myapp/logo.png 找 root /var/www/myapp; # index:指定"默认首页" # 用户访问 / 时,Nginx 自动返回 index.html index index.html; # ---- 最关键配置:SPA 路由支持 ---- # 问题:Vue/React 是单页应用,路由是前端控制的 # 比如用户直接访问 /user/123,Nginx 会去找 /var/www/myapp/user/123 这个文件 # 但这个文件不存在!因为是前端路由,实际只有一个 index.html # # try_files 的作用: # 1. 先找 $uri(比如 /user/123 这个文件)——找不到 # 2. 再找 $uri/(比如 /user/123/ 目录)——找不到 # 3. 最后返回 /index.html(交给前端路由处理) # # 这一步是前端部署最容易出错的地方!配错了就会出现"刷新页面 404" location / { try_files $uri $uri/ /index.html; } # ---- 静态资源缓存策略 ---- # 带 hash 的 JS/CSS 文件(比如 index-abc123.js)内容不会变 # 浏览器可以缓存 1 年,下次直接读本地,不用再下载 location /assets/ { # expires 1y:告诉浏览器"这个文件 1 年内不用再来问我有没有更新" expires 1y; # Cache-Control "public, immutable":公开缓存,且内容不可变(因为有 hash) add_header Cache-Control "public, immutable"; } # ---- index.html 不缓存 ---- # index.html 是入口文件,每次发版都可能变 # 必须让浏览器每次都来问服务器"有没有新版本" location = /index.html { # expires -1:过期时间是"过去"(立即过期),浏览器每次都重新请求 expires -1; add_header Cache-Control "no-cache, no-store, must-revalidate"; } # ---- Gzip 压缩 ---- # 把 JS/CSS 文件压缩后再发给浏览器,传输速度快 3-5 倍 gzip on; # gzip_types:指定哪些类型的文件需要压缩 # text/plain text/css application/javascript 是前端最常用的 gzip_types text/plain text/css application/json application/javascript text/xml; # gzip_min_length:小于 1024 字节的文件不压缩(压缩收益太低) gzip_min_length 1024; # ---- 安全头(可选但推荐)---- # 防止被嵌入 iframe 钓鱼 add_header X-Frame-Options "SAMEORIGIN" always; # 防止浏览器猜测 MIME 类型(安全加固) add_header X-Content-Type-Options "nosniff" always; } ``` **配置文件写完后,让 Nginx 重新加载配置:** ```bash # 先检查配置文件语法是否正确(很重要!写错了 Nginx 会启动失败) sudo nginx -t # 语法 OK 后,平滑重载配置(不会中断正在处理的请求) sudo nginx -s reload ``` --- #### 部署后验证 Checklist ```mermaid graph TD A["部署完成"] --> B{"浏览器访问<br/>http://服务器IP"} B -->|能看到页面| C{"点击页面内的链接<br/>能正常跳转?"} B -->|白屏/404| D["检查 Nginx 错误日志<br/>sudo tail -f /var/log/nginx/error.log"] C -->|能跳转| E{"刷新浏览器<br/>页面是否正常?"} C -->|404| F["检查 try_files 配置<br/>是不是漏写了 /index.html"] E -->|正常| G["✅ 部署成功!"] E -->|404| H["检查 Nginx 配置<br/>try_files 是否正确"] D --> I["根据错误日志排查"] ``` | 现象 | 原因 | 解决办法 | |------|------|----------| | 访问 IP 白屏 | Nginx 没启动 / root 路径写错 | `sudo systemctl status nginx` 检查状态 | | 刷新页面 404 | `try_files` 配置缺失或写错 | 确认 `location /` 块里有 `try_files $uri $uri/ /index.html;` | | 样式错乱/JS 报错 | `dist/` 上传不完整 | 重新 `scp -r dist/*` 上传,或服务器上重新 `npm run build` | | 能访问但很慢 | 没开 Gzip / 图片没压缩 | 检查 `gzip on;` 是否配置,用浏览器 DevTools Network 面板看传输大小 | --- ### 4.4 方式二:Docker 容器化部署(推荐生产环境) #### 这种方式本质是什么? > **用 Docker 跑一个 Nginx 容器,把前端打包好的 HTML/CSS/JS 文件"塞"进去,让容器里的 Nginx 对外提供服务。** 和"方式一"的核心区别: - **方式一**:Nginx 直接装在服务器上,dist 文件放在服务器的目录里 - **方式二**:Nginx 跑在 Docker 容器里,dist 文件要么"打包进镜像",要么"挂载到容器里" 两种子方案对比: | 子方案 | 做法 | 适用场景 | |--------|------|----------| | **A. 打包进镜像**(推荐) | `dist/` 在构建镜像时就复制进去 | CI/CD 自动化部署,版本可追溯 | | **B. 挂载数据卷** | 容器启动后,把宿主机 `dist/` 挂载到容器里 | 快速调试,改了代码不用重新构建镜像 | --- #### 子方案 A:打包进镜像(生产推荐) **完整 Dockerfile(逐行大白话注释):** ```dockerfile # ============ 阶段1:构建前端代码 ============ # 用一个装好 Node.js 的"厨房"来构建项目 FROM node:20-alpine AS builder WORKDIR /app # 先复制 package*.json,再 npm ci # 目的:利用 Docker 缓存,package.json 没变就不重新下载依赖 COPY package*.json ./ RUN npm ci --registry=https://registry.npmmirror.com # 复制所有源代码,然后构建 COPY . . RUN npm run build # 构建完成后,产物在 /app/dist 目录 # ============ 阶段2:用 Nginx 托管 ============ # 换一个"干净盘子":只装 Nginx,不装 Node.js(镜像更小) FROM nginx:1.25-alpine # 删除 Nginx 默认配置(我们不想要那个 "Welcome to nginx!" 页面) RUN rm /etc/nginx/conf.d/default.conf # 把我们写好的 Nginx 配置文件复制进去 # nginx.conf 需要和 Dockerfile 在同一个目录 COPY nginx.conf /etc/nginx/conf.d/default.conf # 把阶段1构建好的 dist/ 复制进 Nginx 的默认网站目录 # /usr/share/nginx/html 是 Nginx 容器的默认 root 路径 COPY --from=builder /app/dist /usr/share/nginx/html # 暴露 80 端口(这只是"文档说明",真正映射端口在 docker run 时指定) EXPOSE 80 # 启动 Nginx(daemon off 表示"前台运行",Docker 容器需要一个前台进程才不会退出) CMD ["nginx", "-g", "daemon off;"] ``` **配套的 Nginx 配置文件(`nginx.conf`):** ```nginx # 这个文件和"方式一"的 Nginx 配置几乎一样 # 唯一的区别:root 路径变成了容器内的 /usr/share/nginx/html server { listen 80; server_name _; # _ 表示匹配所有域名(容器内不需要特定域名) root /usr/share/nginx/html; index index.html; # SPA 路由支持(和方式一完全一样,这个必须配!) location / { try_files $uri $uri/ /index.html; } # 静态资源长缓存 location /assets/ { expires 1y; add_header Cache-Control "public, immutable"; } # index.html 不缓存 location = /index.html { add_header Cache-Control "no-cache, no-store, must-revalidate"; } # Gzip 压缩 gzip on; gzip_types text/plain text/css application/json application/javascript text/xml; gzip_min_length 1024; } ``` **构建和运行命令:** ```bash # 构建镜像(在 Dockerfile 所在目录执行) docker build -t myapp-web:v1.0.0 . # 运行容器 docker run -d \ --name myapp-web \ -p 80:80 \ # 把容器的 80 端口映射到宿主机的 80 端口 --restart unless-stopped \ # 容器挂了自动重启 myapp-web:v1.0.0 # 查看日志(确认 Nginx 正常启动) docker logs -f myapp-web ``` --- #### 子方案 B:挂载数据卷(调试/快速迭代用) **场景**:你在调试前端样式,每次改一点都要重新 `docker build`,太慢了! **解决**:把宿主机的 `dist/` 目录"挂载"到容器里,改完代码重新构建后,刷新浏览器就能看到效果,不用重建镜像。 **目录结构规划:** ``` /opt/myapp/frontend/ ← 前端项目根目录 ├── dist/ ← 构建产物(npm run build 生成) ├── nginx.conf ← Nginx 配置文件 └── docker-compose.yml ← 容器编排文件 ``` **docker-compose.yml(关键配置说明):** ```yaml version: '3.8' services: frontend: image: nginx:1.25-alpine container_name: myapp-frontend restart: always # ports:端口映射,格式"宿主机端口:容器端口" # 访问 http://服务器IP:8080 就能看到前端页面 ports: - "8080:80" # volumes(数据卷挂载):这是本方案的核心! # 格式:"宿主机路径:容器路径[:权限]" volumes: # 挂载1:把宿主机 dist/ 挂到 Nginx 的网站目录 # 这样 dist/ 里的文件变了,容器里立刻生效(不用重启容器) - ./dist:/usr/share/nginx/html:ro # ↑ :ro = read-only 只读 # 防止容器内的进程意外修改你的文件 # 挂载2:把宿主机的 nginx.conf 挂进去 # 这样你改了 nginx.conf,执行 nginx -s reload 就能生效 - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro # command:容器启动后执行的命令 # nginx -g "daemon off;" 是前台运行(Docker 容器必须前台运行) command: nginx -g "daemon off;" ``` **完整工作流(大白话版):** ```mermaid graph LR A["你在本地改代码<br/>保存文件"] --> B["npm run build<br/>重新构建"] B --> C["把 dist/ 同步到服务器<br/>scp -r dist/* user@ip:/opt/myapp/frontend/dist/"] C --> D["刷新浏览器<br/>立刻看到最新效果"] D -->|"Nginx 配置要改?"| E["修改服务器上的<br/>/opt/myapp/frontend/nginx.conf"] E --> F["docker exec myapp-frontend<br/>nginx -s reload"] F --> D ``` **关键命令速查:** | 命令 | 大白话解释 | |------|----------| | `docker-compose up -d` | "启动容器",`-d` 是后台运行 | | `docker-compose down` | "停掉并删除容器",卷挂载的数据不会丢 | | `docker exec myapp-frontend nginx -s reload` | "让 Nginx 重新加载配置",修改 nginx.conf 后必执行 | | `docker logs -f myapp-frontend` | "看容器日志",Nginx 报错信息在这里 | --- #### 两种子方案怎么选? ``` 你在做 ↓ 开发调试 / 快速迭代 │ ▼ 用【子方案 B:挂载数据卷】 → 改代码 → build → 刷新浏览器,秒级见效 → 改 Nginx 配置 → docker exec ... nginx -s reload,秒级见效 │ 发版上线 / CI/CD 自动化 │ ▼ 用【子方案 A:打包进镜像】 → docker build 构建镜像 → docker push 推到镜像仓库 → 服务器 docker pull 拉取 → docker run 启动 → 版本可追溯(v1.0.0、v1.0.1...),出问题秒回滚 ``` ### 4.5 环境变量处理技巧 前端项目在**构建时**注入环境变量,而非运行时: ```bash # Vue / Vite 项目:使用 .env 文件 # .env.production VITE_API_BASE_URL=https://api.example.com VITE_APP_TITLE=My App # React / CRA 项目 # .env.production REACT_APP_API_URL=https://api.example.com ``` ```dockerfile # Dockerfile 中注入构建时变量 ARG VITE_API_BASE_URL=https://api.example.com ENV VITE_API_BASE_URL=$VITE_API_BASE_URL RUN npm run build ``` ```bash # 构建时动态指定 docker build --build-arg VITE_API_BASE_URL=https://api.prod.com -t myapp-web . ``` > **注意**:如果需要运行时动态修改 API 地址,可在 `index.html` 中注入 `window.__CONFIG__`,通过 Nginx 的 `sub_filter` 在启动时替换。 --- ## 五、Nginx 反向代理与域名配置 ![slide_09.png](https://pic.code-nav.cn/post_picture/1609766978631761921/Vqrs5oEodf7sajwT.webp) ### 5.1 反向代理:统一入口 将前端 + 后端 API 统一到同一域名下,避免跨域问题: ```nginx # /etc/nginx/conf.d/myapp.conf # HTTP -> HTTPS 重定向 server { listen 80; server_name www.example.com; return 301 https://$host$request_uri; } # HTTPS 主配置 server { listen 443 ssl http2; server_name www.example.com; # SSL 证书(Let's Encrypt 免费获取) ssl_certificate /etc/letsencrypt/live/www.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/www.example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; # ---- 前端静态文件 ---- location / { root /var/www/myapp; index index.html; try_files $uri $uri/ /index.html; } # ---- 后端 API 代理 ---- location /api/ { proxy_pass http://127.0.0.1:8080/; # Spring Boot proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # WebSocket 支持 proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; # 超时设置 proxy_connect_timeout 60s; proxy_read_timeout 120s; proxy_send_timeout 60s; } # ---- Python API 代理(如有) ---- location /pyapi/ { proxy_pass http://127.0.0.1:8000/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # 静态资源缓存 location /assets/ { expires 1y; add_header Cache-Control "public, immutable"; } } ``` ### 5.2 SSL 证书配置(Let's Encrypt) ```bash # 安装 Certbot sudo yum install -y certbot python3-certbot-nginx # 获取证书(自动修改 Nginx 配置) sudo certbot --nginx -d www.example.com # 自动续期(Certbot 会自动添加 cron) sudo certbot renew --dry-run ``` --- ## 六、Docker Compose 一键编排 ![slide_08.png](https://pic.code-nav.cn/post_picture/1609766978631761921/na8alcGkLaBRNlsI.webp) > 当项目包含前端、后端、数据库等多个服务时,用 Docker Compose 统一管理。 ### 6.1 完整的 docker-compose.yml ```yaml # docker-compose.yml version: "3.9" services: # ---- Nginx 网关 ---- nginx: image: nginx:1.25-alpine ports: - "80:80" - "443:443" volumes: - ./nginx/conf.d:/etc/nginx/conf.d - ./nginx/ssl:/etc/letsencrypt - frontend-dist:/usr/share/nginx/html depends_on: - java-api - python-api restart: unless-stopped networks: - app-network # ---- Java Spring Boot 后端 ---- java-api: build: context: ./java-backend dockerfile: Dockerfile environment: - SPRING_PROFILES_ACTIVE=prod - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/mydb - SPRING_DATASOURCE_USERNAME=${DB_USER} - SPRING_DATASOURCE_PASSWORD=${DB_PASSWORD} - TZ=Asia/Shanghai ports: - "8080:8080" # 仅调试用,生产可去掉 depends_on: db: condition: service_healthy restart: unless-stopped networks: - app-network # ---- Python FastAPI 后端 ---- python-api: build: context: ./python-backend dockerfile: Dockerfile environment: - DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@db:5432/mydb - TZ=Asia/Shanghai ports: - "8000:8000" # 仅调试用 depends_on: db: condition: service_healthy restart: unless-stopped networks: - app-network # ---- PostgreSQL 数据库 ---- db: image: postgres:16-alpine environment: - POSTGRES_DB=mydb - POSTGRES_USER=${DB_USER} - POSTGRES_PASSWORD=${DB_PASSWORD} - TZ=Asia/Shanghai volumes: - pgdata:/var/lib/postgresql/data # 不暴露端口到公网! # ports: # - "5432:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"] interval: 10s timeout: 5s retries: 5 restart: unless-stopped networks: - app-network # ---- Redis 缓存 ---- redis: image: redis:7-alpine command: redis-server --requirepass ${REDIS_PASSWORD} volumes: - redisdata:/data restart: unless-stopped networks: - app-network volumes: pgdata: redisdata: frontend-dist: networks: app-network: driver: bridge ``` ### 6.2 环境变量管理 ```bash # .env 文件(不要提交到 Git!) DB_USER=myuser DB_PASSWORD=your_strong_password_here REDIS_PASSWORD=your_redis_password_here ``` ```bash # .gitignore 中添加 .env ``` ### 6.3 一键启停 ```bash # 启动所有服务 docker-compose up -d # 查看运行状态 docker-compose ps # 查看日志 docker-compose logs -f java-api # 重启单个服务 docker-compose restart java-api # 停止并删除容器(数据卷保留) docker-compose down # 重新构建并启动 docker-compose up -d --build ``` --- ## 七、CI/CD 自动化部署 ![slide_07.png](https://pic.code-nav.cn/post_picture/1609766978631761921/Xe9d6mnXL84PDUmL.webp) ### 7.1 GitHub Actions 示例 **Java Spring Boot + Docker 自动部署**: ```yaml # .github/workflows/deploy-java.yml name: Deploy Java API on: push: branches: [main] paths: - 'java-backend/**' jobs: deploy: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Set up JDK 17 uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' - name: Build with Maven run: | cd java-backend mvn clean package -DskipTests - name: Build Docker Image run: | cd java-backend docker build -t myapp-java:${{ github.sha }} . - name: Deploy to Server uses: appleboy/ssh-action@v1 with: host: ${{ secrets.SERVER_HOST }} username: ${{ secrets.SERVER_USER }} key: ${{ secrets.SSH_PRIVATE_KEY }} script: | cd /opt/myapp docker pull myregistry/myapp-java:${{ github.sha }} docker-compose up -d java-api docker image prune -f ``` **前端 Vue/React 自动部署**: ```yaml # .github/workflows/deploy-frontend.yml name: Deploy Frontend on: push: branches: [main] paths: - 'frontend/**' jobs: deploy: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' cache-dependency-path: frontend/package-lock.json - name: Install & Build run: | cd frontend npm ci npm run build - name: Deploy to Server uses: appleboy/scp-action@v0.1.7 with: host: ${{ secrets.SERVER_HOST }} username: ${{ secrets.SERVER_USER }} key: ${{ secrets.SSH_PRIVATE_KEY }} source: "frontend/dist/*" target: "/var/www/myapp" strip_components: 2 - name: Reload Nginx uses: appleboy/ssh-action@v1 with: host: ${{ secrets.SERVER_HOST }} username: ${{ secrets.SERVER_USER }} key: ${{ secrets.SSH_PRIVATE_KEY }} script: sudo nginx -s reload ``` ### 7.2 GitLab CI 示例 ```yaml # .gitlab-ci.yml stages: - build - deploy variables: DOCKER_IMAGE: registry.example.com/myapp build-java: stage: build image: maven:3.9-eclipse-temurin-17 script: - cd java-backend - mvn clean package -DskipTests - docker build -t $DOCKER_IMAGE/java:$CI_COMMIT_SHA . - docker push $DOCKER_IMAGE/java:$CI_COMMIT_SHA only: changes: - java-backend/**/* deploy: stage: deploy script: - ssh deploy@server "cd /opt/myapp && docker-compose pull && docker-compose up -d" only: - main when: manual # 需手动触发部署 ``` --- ## 八、生产环境最佳实践 ![slide_10.png](https://pic.code-nav.cn/post_picture/1609766978631761921/Levj7Abrurea6lrr.webp) ### 8.1 安全清单 - [ ] **不使用 root 运行应用**:Docker 容器内创建专用用户 - [ ] **数据库不暴露公网**:通过安全组/防火墙限制为内网访问 - [ ] **敏感信息使用环境变量**:密码、密钥不硬编码,不提交 Git - [ ] **启用 HTTPS**:Let's Encrypt 免费 SSL - [ ] **设置安全响应头**:X-Frame-Options、CSP、HSTS - [ ] **定期更新依赖**:关注安全漏洞公告 - [ ] **SSH 密钥登录**:禁用密码登录 ### 8.2 日志管理 ```bash # Docker 日志限制 # /etc/docker/daemon.json { "log-driver": "json-file", "log-opts": { "max-size": "50m", "max-file": "3" } } ``` ```bash # Spring Boot 日志配置(application-prod.yml) logging: file: name: /app/logs/application.log logback: rollingpolicy: max-file-size: 50MB max-history: 30 total-size-cap: 1GB ``` ### 8.3 监控告警 | 工具 | 用途 | 推荐场景 | |------|------|----------| | Prometheus + Grafana | 指标采集与可视化 | 中大型项目 | | Uptime Kuma | 站点可用性监控 | 个人/小团队 | | Sentry | 错误追踪 | 所有项目 | | Portainer | Docker 可视化管理 | 容器化项目 | ### 8.4 备份策略 ```bash #!/bin/bash # backup.sh - 数据库定时备份脚本 DATE=$(date +%Y%m%d_%H%M%S) BACKUP_DIR="/opt/backups" # PostgreSQL 备份 docker exec db pg_dump -U myuser mydb | gzip > "$BACKUP_DIR/db_$DATE.sql.gz" # 保留最近 7 天备份 find $BACKUP_DIR -name "*.sql.gz" -mtime +7 -delete echo "Backup completed: db_$DATE.sql.gz" ``` ```bash # 添加 crontab 定时任务(每天凌晨 3 点执行) crontab -e 0 3 * * * /opt/scripts/backup.sh >> /opt/backups/backup.log 2>&1 ``` ### 8.5 性能优化速查 | 项目类型 | 优化点 | 具体措施 | |----------|--------|----------| | Java | JVM 调优 | G1GC、容器感知内存、连接池 | | Python | 并发模型 | Gunicorn 多 worker、异步 IO | | 前端 | 资源优化 | Gzip/Brotli 压缩、CDN、懒加载 | | Nginx | 连接优化 | keepalive、upstream 缓存、限流 | | 数据库 | 查询优化 | 索引、连接池、读写分离 | --- ## 九、常见问题排查 ### 9.1 Java 项目 | 问题 | 原因 | 解决方案 | |------|------|----------| | `OutOfMemoryError` | 堆内存不足 | 调大 `-Xmx`,查看 `logs/heapdump.hprof` 分析内存 | | 启动慢 | 依赖多、扫描路径大 | 开启懒加载 `spring.main.lazy-initialization=true` | | 连接数据库超时 | 网络或连接池配置 | 检查安全组,调整 `spring.datasource.hikari` | | Docker 容器时区不对 | 默认 UTC | 设置 `TZ=Asia/Shanghai` | | 脚本 stop 后进程仍在 | 优雅停机超时 | 检查应用是否注册了 shutdown hook,脚本会在 30 秒后自动 `kill -9` | | 外部配置不生效 | `--spring.config.location` 路径错误 | 确认以 `/` 结尾,如 `../config/`,文件名须为 `application-prod.yml` | | 脚本提示 JAR not found | JAR 文件名与脚本配置不一致 | 检查 `app.sh` 中的 `JAR_NAME` 变量是否与实际文件名匹配 | | 日志目录不存在 | 首次部署未创建目录 | 脚本会自动创建;如手动启动需 `mkdir -p logs tmp` | | `kill -9` 后端口仍占用 | 进程残留 | 等待 1-2 分钟释放,或 `ss -tlnp \| grep :8080` 检查 | | 回滚后配置不匹配 | 备份的 JAR 与现有 config 不兼容 | 同时备份 config 目录,或确保配置向后兼容 | ### 9.2 Python 项目 | 问题 | 原因 | 解决方案 | |------|------|----------| | `ModuleNotFoundError` | 依赖未安装 | 检查 `requirements.txt`,确认 venv 激活 | | 502 Bad Gateway | Gunicorn 挂了 | 查看日志 `docker logs`,增加 worker/超时 | | 静态文件 404 | 路径配置错误 | 检查 `STATIC_URL` 和 Nginx alias | | 并发上不去 | GIL 限制 | 增加 worker 数,考虑异步框架(FastAPI) | ### 9.3 前端项目 | 问题 | 原因 | 解决方案 | |------|------|----------| | 刷新页面 404 | Nginx 未配置 SPA 回退 | 添加 `try_files $uri $uri/ /index.html` | | 接口跨域 | 前后端不同端口/域名 | Nginx 反向代理统一域名 | | 更新后白屏 | 浏览器缓存旧文件 | index.html 设 `no-cache`,静态资源加 hash | | 环境变量不生效 | Vite/CRA 构建时注入 | 检查 `.env.production`,确认 `VITE_`/`REACT_APP_` 前缀 | | Docker 镜像太大 | 未用多阶段构建 | 使用 builder 阶段,最终镜像仅含 Nginx + 静态文件 | ### 9.4 通用排查命令 ```bash # 检查端口占用 ss -tlnp | grep :8080 # 检查 Docker 容器状态 docker ps -a # 查看容器日志(最后 100 行) docker logs --tail 100 -f myapp # 检查 Nginx 配置语法 sudo nginx -t # 测试接口连通性 curl -v http://localhost:8080/actuator/health curl -v http://localhost:8000/health # 查看磁盘空间 df -h # 查看内存使用 free -h # 查看系统负载 uptime ``` --- ## 快速部署速查表 | 技术栈 | 构建命令 | 部署方式 | 默认端口 | |--------|----------|----------|----------| | Spring Boot | `mvn clean package` | 脚本 / Docker Compose / Swarm | 8080 | | FastAPI | - | Gunicorn + Uvicorn / Docker | 8000 | | Flask | - | Gunicorn / Docker | 8000 | | Vue (Vite) | `npm run build` | Nginx / Docker | 80 | | React (Vite/CRA) | `npm run build` | Nginx / Docker | 80 | --- > **写在最后**:部署没有银弹,适合自己的才是最好的。小型项目用 JAR/venv + systemd 就够了,中大型项目上 Docker Compose + CI/CD,微服务架构考虑 Kubernetes。先跑起来,再优化。遇到问题别慌,看日志,查端口,99% 的问题都能定位。

几十元带你白嫖一年的Claude和Gemini模型

# 几十元带你白嫖一年的Claude和Gemini模型 ## 前言 大家好,我是汉堡。本篇文章带大家用几十元带你白嫖一年的Claude和Gemini模型,可谓是非常的爽。话不多说,直接实操。 ## 批发 Google AI Pro 先在闲鱼买 Google AI Pro 的认证,前提是需要有谷歌账号,没有可以注册一个,这个我就不教了。一个 Pro 认证大概是几十块钱,如果谷歌账号多的话,建议有多少买多少,我是买了 2 个,花了 50 元左右。买完跟着商家的要求进行操作即可,基本是完全没问题,建议买之前问下不成功能不能退款。 ![image.png](https://pic.code-nav.cn/post_picture/1608639807578177538/OxfEmXF9VLG9Zni8.webp) ## 部署 antigravity claude proxy 首先,在本地 clone 开源项目<https://github.com/badrisnarayanan/antigravity-claude-proxy> 这是一个将 Google Antigravity 的账号额度反向代理成 Claude API 的兼容格式项目,咱们这一步可以直接把git repo地址甩给 codex 或者 Claude Code,让他帮我在本地进行部署,如果你有服务器的话,可以直接部署在服务器上,这样在哪都可以用了,不过前提是服务器得要有科学上网的条件,因为是模拟 Google Antigravity 的请求谷歌服务器,再将返回的内容转发给本机的。而我刚好有一个 NAS,就直接部署在 NAS 上了,走到哪用到哪,如果你也想买 NAS 的话,我推荐懒猫微服,这个项目可以直接在懒猫微服上下载即用。 ![image.png](https://pic.code-nav.cn/post_picture/1608639807578177538/m4Ng7HkUyTbYDeu8.webp) ![image.png](https://pic.code-nav.cn/post_picture/1608639807578177538/zJbf0yTx7qI6yLSy.webp) 安装完毕,打开 \`<http://localhost:8080>\` 访问界面。 ![image.png](https://pic.code-nav.cn/post_picture/1608639807578177538/NgRLLdpWQSXBRQLz.webp) ## 添加谷歌账号 打开账号管理界面,添加已经认证了 Google AI Pro 的谷歌账号,支持多种方式进行登录, 直接通过谷歌账号登录即可。可以看到可以添加多个账号,主要你谷歌账号多理论上就有无限量的模型可以使用。 ![image.png](https://pic.code-nav.cn/post_picture/1608639807578177538/DEVub7ILXY5GEGs3.webp) ![image.png](https://pic.code-nav.cn/post_picture/1608639807578177538/MiyzDsGALMTEqy5y.webp) 如果发现账号状态不是 OK,不要慌点击 Fix 按钮,会弹出授权页面进行验证即可,因为在使用前需要开启 Gemini CLI、Google Antigravity 的使用授权。 ![image.png](https://pic.code-nav.cn/post_picture/1608639807578177538/aHGv0SXmzpJsXknE.webp) 然后刷新一下就可以了。 ## 调用模型 打开 cherry studio 等工具就可以使用了。点击左下角的设置图标配置 Provider,记得选择 Provider 类型为 Anthropic,因为这个项目是转为 Anthropic 的兼容格式。 ![image.png](https://pic.code-nav.cn/post_picture/1608639807578177538/qfgD3qptT1iRJkNl.webp) ![image.png](https://pic.code-nav.cn/post_picture/1608639807578177538/LrJtOdNAyN6R9Nal.png) API 地址填写 \`\`,不知道的话可以问一下 codex/Claude Code,如果是部署在服务器上的话换为服务器 ip,记得要开放下端口。API 密钥在 config.json 文件进行配置,如果是部署在本地可以不配置(默认是空字符串),如果是部署在服务器上,强烈建议配置一下。如果配置了那么 cherry studio 的 API 密钥也是需要填写。 如果不会改,可以让 codex/Claude Code 帮你修改一下,甚至可以让其给你修改成在设置页面配置 API 密钥,省去在配置文件中改来改去了。如果在本地文件改了密钥,还要重启项目。 ![image.png](https://pic.code-nav.cn/post_picture/1608639807578177538/TI46AhfRf4G5posD.webp) 配置完,点击“获取模型列表”按钮,添加可以用的模型,全都添加。 ![image.png](https://pic.code-nav.cn/post_picture/1608639807578177538/be079Fqg9P29ni22.webp) ![image.png](https://pic.code-nav.cn/post_picture/1608639807578177538/kNC0wAJO6TmW5cSn.webp) ![image.png](https://pic.code-nav.cn/post_picture/1608639807578177538/vMcbvsS7bYPa9kkz.webp) 可以看到除了可以用 Gemini 的对话模型和生图模型,还可以用 Claude 4.6 系列的模型,连 opus 都可以用,可谓是十分的香啊。 进入对话页面选择 Claude 4.6 模型,就可以使用啦。 ![image.png](https://pic.code-nav.cn/post_picture/1608639807578177538/2GJRswKTBAJ5hlkJ.webp) ![image.png](https://pic.code-nav.cn/post_picture/1608639807578177538/ZukI2H08wmZnb3La.webp)

下载 APP