接到迁移任务的时候,新服务器是 4 核 8G,装了 1Panel 面板。
旧机要下线,三端服务整体搬过去。
三端指的是一个 H5 静态站、一个 Go 后端、一个 Python 管理后台。
真正的麻烦是这台新机上,已经跑着 6 个别人的线上站点。
一开始我想的方案最省事:代码传上去,在服务器上跑一条 docker compose build,起容器,完事。
Docker 是容器运行时,compose 是它官方的多容器编排工具。
一份 yaml 里写清几个容器怎么起、怎么连。
三步走完,本地不用装任何东西,也不用镜像导出。
然后我看了下服务器的实测回显,这个方案就放弃了。
第一眼,同机不是空机。面板里列出来的站点目录有 6 个。
也就是说,任何一次把 CPU 打满的动作,都是同时在影响 6 个跟我毫不相干的业务。
第二眼,出网探测。服务器不是我的开发机,它能不能访问外网,得实测。
yaml
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 MB,nginx 加静态产物。
app-gin:latest 最小,48.4 MB,Go 静态二进制。
三个加起来大约 463 MB,docker 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 目录,几十 MB;archive 不带。
node_modules 那边,clone 视情况带,archive 一定不带,因为它只导已跟踪文件。
仓库是私有的,clone 要配 PAT 或者 SSH;archive 不需要任何凭证。
上传体积上,clone 是仓库全量,archive 大约只有 45MB。
bash
git archive --format=tar.gz -o app-src.tar.gz HEAD
ls -lh app-src.tar.gz # 预期约 45MB
这一条同时解决三件事。服务器出不出网,已经不重要了。
不用在服务器上留一份 PAT。
一个几百 MB 的仓库,被压成了 45MB。
再说构建产物。
这是最容易被忽略的一刀。实测。
bash
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 必须作为独立产物,单独打包,单独传。
bash
cd 2_Code/2_UniApp_Vue3
npm run build
ls -la dist/index.html # 确认时间戳是刚刚生成的
tar -czf app-dist.tar.gz dist
顺带一条。dist 是带过期时间的产物。
仓库里躺着的那个,可能是几周前构建的。
部署前必须重跑一次 npm run build,否则你上的是旧代码。
剩下的是镜像。
docker save 把本地构建好的镜像导出成一个 tar 文件,服务器侧用 docker load 读回来。
整个过程里,服务器不编译、不拉基础镜像、不装依赖。
bash
docker save app-gin:latest app-admin:latest app-h5:latest -o app-images.tar
sha256sum app-images.tar | tee app-images.tar.sha256
gzip -9 app-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 B,sha256sum 算出来的。
在服务器上跑的命令里,没有一条是 docker build,收尾的是 docker load。
有一点我到现在也没完全搞明白:镜像包 gzip 之后体积为什么一点没变