服务器不能编译怎么办
接到迁移任务的时候新服务器是 4 核 8G装了 1Panel 面板。旧机要下线三端服务整体搬过去。三端指的是一个 H5 静态站、一个 Go 后端、一个 Python 管理后台。真正的麻烦是这台新机上已经跑着 6 个别人的线上站点。一开始我想的方案最省事代码传上去在服务器上跑一条 docker compose build起容器完事。Docker 是容器运行时compose 是它官方的多容器编排工具。一份 yaml 里写清几个容器怎么起、怎么连。三步走完本地不用装任何东西也不用镜像导出。然后我看了下服务器的实测回显这个方案就放弃了。第一眼同机不是空机。面板里列出来的站点目录有 6 个。也就是说任何一次把 CPU 打满的动作都是同时在影响 6 个跟我毫不相干的业务。第二眼出网探测。服务器不是我的开发机它能不能访问外网得实测。goproxy.cn : HTTP/2 200 √ pypi 清华源 : HTTP/2 200 √ github.com : 空 × docker hub : 空 × 直连不通但配了云厂商内网加速兜底后注一条。我当时判的「github 不通」后来被推翻过。当时的探测命令带了 8 秒超时空响应其实是超时不是不通。但「源码走本地导出」这个结论没有被推翻原因在后面。「最省事」这个想法是我站在「只管我这一个项目」的视角得出的。一旦把同机的 6 个站点算进去它立刻变成最麻烦的那个方案。先说镜像怎么来的。三个 Dockerfile 的构建方式代价完全不是一个量级。app-h5 最轻。Dockerfile 只有一句 COPY dist把静态产物拷进 nginx:1.27-alpine纯拷贝。app-gin 最重走多阶段构建在 golang:1.25-alpine 里跑 go build。它要拉约 300MB 的基础镜像还要拉依赖、编译。app-admin 居中基础镜像是 python:3.11-slim再加 pip install 装 20 多个包。它要拉约 130MB 的镜像再装依赖。三个镜像做完之后实测体积是这样的。app-admin:latest 是 305 MB基础镜像加 pip 依赖占了大头。app-h5:latest 是 110 MBnginx 加静态产物。app-gin:latest 最小48.4 MBGo 静态二进制。三个加起来大约 463 MBdocker save 的输出量级大致也是这个数。顺带说一句 :latest。它只是镜像的 tag一个可以移动的名字。真正标识一个镜像的是它那串 sha256 摘要。这两者不是一回事后面我会专门讲它。于是有了三条路线的对比。它按的是「编译这件事发生在哪台机器上」跟哪个更高级无关。路线 A全在服务器 build。服务器上要跑编译本地不需要 Docker。但它得让服务器出网拉基础镜像约 270MB。首次构建估计要 8 到 15 分钟满载 CPU。这条路对同机 6 个站点的影响是实打实的CPU 打满响应变慢甚至超时。后续改代码发版靠的是 git pull 加 build。路线 B只传镜像源码不上机。服务器不跑编译本地需要 Docker服务器只拉数据库和缓存。源码不在服务器上只有配置。排障最差因为是个黑盒。风险是镜像和源码容易脱节。路线 C源码上机镜像在本地构建。服务器同样不跑编译本地需要 Docker。服务器只拉数据库和缓存源码留在服务器上排障方便。代价是依赖本地 Docker。改代码之后本地重构建重传的只是那一个镜像。我选的是 C。三条理由每一条都对着上面的一行。编译的 CPU 花在我自己的电脑上不花在别人正在访问的机器上对同机业务零冲击。首次部署和后续发版走的是同一条路不会出现首次能上、之后没法发版的断层。源码仍然留在服务器上出了事能上去看配置、改 .env、翻日志不是一个打不开的黑盒。路线 A 被否掉的核心不是它慢。慢是自己扛的事可它同时还是一次对共享资源的争抢这部分是服务器上其他人的系统替我扛的。决定不在这台服务器上编译之后下一步是把「要传什么」拆开。一拆就发现代码仓库里其实混着三种命运完全不同的东西。先说源码。git archive 把某个提交的已跟踪文件直接导成一个压缩包。它不产出 .git 目录也不需要任何凭证。它和 git clone 的差别在服务器上尤其明显。clone 必须让服务器出网到代码托管平台archive 不需要。clone 带着 .git 目录几十 MBarchive 不带。node_modules 那边clone 视情况带archive 一定不带因为它只导已跟踪文件。仓库是私有的clone 要配 PAT 或者 SSHarchive 不需要任何凭证。上传体积上clone 是仓库全量archive 大约只有 45MB。gitarchive--formattar.gz-oapp-src.tar.gz HEADls-lhapp-src.tar.gz# 预期约 45MB这一条同时解决三件事。服务器出不出网已经不重要了。不用在服务器上留一份 PAT。一个几百 MB 的仓库被压成了 45MB。再说构建产物。这是最容易被忽略的一刀。实测。git ls-files 2_Code/2_UniApp_Vue3/dist | wc -l → 0 ← dist 未被跟踪dist 是前端打包的输出目录本来就不该进版本库。但 app-h5 的 Dockerfile 里写的是 COPY dist /usr/share/nginx/html。dist 不存在或者为空构建出来的就是一个空站点。表现是页面全白没有任何报错。因为 nginx 确实起来了只是没内容可给。所以 dist 必须作为独立产物单独打包单独传。cd2_Code/2_UniApp_Vue3npmrun buildls-ladist/index.html# 确认时间戳是刚刚生成的tar-czfapp-dist.tar.gz dist顺带一条。dist 是带过期时间的产物。仓库里躺着的那个可能是几周前构建的。部署前必须重跑一次 npm run build否则你上的是旧代码。剩下的是镜像。docker save 把本地构建好的镜像导出成一个 tar 文件服务器侧用 docker load 读回来。整个过程里服务器不编译、不拉基础镜像、不装依赖。dockersave app-gin:latest app-admin:latest app-h5:latest-oapp-images.tar sha256sum app-images.tar|teeapp-images.tar.sha256gzip-9app-images.tar顺带一句。我最初按基础镜像的经验值估的是 gzip 后约 200MB真打出来是 121MB。所以体积这件事一律以 ls -lh 的实测为准别拿估算值去规划带宽。真正花时间的地方不在打包命令而在决定什么不该进包。一共有三处打包处置。Python 的 venv 目录被排除了。它是 Windows 虚拟环境虽然被 git 跟踪了但传上 Linux 完全没用。这一条让包从 74MB 降到 36.8MB。5 个 Go 的 .exe 编译产物也被排除了。它们全是 Windows 二进制.dockerignore 本来就排除它们。这一条省掉 78MB。第三处是改用 tar不再用 git archive。git archive HEAD 只导已跟踪文件会漏掉还没 commit 的新文件。比如服务器版编排文件、导入脚本、env 模板这些就漏了。换成 tar 之后这些新文件才进得了包。第三处最值得记住。git archive 的「只导已跟踪文件」既是优点也是边界。它天然排除了 node_modules 和 .git但也天然会漏掉昨天刚写、还没提交的配置。一旦包里必须包含未提交文件就得换成 tar 加一份显式排除清单。这时候「排除什么」变成了要负责的事。然后是那个反直觉的发现。app-images.tar 121 MB app-images.tar.gz 121 MB ← gzip 之后体积没变为什么没变因为镜像是由若干只读的层叠加而成的。而 Docker 的每一层本身就是一个 gzip 压缩过的 tar。外层再压一遍等于拿 gzip 去压 gzip几乎零收益。这件事有两个推论。别为镜像包花时间等 gzip 了。但要注意sha256sum 算的是未压缩的那个 tar。要么先 gunzip -k 解出原包再 -c 校验要么干脆跳过校验直接 docker load。load 成功本身就已经证明包是完整的。当时还算运气的一点是docker load 能直接读 .tar.gz不必先解压。一次「服务器零编译」的迁移最终交付的就是四样东西。app-src.tar.gz 是源码包437 个文件36.8 MB。它由本地打包生成打包时排除了 venv 和 .exe。app-dist.tar.gz 是 H5 静态产物137 个文件17.1 MB。它是本地跑 npm run build 生成的。app-images.tar.gz 里是三个业务镜像121 MB本地 docker save 生成。app-images.tar.sha256 是完整性校验值82 Bsha256sum 算出来的。在服务器上跑的命令里没有一条是 docker build收尾的是 docker load。有一点我到现在也没完全搞明白镜像包 gzip 之后体积为什么一点没变