<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Docker on 友派博客</title><link>https://blog.uipad.cn/tags/docker/</link><description>Recent content in Docker on 友派博客</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Sun, 26 Apr 2026 21:30:00 +0800</lastBuildDate><atom:link href="https://blog.uipad.cn/tags/docker/index.xml" rel="self" type="application/rss+xml"/><item><title>Docker + Postgres 密码玄学：一次 Nano 自动换行引发的血案与排错实录</title><link>https://blog.uipad.cn/post/2026-04/docker-postgres-auth-nano-wrap-bug/</link><pubDate>Sun, 26 Apr 2026 21:30:00 +0800</pubDate><guid>https://blog.uipad.cn/post/2026-04/docker-postgres-auth-nano-wrap-bug/</guid><description>&lt;p&gt;作为一个折腾服务器的独立开发者，经常和 Docker 打交道。本以为 &lt;code&gt;docker-compose up -d&lt;/code&gt; 部署个 PostgreSQL 是闭着眼睛都能搞定的常规操作，结果最近却结结实实栽进了一个连环坑里。&lt;/p&gt;
&lt;p&gt;报错信息大家都很熟悉：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;FATAL: password authentication failed for user &amp;quot;app&amp;quot;&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;但在我反复核对环境变量、网络配置、甚至是容器间的互通性都没问题后，事情开始变得诡异起来。经过两天的抽丝剥茧，我发现这不仅仅是一个简单的密码拼写错误，而是由&lt;strong&gt;幽灵配置、挂载盲区和编辑器特性&lt;/strong&gt;共同组成的三重陷阱。&lt;/p&gt;
&lt;p&gt;这篇博客记录了整个排查过程，希望能帮到同样在终端里怀疑人生的你。&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id="第一重陷阱负负得正的幽灵密码"&gt;第一重陷阱：“负负得正”的幽灵密码
&lt;/h3&gt;&lt;p&gt;事情的起因是这样的：我发现新建的应用容器死活连不上 Postgres，但我之前部署的另一个 Auth 服务却能正常连接。&lt;/p&gt;
&lt;p&gt;我一开始猜测是不是走 Docker 内部网络（比如 &lt;code&gt;Host=pgsql&lt;/code&gt;）就能免密？但 Postgres 的 &lt;code&gt;pg_hba.conf&lt;/code&gt; 机制决定了 TCP 连接必须走 &lt;code&gt;scram-sha-256&lt;/code&gt; 校验，不可能免密。&lt;/p&gt;
&lt;p&gt;通过在容器内部直接 &lt;code&gt;echo&lt;/code&gt; 环境变量，我揪出了第一个内鬼——&lt;strong&gt;未加引号的 .env 注释&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在早期的项目中，我有个&lt;code&gt;.env&lt;/code&gt;文件是这样写的：&lt;code&gt;PGSQL_APP_PASSWORD=app@pgsql # 应用通用密码&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;然后我在docker-compose.yaml中这样写了：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;POSTGRES_APP_PASSWORD&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;${PGSQL_APP_PASSWORD:-app@pgsql}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;因为&lt;code&gt;.env&lt;/code&gt;文件没有加双引号，Docker Compose 粗暴地把后面的空格和中文注释一并吃进了环境变量里。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据库初始化时，把 &lt;code&gt;app@pgsql # 应用通用密码&lt;/code&gt; 当成了完整的密码存了进去。&lt;/li&gt;
&lt;li&gt;旧的 Auth 容器读取同样的配置，带着这串超长且带中文的密码去请求，&lt;strong&gt;两边竟然完美对上号了（负负得正）&lt;/strong&gt;！&lt;/li&gt;
&lt;li&gt;而我在命令行手动敲写干净的 &lt;code&gt;app@pgsql&lt;/code&gt;，自然被无情拒绝。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;避坑指南：&lt;/strong&gt; &lt;code&gt;.env&lt;/code&gt; 文件或 &lt;code&gt;docker-compose.yml&lt;/code&gt; 中的复杂字符串，特别是带有特殊字符的密码，&lt;strong&gt;务必养成加双引号的习惯&lt;/strong&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id="第二重陷阱被忽视的数据卷volume初始化盲区"&gt;第二重陷阱：被忽视的数据卷（Volume）初始化盲区
&lt;/h3&gt;&lt;p&gt;发现了上面的问题后，我决定修正配置，改用干净的密码 &lt;code&gt;app#pgsql&lt;/code&gt;，并重新启动容器。结果——&lt;strong&gt;依然报错！&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我进入容器内部打印环境变量，确认 &lt;code&gt;$POSTGRES_APP_PASSWORD&lt;/code&gt; 已经是正确的 &lt;code&gt;app#pgsql&lt;/code&gt;，为什么还是连不上？&lt;/p&gt;
&lt;p&gt;这时候，我注意到了 &lt;code&gt;docker-compose.yml&lt;/code&gt; 里的这行代码：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;volumes&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;./data:/var/lib/postgresql/data&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这是 Postgres Docker 镜像的一个经典“潜规则”：&lt;strong&gt;只要挂载的目标目录（&lt;code&gt;/var/lib/postgresql/data&lt;/code&gt;）不为空，容器启动时就会直接跳过整个初始化流程（包括执行 &lt;code&gt;/docker-entrypoint-initdb.d/&lt;/code&gt; 下的脚本）。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;因为我之前已经运行过一次，&lt;code&gt;./data&lt;/code&gt; 目录下已经有旧数据了。所以即便我改了配置，重启了容器，数据库里的密码依然是上一次初始化时的那个带中文的“幽灵密码”。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;避坑指南：&lt;/strong&gt; 在开发阶段调试初始化脚本时，如果修改了密码或初始数据库结构，&lt;strong&gt;必须清空宿主机的 &lt;code&gt;./data&lt;/code&gt; 目录&lt;/strong&gt;（&lt;code&gt;rm -rf ./data/*&lt;/code&gt;），或者进入容器用 &lt;code&gt;ALTER USER&lt;/code&gt; 强制修改密码。&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id="最终-bossnano-复制粘贴的背刺"&gt;最终 Boss：Nano 复制粘贴的“背刺”
&lt;/h3&gt;&lt;p&gt;好，我删除了旧数据，确信这次一定会重新初始化，&lt;code&gt;docker logs&lt;/code&gt; 也清楚地打印出执行了我的自定义脚本 &lt;code&gt;01-init-user-and-permissions.sh&lt;/code&gt;，甚至打出了 &lt;code&gt;NOTICE: User created: app&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;满心欢喜地去测试连接。
&lt;strong&gt;报错：&lt;code&gt;password authentication failed&lt;/code&gt;。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;那一刻我真的怀疑自己对 Docker 的认知是不是被篡改了。网络没问题、环境变量没问题、数据也清空重来了，究竟是哪里出了鬼？&lt;/p&gt;
&lt;p&gt;直到我打开那个 &lt;code&gt;01-init-user-and-permissions.sh&lt;/code&gt; 脚本，一行一行地扫，终于发现了这个极其隐蔽的致命伤：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;CREATE USER &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$POSTGRES_APP_USER&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; WITH PASSWORD &lt;span style="color:#e6db74"&gt;&amp;#39;$POSTGRES_APP_PASSWO
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;RD&amp;#39;&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;是的，你没看错。&lt;strong&gt;变量名 &lt;code&gt;$POSTGRES_APP_PASSWORD&lt;/code&gt; 被从中间腰斩，换行了！&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这是怎么发生的？
因为我是直接在 SSH 终端里用 &lt;code&gt;nano&lt;/code&gt; 编辑器，把另一个服务器的脚本&lt;strong&gt;复制粘贴&lt;/strong&gt;进去的。
当你的终端窗口不够宽，且 &lt;code&gt;nano&lt;/code&gt; 开启了默认的&lt;strong&gt;自动换行（Word Wrap）&lt;strong&gt;时，它会在长字符串中间插入一个真正的&lt;/strong&gt;硬回车（Hard Return）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在 Bash 的逻辑里，它找不到 &lt;code&gt;$POSTGRES_APP_PASSWO&lt;/code&gt; 这个变量（因为后面被回车截断了），于是把它解析为&lt;strong&gt;空字符串&lt;/strong&gt;。
最终，数据库真的成功执行了这条 SQL，只不过它执行的是：
&lt;code&gt;CREATE USER &amp;quot;app&amp;quot; WITH PASSWORD '';&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;密码是空的！&lt;/strong&gt; 这就是我用正确的密码死活连不上的终极原因。&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id="总结与最佳实践"&gt;总结与最佳实践
&lt;/h3&gt;&lt;p&gt;这三个坑叠在一起，节目效果直接拉满。为了防止自己（或者正在看文章的你）再次踩坑，总结以下几条开发铁律：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;终端编辑器防身术：&lt;/strong&gt;
如果你要在 Linux 终端下用 &lt;code&gt;nano&lt;/code&gt; 粘贴长代码或长配置，&lt;strong&gt;永远记得带上 &lt;code&gt;-w&lt;/code&gt; 参数&lt;/strong&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;nano -w script.sh
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这会禁用自动换行功能，保命必备。更好的做法是使用 VS Code 的 Remote SSH 插件，直接修改服务器文件，告别终端剪贴板折磨。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;环境变量 密码规范：&lt;/strong&gt;
&lt;code&gt;.env&lt;/code&gt;密码和包含特殊字符的变量，用双引号包起来。行内注释最好另起一行写。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# 应用通用密码&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;PGSQL_APP_PASSWORD&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;app@pgsql&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;数据库调试三板斧：&lt;/strong&gt;
当碰到 Docker DB 密码玄学时，直接在宿主机用超管权限查岗，一秒定音：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# 检查数据库到底吃进去了什么配置&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker exec -it pgsql /bin/bash
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;echo $POSTGRES_APP_PASSWORD
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# 直接用环境变量强制连接测试&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;PGPASSWORD&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$POSTGRES_APP_PASSWORD&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; psql -h 127.0.0.1 -U app -d postgres
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;服务器运维就是这样，有时候折磨你两天的并非什么底层内核级 Bug，仅仅是一个看不见的换行符。记录下来，就当是给以后的自己提个醒吧！&lt;/p&gt;</description></item><item><title>Gitea Runner 多架构 .NET 构建优化日志：告别重复下载，实现本地缓存终极方案</title><link>https://blog.uipad.cn/post/2026-04/gitea-runner-dotnet-multi-arch-build-optimization/</link><pubDate>Fri, 03 Apr 2026 15:30:00 +0800</pubDate><guid>https://blog.uipad.cn/post/2026-04/gitea-runner-dotnet-multi-arch-build-optimization/</guid><description>&lt;p&gt;作为独立开发者，我们使用 Gitea 搭建了私有代码仓库，并通过 Gitea Runner 实现 .NET 多服务的 CI/CD 自动构建。核心需求很明确：构建 amd64 + arm64 双架构镜像，同时解决 &lt;strong&gt;基础镜像重复下载、耗流量、构建速度慢&lt;/strong&gt; 的痛点——毕竟 .NET SDK 镜像体积不小，每次构建都重新下载，不仅浪费时间，也增加了外网流量成本。&lt;/p&gt;
&lt;p&gt;这篇日志记录了我们从初始方案、踩坑试错，到最终找到最优解的完整过程，希望能给有同样需求的开发者提供参考。&lt;/p&gt;
&lt;h2 id="一初始需求与痛点"&gt;一、初始需求与痛点
&lt;/h2&gt;&lt;p&gt;我们有两个 .NET 微服务，需要通过 Gitea Actions 自动构建双架构镜像（amd64 用于服务器部署，arm64 用于边缘设备），并推送到私有镜像仓库。初始面临的核心问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;每次构建都会重新下载 .NET SDK、Runtime 基础镜像，耗时久、耗流量；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;尝试过本地文件缓存，不仅配置复杂，还存在权限问题和磁盘膨胀风险；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;多架构构建（buildx）默认不保留基础镜像，缓存方案难以落地。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="二第一版方案本地文件缓存踩坑"&gt;二、第一版方案：本地文件缓存（踩坑）
&lt;/h2&gt;&lt;p&gt;最初想到的解决方案是使用 buildx 的本地文件缓存（type=local），通过映射宿主机目录，将构建缓存持久化，试图避免重复下载基础镜像。&lt;/p&gt;
&lt;p&gt;第一版 workflow 核心配置：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Build and Push Multiple .NET Services&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;on&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;push&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;tags&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#e6db74"&gt;&amp;#39;v*&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;workflow_dispatch&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;jobs&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;build-multiple-services&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;runs-on&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;runner-host&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;strategy&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;fail-fast&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;max-parallel&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;matrix&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;project&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;image_name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;service-account&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;dockerfile&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;./AccountServer/Dockerfile&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;image_name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;service-ai&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;dockerfile&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;./AIServer/Dockerfile&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;steps&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Checkout Code&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;actions/checkout@v3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Set up Docker Buildx&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;docker/setup-buildx-action@v3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Login to Docker Registry&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;docker/login-action@v3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;with&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;registry&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;registry.example.com&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;username&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;${{ secrets.REGISTRY_USERNAME }}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;password&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;${{ secrets.REGISTRY_PASSWORD }}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Extract Version&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;id&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;vars&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;run&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; REF_NAME=&amp;#34;${GITHUB_REF_NAME:-${GITEA_REF_NAME:-}}&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; if [ -z &amp;#34;$REF_NAME&amp;#34; ]; then REF_NAME=&amp;#34;${GITHUB_REF##*/}&amp;#34;; fi
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; if [[ &amp;#34;$REF_NAME&amp;#34; == v* ]]; then
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; VERSION=&amp;#34;$REF_NAME&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; else
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; VERSION=&amp;#34;latest&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; fi
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; echo &amp;#34;version=$VERSION&amp;#34; &amp;gt;&amp;gt; $GITHUB_OUTPUT&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# 尝试本地文件缓存，解决基础镜像重复下载&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Prepare Cache Directories&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;run&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; mkdir -p /data/build-cache/${{ matrix.project.image_name }}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; mkdir -p /data/build-cache/${{ matrix.project.image_name }}-new&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Build and Push ${{ matrix.project.image_name }}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;docker/build-push-action@v5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;with&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;context&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;file&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;${{ matrix.project.dockerfile }}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;platforms&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;linux/amd64,linux/arm64&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;push&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;tags&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; registry.example.com/group/${{ matrix.project.image_name }}:${{ steps.vars.outputs.version }}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; registry.example.com/group/${{ matrix.project.image_name }}:latest&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# 本地文件缓存配置&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;cache-from&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;type=local,src=https://blog.uipad.cn/data/build-cache/${{ matrix.project.image_name }}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;cache-to&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;type=local,dest=/data/build-cache/${{ matrix.project.image_name }}-new,mode=max&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# 清理旧缓存，防止磁盘膨胀&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Move cache to prevent disk bloat&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;run&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; rm -rf /data/build-cache/${{ matrix.project.image_name }}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; mv /data/build-cache/${{ matrix.project.image_name }}-new /data/build-cache/${{ matrix.project.image_name }} || true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="踩坑总结"&gt;踩坑总结
&lt;/h3&gt;&lt;p&gt;这个方案看似能解决缓存问题，但实际使用中发现多个致命缺陷：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;并行构建冲突&lt;/strong&gt;：为了避免缓存目录冲突，不得不设置 &lt;code&gt;max-parallel: 1&lt;/code&gt;，强制串行构建，降低了构建效率。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;部署迁移麻烦&lt;/strong&gt;：缓存目录交替删除重命名，防止缓存复写出现问题。runner部署是容器化的，它需要额外挂载一个卷来映射/data，需要配置，清理和迁移都麻烦。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;显然，本地文件缓存方案不适合我们的场景，必须寻找更优解。&lt;/p&gt;
&lt;h2 id="三第二版方案镜像仓库缓存不适用"&gt;三、第二版方案：镜像仓库缓存（不适用）
&lt;/h2&gt;&lt;p&gt;参考 Docker 官方文档，尝试使用 &lt;code&gt;type=registry&lt;/code&gt; 缓存，将构建缓存推送到私有镜像仓库，试图实现缓存共享和持久化。&lt;/p&gt;
&lt;p&gt;核心修改点：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# 替换原有的本地缓存配置&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;cache-from&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;type=registry,ref=registry.example.com/group/${{ matrix.project.image_name }}:buildcache&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;cache-to&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;type=registry,ref=registry.example.com/group/${{ matrix.project.image_name }}:buildcache,mode=max&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="不适用原因"&gt;不适用原因
&lt;/h3&gt;&lt;p&gt;这个方案虽然解决了缓存共享和磁盘膨胀问题，但完全不符合我们的核心需求：&lt;/p&gt;
&lt;p&gt;我们的痛点是「避免重复下载基础镜像、节省外网流量」，而将缓存推送到私有镜像仓库，本质上还是需要从网络拉取缓存——无论是拉取基础镜像，还是拉取仓库中的缓存，依然会消耗外网流量，和直接下载基础镜像没有本质区别。&lt;/p&gt;
&lt;p&gt;如果私有镜像仓库是内网部署，这个方案可行，但我们的镜像仓库需要外网访问，因此这个方案被放弃。&lt;/p&gt;
&lt;h2 id="四第三版方案原生-docker-build牺牲多架构"&gt;四、第三版方案：原生 Docker Build（牺牲多架构）
&lt;/h2&gt;&lt;p&gt;既然 buildx 多架构构建难以缓存基础镜像，我们尝试放弃 buildx，使用原生 &lt;code&gt;docker build&lt;/code&gt;，因为原生 Docker 会将基础镜像永久缓存到宿主机，不会重复下载。&lt;/p&gt;
&lt;p&gt;核心修改点：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# 替换 buildx 构建步骤&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;- &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Build &amp;amp; Push ${{ matrix.project.image_name }}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;run&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; IMAGE=&amp;#34;registry.example.com/group/${{ matrix.project.image_name }}&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; VERSION=&amp;#34;${{ steps.vars.outputs.version }}&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; # 原生 docker build，自动缓存基础镜像
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; docker build \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; -t $IMAGE:$VERSION \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; -t $IMAGE:latest \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; -f ${{ matrix.project.dockerfile }} .
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; # 推送镜像
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; docker push $IMAGE:$VERSION
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; docker push $IMAGE:latest&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="优缺点分析"&gt;优缺点分析
&lt;/h3&gt;&lt;p&gt;优点：完美解决基础镜像重复下载问题，第一次下载后，后续构建直接复用宿主机缓存，不耗流量、速度极快；配置简单，无权限和磁盘膨胀问题。&lt;/p&gt;
&lt;p&gt;缺点：&lt;strong&gt;无法构建 arm64 架构&lt;/strong&gt;——原生 &lt;code&gt;docker build&lt;/code&gt; 不支持多架构构建，而我们需要同时构建 amd64 和 arm64 双架构，因此这个方案只能作为过渡，无法满足最终需求。&lt;/p&gt;
&lt;h2 id="五终极方案持久化-buildx-构建器完美解决"&gt;五、终极方案：持久化 Buildx 构建器（完美解决）
&lt;/h2&gt;&lt;p&gt;经过多次试错，我们终于找到了解决方案：&lt;strong&gt;创建一个长期驻留在宿主机上的 Buildx 构建器&lt;/strong&gt;，让构建器容器常驻宿主机，其内部缓存会永久保留，既支持多架构构建，又能避免基础镜像重复下载。&lt;/p&gt;
&lt;p&gt;这个方案的核心逻辑是：Buildx 构建器本身是一个 Docker 容器，只要这个容器不被删除，其内部的基础镜像、构建中间层缓存就会永久保留，后续构建直接复用，无需重新下载。&lt;/p&gt;
&lt;h3 id="最终定稿-workflow"&gt;最终定稿 workflow
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Build and Push Multiple .NET Services&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;on&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;push&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;tags&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#e6db74"&gt;&amp;#39;v*&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;workflow_dispatch&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;jobs&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;build-multiple-services&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;runs-on&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;runner-host&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;strategy&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;fail-fast&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt; &lt;span style="color:#75715e"&gt;# 单个服务构建失败，不影响其他服务&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;matrix&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# 多服务构建矩阵，可按需扩展&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;project&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;image_name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;service-account&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;dockerfile&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;./AccountServer/Dockerfile&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;image_name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;service-ai&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;dockerfile&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;./AIServer/Dockerfile&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;steps&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Checkout Code&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;actions/checkout@v3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# 核心：创建/复用持久化 Buildx 构建器，缓存永久保留&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Set up Persistent Docker Buildx Builder&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;run&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; # 尝试使用已有的构建器，不存在则创建
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; docker buildx use custom-builder 2&amp;gt;/dev/null || \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; docker buildx create --name custom-builder --driver docker-container --use --bootstrap&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Login to Docker Registry&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;docker/login-action@v3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;with&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;registry&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;registry.example.com&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;username&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;${{ secrets.REGISTRY_USERNAME }}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;password&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;${{ secrets.REGISTRY_PASSWORD }}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Extract Version&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;id&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;vars&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;run&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; REF_NAME=&amp;#34;${GITHUB_REF_NAME:-${GITEA_REF_NAME:-}}&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; if [ -z &amp;#34;$REF_NAME&amp;#34; ]; then REF_NAME=&amp;#34;${GITHUB_REF##*/}&amp;#34;; fi
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; if [[ &amp;#34;$REF_NAME&amp;#34; == v* ]]; then
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; VERSION=&amp;#34;$REF_NAME&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; else
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; VERSION=&amp;#34;latest&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; fi
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; echo &amp;#34;version=$VERSION&amp;#34; &amp;gt;&amp;gt; $GITHUB_OUTPUT&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# 多架构构建 + 自动复用缓存&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Build and Push ${{ matrix.project.image_name }}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;uses&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;docker/build-push-action@v5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;with&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;context&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;.&lt;/span&gt; &lt;span style="color:#75715e"&gt;# .NET 构建必须使用仓库根目录作为上下文&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;file&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;${{ matrix.project.dockerfile }}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;platforms&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;linux/amd64,linux/arm64&lt;/span&gt; &lt;span style="color:#75715e"&gt;# 双架构构建&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;push&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;tags&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; registry.example.com/group/${{ matrix.project.image_name }}:${{ steps.vars.outputs.version }}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; registry.example.com/group/${{ matrix.project.image_name }}:latest&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="关键优化点解析"&gt;关键优化点解析
&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;持久化 Buildx 构建器&lt;/strong&gt;：&lt;code&gt;docker buildx create --name custom-builder --driver docker-container&lt;/code&gt; 创建一个命名构建器，使用 &lt;code&gt;docker-container&lt;/code&gt; 驱动，构建器容器会常驻宿主机，不会每次构建都销毁；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;缓存自动保留&lt;/strong&gt;：构建器容器内部会自动缓存 .NET SDK、Runtime 等基础镜像，以及构建中间层，后续构建直接复用，无需重新下载；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;无需额外缓存配置&lt;/strong&gt;：不需要 &lt;code&gt;cache-from&lt;/code&gt;、&lt;code&gt;cache-to&lt;/code&gt; 配置，Buildx 构建器会自动管理缓存，简洁高效；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;支持多架构 + 并行构建&lt;/strong&gt;：去掉了 &lt;code&gt;max-parallel: 1&lt;/code&gt;，可以并行构建多个服务（根据服务器性能调整），同时完美支持 amd64 + arm64 双架构。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="六可选优化禁止缓存自动清理更稳"&gt;六、可选优化：禁止缓存自动清理（更稳）
&lt;/h2&gt;&lt;p&gt;为了确保基础镜像缓存永久保留，避免 Buildx 自动清理缓存，可在创建构建器时添加 &lt;code&gt;--buildkitd-flags '--oci-worker-gc=false'&lt;/code&gt;，禁止自动垃圾回收：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;- &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Set up Persistent Docker Buildx Builder&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;run&lt;/span&gt;: |&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; docker buildx use custom-builder 2&amp;gt;/dev/null || \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; docker buildx create --name custom-builder --driver docker-container \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --use --bootstrap \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; --buildkitd-flags &amp;#39;--oci-worker-gc=false&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="七最终效果与总结"&gt;七、最终效果与总结
&lt;/h2&gt;&lt;p&gt;优化后，我们的 CI/CD 构建实现了以下目标：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;✅ 双架构构建（amd64 + arm64）正常运行，满足多设备部署需求；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;✅ .NET SDK、Runtime 基础镜像只下载一次，后续构建零流量消耗；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;✅ 构建速度提升 80% 以上，从每次构建 10+ 分钟缩短到 2 分钟以内；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;✅ 无权限问题、无磁盘膨胀风险，无需手动维护缓存；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;✅ 配置简洁，可直接复用，支持多服务扩展。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="经验总结"&gt;经验总结
&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;不要盲目使用本地文件缓存或镜像仓库缓存，先明确核心痛点：我们的痛点是「基础镜像重复下载」，而非「项目构建层缓存」；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Buildx 多架构构建的缓存关键的是「持久化构建器」，而非额外的缓存配置；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;原生 Docker Build 和 Buildx 各有优劣，根据是否需要多架构选择合适的方案；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于 Gitea Runner + .NET 多架构构建场景，「持久化 Buildx 构建器」是最优解，兼顾简洁性、稳定性和效率。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;希望这篇日志能帮助到有同样需求的开发者，少走弯路，快速实现高效、省流量的 CI/CD 构建流程。&lt;/p&gt;</description></item><item><title>全网首发：ColoCrossing 独服踩坑记，用 Docker 优雅征服上古 IPMI 与 KVM 假死</title><link>https://blog.uipad.cn/post/2026-04/colocrossing-ipmi-kvm-docker-solution/</link><pubDate>Thu, 02 Apr 2026 21:00:00 +0800</pubDate><guid>https://blog.uipad.cn/post/2026-04/colocrossing-ipmi-kvm-docker-solution/</guid><description>&lt;p&gt;便宜大碗的 ColoCrossing 独立服务器一直备受独立开发者青睐。但当你满怀期待地拿到机器，准备通过 IPMI 挂载 ISO 安装系统时，通常会被现实狠狠地泼一盆冷水——&lt;strong&gt;它的底层硬件和 IPMI 管理系统，实在是太老旧了！&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;翻遍了目前的中文互联网，几乎找不到用现代优雅方式解决这个问题的完整记录。这篇文章记录了我最近在 ColoCrossing 服务器上的一场“受难记”，以及最终如何通过 Docker 实现降维打击的填坑历程。&lt;/p&gt;
&lt;h2 id="-灾难的开始现代浏览器与上古协议的冲突"&gt;💥 灾难的开始：现代浏览器与上古协议的冲突
&lt;/h2&gt;&lt;p&gt;面对基于老旧 Supermicro 主板的 IPMI，我的第一反应是直接用浏览器打开管理地址。然而，噩梦开始了：&lt;/p&gt;
&lt;p&gt;现代浏览器早就彻底封杀了不安全的通信协议。无论我是用 Chrome，还是煞有介事地打开 Edge 的 IE 兼容模式，甚至是换用 Firefox，不是“过期的或不安全的 TLS 安全设置”，就是“PR_END_OF_FILE_ERROR”。因为这些老古董 IPMI 使用的是早已被废弃的 TLS 1.0 甚至更古老的弱加密算法。浏览器连登录界面都不让你看。&lt;/p&gt;
&lt;h2 id="-越陷越深ipmiviewer-与-java-security-地狱"&gt;🌀 越陷越深：IPMIViewer 与 Java Security 地狱
&lt;/h2&gt;&lt;p&gt;Web 端走不通，那就用官方推荐的方法：下载 IPMIViewer（官方基于 Java 的客户端工具）。&lt;/p&gt;
&lt;p&gt;但这引发了第二个深坑。现代版本的本地 JDK 同样对弱加密零容忍。网上的传统教程会教你：找到本地 JDK 目录下的 &lt;code&gt;java.security&lt;/code&gt; 文件，把里面禁用的 &lt;code&gt;TLSv1&lt;/code&gt;、&lt;code&gt;MD5&lt;/code&gt; 等弱加密算法从黑名单里删掉。&lt;/p&gt;
&lt;p&gt;我照做了，一顿操作猛如虎，满心欢喜地打开 IPMIViewer 输入 IP 和密码，结果等了半天，直接弹出一个无情的 &lt;strong&gt;&amp;ldquo;Connection failed&amp;rdquo;&lt;/strong&gt;。无论怎么调参数、换 Java 版本，就是连不上 KVM 控制台。本地环境被搞得乱七八糟，心态几近崩溃。&lt;/p&gt;
&lt;h2 id="-破局之道docker-容器化降维打击"&gt;🐳 破局之道：Docker 容器化降维打击
&lt;/h2&gt;&lt;p&gt;在折腾了许久毫无进展后，我意识到：&lt;strong&gt;用现代系统去向下兼容上古环境，本身就是一条弯路。&lt;/strong&gt; 最干净、最优雅的解法是：用 Docker 给它造一个原汁原味的“上古沙盒”。&lt;/p&gt;
&lt;p&gt;经过一番搜寻，我找到了一个堪称神器的开源镜像：&lt;strong&gt;&lt;code&gt;solarkennedy/ipmi-kvm-docker&lt;/code&gt;&lt;/strong&gt;。这个镜像专门为老旧 IPMI 设计，内置了匹配的 Java 环境和 VNC 桥接。&lt;/p&gt;
&lt;h3 id="操作步骤极其简单"&gt;操作步骤极其简单
&lt;/h3&gt;&lt;p&gt;只需在任意一台装有 Docker 的机器上（甚至是你的本地电脑）执行以下命令：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker run -d &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --name ipmi-kvm &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -p 8080:8080 &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; solarkennedy/ipmi-kvm-docker
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;见证奇迹的时刻：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;容器启动后，打开你本机的现代浏览器，访问 &lt;code&gt;http://localhost:8080&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;此时，你的浏览器里会通过 HTML5 NoVNC 渲染出一个极其清爽的虚拟桌面。&lt;/li&gt;
&lt;li&gt;在这个容器提供的浏览器里，输入你 ColoCrossing 服务器的 IPMI IP 地址。在这里，所有的 TLS 协议报错都不复存在。&lt;/li&gt;
&lt;li&gt;顺畅登录，点击 Launch KVM，下载 &lt;code&gt;.jnlp&lt;/code&gt; 文件并直接在容器内双击运行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;毫无阻碍，KVM 画面瞬间出现！没有 Connection failed，没有任何报错。&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src="https://blog.uipad.cn/images/2026/04/022103_ipmi_kvm.png"
loading="lazy"
alt="ipmi-kvm-古老超微服务器的救星"
&gt;&lt;/p&gt;
&lt;p&gt;用完之后，直接 &lt;code&gt;docker rm -f ipmi-kvm&lt;/code&gt; 销毁容器，深藏功与名，宿主机依然一尘不染。&lt;/p&gt;
&lt;h2 id="-意料之外的-boss-战kvm-画面假死"&gt;🛑 意料之外的 Boss 战：KVM 画面假死
&lt;/h2&gt;&lt;p&gt;本以为搞定了 KVM 控制台就可以挂载镜像、一路 Next 安装系统了。结果在尝试安装 Ubuntu 时，我又踩到了最后也是最隐蔽的一个坑。&lt;/p&gt;
&lt;p&gt;Ubuntu 的系统引导菜单正常显示，但一敲回车进入安装程序，KVM 画面就瞬间定格，仿佛死机了一样。键盘没反应，无论怎么重新连接 KVM 都没用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;背后的真相是：&lt;/strong&gt; 当 Ubuntu 等现代系统内核启动，并尝试启用 KMS (Kernel Mode Setting) 切换到高分辨率的现代图形安装界面时，老旧主板上的那颗负责“录制”KVM 画面的底层管理芯片根本无法解码这种高级别/高分辨率的画面，直接“瞎了”。系统其实没死机，还在后台等我点安装，只是我成了盲人。&lt;/p&gt;
&lt;h3 id="优雅的解决方案退守纯文本模式"&gt;优雅的解决方案：退守纯文本模式
&lt;/h3&gt;&lt;p&gt;既然复杂的图形驱动会搞瞎管理芯片，那就彻底放弃图形界面！&lt;/p&gt;
&lt;p&gt;放弃死磕 Ubuntu，我果断换用了 &lt;strong&gt;Debian 13 (Trixie)&lt;/strong&gt; 的 ISO。在启动引导菜单中，选择 &lt;strong&gt;&lt;code&gt;Command-line install&lt;/code&gt; (文本/命令行安装模式)&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;Debian 经典的蓝黑相间字符安装界面虽然复古，它极其克制，绝不去触碰底层的复杂显卡驱动。在这个模式下，安装过程异常稳健，行云流水般就完成了磁盘划分（还顺手调教了 LVM 取消了 Swap 分区）和系统安装。&lt;/p&gt;
&lt;h2 id="-总结与启发"&gt;💡 总结与启发
&lt;/h2&gt;&lt;p&gt;这次 ColoCrossing 的装机之旅可谓一波三折，但也留下了极其宝贵的运维经验：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;容器化是跨时代运维的最强武器&lt;/strong&gt;：面对老旧的硬件和协议，不要试图去降级自己的主力系统，用 Docker 隔离运行特定的上古环境才是正解。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;克制即稳定&lt;/strong&gt;：在老旧服务器上，抛弃花里胡哨的图形安装界面，拥抱纯净的 Debian 文本模式，不仅能避开显卡驱动的暗坑，还能为后期的业务留出最极致的系统资源。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;底层基建搭好了，接下来就可以放心跑核心业务了。如果你对后续的高可用架构感兴趣，可以看看我的另一篇实践：&lt;a class="link" href="https://blog.uipad.cn/post/2026-03/postgresql-cross-cloud-streaming-replication-with-ssl/" &gt;《独立开发者的异地灾备实践：PostgreSQL 跨云流复制与全链路 SSL 加固》&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;折腾不息，运维不止。祝各位的服务器永不宕机！&lt;/p&gt;</description></item><item><title>从 File Not Found 到一针见血：记一次 Nginx + Docker 路径映射的乌龙与 AI 降维打击</title><link>https://blog.uipad.cn/post/2026-03/nginx-docker-wordpress-path-mismatch-fix/</link><pubDate>Wed, 18 Mar 2026 10:30:00 +0800</pubDate><guid>https://blog.uipad.cn/post/2026-03/nginx-docker-wordpress-path-mismatch-fix/</guid><description>&lt;p&gt;今天为了折腾那套 WordPress 环境，真是差点把键盘给敲烂了。&lt;/p&gt;
&lt;p&gt;事情起因很简单：我用 &lt;code&gt;wordpress:6.9-php8.2-fpm&lt;/code&gt; 镜像搭了个环境，外面套了一层宿主机的 Nginx 做转发。本以为这种教科书级别的配置，闭着眼睛都能跑通，结果浏览器一刷新，屏幕上冷冰冰地躺着四个大字：&lt;strong&gt;“File not found.”&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;我反手就是一个 &lt;code&gt;cat&lt;/code&gt; 检查了宿主机的权限和文件内容，&lt;code&gt;www-data&lt;/code&gt; 用户读写正常，路径也对得上，Nginx 配置里的 &lt;code&gt;fastcgi_pass&lt;/code&gt; 更是直连容器端口。这种“文件明明在，但它死活说没有”的感觉，就像是你在兜里摸到了钥匙，但锁却告诉你门没关、只是你手感不对。&lt;/p&gt;
&lt;p&gt;这时候，我本着“能问 AI 绝不自己抠脑壳”的原则，先去问了下豆包。结果这哥们儿给我绕了一大圈：先是让我查 Linux 文件权限，接着让我改 Nginx 的 &lt;code&gt;user&lt;/code&gt;，最后甚至开始教我怎么重新安装 &lt;code&gt;php-ext-install&lt;/code&gt;。折腾了半天，权限改成了 777，镜像重构了好几次，问题依然稳如泰山。那一刻，我真想问问它：你是不是在跟我玩“运维消消乐”？&lt;/p&gt;
&lt;p&gt;抱着试一试的心态，我转头把同样的 Nginx 配置和报错丢给了 Gemini。&lt;/p&gt;
&lt;p&gt;结果 Gemini 连一句废话都没有，直接点出了那个被我忽略的“盲点”：&lt;strong&gt;路径隔离&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;我宿主机的 Web 根目录在 &lt;code&gt;/opt/wordpress/html&lt;/code&gt;，Nginx 的 &lt;code&gt;$document_root&lt;/code&gt; 也是这个。但我忘了，PHP-FPM 是住在 Docker 容器这个“样板间”里的，在它的世界观里，压根就没有 &lt;code&gt;/opt&lt;/code&gt; 这个路径。它只认官方镜像默认的 &lt;code&gt;/var/www/html&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;当 Nginx 傻乎乎地把 &lt;code&gt;/opt/wordpress/html/index.php&lt;/code&gt; 这个路径传给容器里的 FPM 时，FPM 当然只能回敬一句“File not found”。&lt;/p&gt;
&lt;p&gt;解决办法简单到想哭：只需要在 &lt;code&gt;fastcgi_param SCRIPT_FILENAME&lt;/code&gt; 那里，把 &lt;code&gt;$document_root&lt;/code&gt; 换成容器内部的绝对路径 &lt;code&gt;/var/www/html&lt;/code&gt; 就可以了。&lt;/p&gt;
&lt;p&gt;说实话，这次排障让我感触挺深的。现在的 AI 模型很多，但像 Gemini（哪怕只是 Flash 版本）这样能一眼看穿系统架构逻辑的真的不多。它不是在机械地检索关键词，而是真的理解了“宿主机-容器”这种双重空间下的路径映射逻辑。相比之下，那些只会复读权限指令的 AI，确实显得有些“人工智障”了。&lt;/p&gt;
&lt;p&gt;有时候，强大的模型并不在于它能写多长的代码，而在于它能多快定位到那一个让你抓狂的小变量。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最后的小贴士：&lt;/strong&gt;
如果你也在搞 Nginx 转发 Docker FPM，记住：Nginx 是个传声筒，它说的话（路径），听众（容器）得能听懂才行。&lt;/p&gt;</description></item><item><title>独立开发者的异地灾备实践：PostgreSQL 跨云流复制与全链路 SSL 加固</title><link>https://blog.uipad.cn/post/2026-03/postgresql-cross-cloud-streaming-replication-with-ssl/</link><pubDate>Sat, 14 Mar 2026 21:00:00 +0800</pubDate><guid>https://blog.uipad.cn/post/2026-03/postgresql-cross-cloud-streaming-replication-with-ssl/</guid><description>&lt;p&gt;作为一名独立开发者，最担心的莫过于“单点故障”。虽然 Oracle Cloud 的机器很香，但万一哪天账号被封或机房波动，数据就是命根子。为了实现数据异地实时灾备，我近期完成了一次 PostgreSQL 跨公网的主从同步部署。&lt;/p&gt;
&lt;p&gt;这不仅仅是简单的 &lt;code&gt;pg_dump&lt;/code&gt;，而是基于流复制（Streaming Replication）的“实时影子库”方案。&lt;/p&gt;
&lt;h2 id="1-需求分析为什么不直接全量备份"&gt;1. 需求分析：为什么不直接全量备份？
&lt;/h2&gt;&lt;p&gt;早期的方案是每天定时跑脚本备份到对象存储，但存在两个痛点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;数据不一致&lt;/strong&gt;：如果主库在备份间隔内崩掉，会丢失数小时的数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;恢复慢&lt;/strong&gt;：紧急情况下，重新拉起容器并还原 TB 级或 GB 级数据的时间成本太高。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;我的目标&lt;/strong&gt;：在另一台 VPS 上维护一个随时待命的只读副本，一旦主库失联，分钟级完成主从切换。&lt;/p&gt;
&lt;h2 id="2-解决方案流复制--双向-ssl-加密"&gt;2. 解决方案：流复制 + 双向 SSL 加密
&lt;/h2&gt;&lt;p&gt;由于数据需要跨越公网传输，&lt;strong&gt;安全性&lt;/strong&gt;是首要考量。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;传输层安全&lt;/strong&gt;：必须开启 SSL，防止数据在公网被嗅探。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;身份验证&lt;/strong&gt;：不使用简单的密码，而是采用 &lt;strong&gt;双向证书校验（Cert Authentication）&lt;/strong&gt;。只有持有我签发的客户端证书的服务器，才有权连接主库进行数据同步。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;网络隔离&lt;/strong&gt;：非标准端口（如 &lt;code&gt;8765&lt;/code&gt;）+ 严格的防火墙（iptables/ufw）白名单。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="3-实战避坑指南"&gt;3. 实战避坑指南
&lt;/h2&gt;&lt;h3 id="坑一docker-容器与防火墙的暗箱操作"&gt;坑一：Docker 容器与防火墙的“暗箱操作”
&lt;/h3&gt;&lt;p&gt;在使用 Docker 暴露端口时，Docker 会自动操作 &lt;code&gt;iptables&lt;/code&gt; 规则，导致很多时候即便你没开系统防火墙，端口也是暴露的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;对策&lt;/strong&gt;：手动在 &lt;code&gt;INPUT&lt;/code&gt; 链中通过 &lt;code&gt;-s &amp;lt;备库IP&amp;gt;&lt;/code&gt; 锁定来源地址，并持久化规则（&lt;code&gt;iptables-persistent&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="坑二pg_basebackup-的路径幽灵"&gt;坑二：pg_basebackup 的路径“幽灵”
&lt;/h3&gt;&lt;p&gt;第一次执行 &lt;code&gt;pg_basebackup&lt;/code&gt; 克隆数据时，它生成的 &lt;code&gt;postgresql.auto.conf&lt;/code&gt; 会保留当时临时容器的证书路径（比如 &lt;code&gt;/temp_certs&lt;/code&gt;）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;对策&lt;/strong&gt;：克隆完成后，务必手动修正数据目录下的配置文件，将证书路径指向容器启动时的正式挂载路径（如 &lt;code&gt;/var/lib/postgresql/data/certs/&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="坑三权限权限还是权限"&gt;坑三：权限！权限！还是权限！
&lt;/h3&gt;&lt;p&gt;Postgres 对私钥文件的权限极其挑剔。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;报错&lt;/strong&gt;：&lt;code&gt;could not open file &amp;quot;server.key&amp;quot;: Permission denied&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对策&lt;/strong&gt;：在宿主机执行 &lt;code&gt;chown -R 999:999&lt;/code&gt; 修正 UID。即便在宿主机看是 root，在容器里必须是 &lt;code&gt;postgres&lt;/code&gt; 用户（UID 999）才能读取。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="4-关键配置概览"&gt;4. 关键配置概览
&lt;/h2&gt;&lt;h3 id="主库-pg_hbaconf-安全策略"&gt;主库 &lt;code&gt;pg_hba.conf&lt;/code&gt; 安全策略
&lt;/h3&gt;&lt;pre tabindex="0"&gt;&lt;code class="language-conf" data-lang="conf"&gt;# 仅允许 replication_user 通过证书验证进行流复制
hostssl replication replication_user 备库IP/32 cert
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="备库同步配置-postgresqlautoconf"&gt;备库同步配置 (&lt;code&gt;postgresql.auto.conf&lt;/code&gt;)
&lt;/h3&gt;&lt;pre tabindex="0"&gt;&lt;code class="language-conf" data-lang="conf"&gt;primary_conninfo = &amp;#39;user=replication_user host=主库IP port=8765 sslmode=verify-ca sslcert=/path/to/client.crt sslkey=/path/to/client.key sslrootcert=/path/to/ca.crt&amp;#39;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="5-迁移与维护的思考"&gt;5. 迁移与维护的思考
&lt;/h2&gt;&lt;p&gt;关于数据卷映射，我最终选择了 &lt;strong&gt;Bind Mounts（直接路径映射）&lt;/strong&gt; 而非 Docker Named Volumes。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;理由&lt;/strong&gt;：当需要更换服务器迁移备库时，直接一个 &lt;code&gt;tar&lt;/code&gt; 命令打包 &lt;code&gt;/opt/pgsql/data&lt;/code&gt; 极其透明。不需要去翻 &lt;code&gt;/var/lib/docker/volumes/&lt;/code&gt; 那些隐秘的哈希目录，迁移效率提升 200%。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="最后"&gt;最后
&lt;/h2&gt;&lt;p&gt;高可用架构不是为了炫技，而是为了在意外发生时能睡个好觉。目前这套方案在 Uptime Kuma 的监控下表现稳定，主从延迟几乎保持在 0ms。&lt;/p&gt;</description></item></channel></rss>