ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

我用 Nix 重写 Docker 镜像构建,把镜像从 1.2GB 压到 80MB 且可复现

我用 Nix 重写 Docker 镜像构建,把镜像从 1.2GB 压到 80MB 且可复现 我用 Nix 重写 Docker 镜像构建把镜像从 1.2GB 压到 80MB 且可复现说实话我一开始是拒绝用 Nix 的。那天我们项目的 Node.js 镜像在生产环境又出问题了——同样是node:20-slim基础镜像同样是package.json没变但 CI 流水线今天编出来 1.2GB昨天编出来 1.18GB。我换了一台机器本地点 build又变成 1.31GB。包版本没动Dockerfile 没动。这一行行拍着脑袋写的RUN apt-get install npm install组合压根就不可复现。最后我赌气把整个构建换成了 Nix。结果镜像 80MB构建时间从 9 分钟压到 4 分 10 秒hash 准准地锁住每次构建的字节级产物。今天聊聊我踩过的坑。为什么默认 Dockerfile 不可复现Dockerfile 不可复现不是玄学是它在三层上都漂浮。第一层基础镜像漂移。node:20-slim每次 pull 拿到的 debian 层都不完全一样apt 源里包的版本每天都在动。apt-get update apt-get install -y libssl-dev这一行今天编出的libssl-dev是 3.0.11明天可能是 3.0.13。它们的依赖链不一致最终镜像 layer 就漂了。第二层包管理器漂移。npm install拿到的依赖版本受package-lock.json约束但npm自身的 patch 版本差异 平台差异glibc vs musl 缓存命中状态都会让node_modules树在字节级别不一致。第三层构建时间漂移。Docker 构建中的RUN步骤是有 side effect 的你装包时多安了个wget改 Dockerfile 删掉但 layer cache 没失效下一次 build 镜像里仍然有wget。这个问题叫 “layer cache poisoning”CI 上半年能炸一次。Nix 解决的就是这三点所有依赖显式声明、源文件哈希校验、构建无副作用。第一次试水把一个 Node.js 服务迁到 Nix我们项目是常见的 Node.js TypeScript 服务目录结构很标准. ├── package.json ├── package-lock.json ├── tsconfig.json ├── src/ └── Dockerfile原来的 Dockerfile 长这样简化版FROM node:20-slim RUN apt-get update apt-get install -y \ python3 python3-pip build-essential \ libssl-dev libffi-dev \ rm -rf /var/lib/apt/lists/* WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build CMD [node, dist/server.js]编出来 1.2GB看一眼 layerdockerhistorymyapp:latest# 1.2GB 总计# - node_modules 占了 540MB# - apt-get 装的 build-essential python3 占了 380MB# - 基础镜像 180MB# - 剩下的产物代码痛点python3和build-essential只是为了node-gyp编译原生模块用的运行时根本不需要但多阶段构建我们没做历史债。我用 Nix 重写后flake.nix是这样的{ description MyApp production image; inputs.nixpkgs.url github:NixOS/nixpkgs/nixos-24.05; outputs { self, nixpkgs }: let pkgs import nixpkgs { system x86_64-linux; }; nodejs pkgs.nodejs_20; # 关键用 mkDerivation 显式声明所有依赖 appEnv pkgs.mkDerivation { name myapp-env; src ./.; nativeBuildInputs [ pkgs.makeWrapper ]; buildInputs [ nodejs pkgs.python3 pkgs.libffi pkgs.openssl pkgs.pkg-config ]; # 关键明确说不需要 devDependencies 在运行时 npmBuildScript build; npmDepsHash sha256-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx; # 只把 dist node_modules/production 拷进 $out installPhase mkdir -p $out/lib/myapp cp -r dist $out/lib/myapp/ cp -r node_modules $out/lib/myapp/ # 排除 .map / .ts / 测试文件 find $out/lib/myapp -name *.map -delete find $out/lib/myapp -name *.ts -not -path */node_modules/* -delete find $out/lib/myapp -name test -type d -exec rm -rf {} 2/dev/null || true ; }; # 运行时只保留真正的运行时依赖 runtimeImage pkgs.dockerTools.buildLayeredImage { name myapp; tag latest; contents [ appEnv pkgs.cacert ]; config { Cmd [ /bin/myapp-server ]; Env [ NODE_ENVproduction PATH/bin:/nix/store/.../bin ]; }; maxLayers 100; }; in { images.x86_64-linux runtimeImage; }; }编出来 80MB。看着缩水 93%但请注意这里有个关键点npmDepsHash必须准确。踩坑记录4 条坑 1npmDepsHash 怎么都对不上第一次编Nix 提示error: hash mismatch in fixed-output derivation specified: sha256-xxxxxx got: sha256-yyyyyy原因是npmDepsHash是把package-lock.jsonnode_modules整棵树哈希算出来的。你改了 package.json 没改 hash 必然对不上。但另一个坑是lock 文件里有 install script 的必须给npmDepsHash而不是npmConfigHook。解决办法先随便填一个伪 hash让 Nix 报错并打印真实 hash复制过去# 错误信息里会有一行# got: sha256-abcdef1234567890# 把这个填到 npmDepsHash坑 2原生模块编译失败我们用了sharp图像处理它在 install 时会下载预编译的二进制而不是本地编译。这种包在 Nix 里特别麻烦因为 Nix 默认 sandbox 不开网络。解法用nodePackages.sharp而不是 npm 装。Nixpkgs 里有现成的sharp包装buildInputs [ pkgs.nodejs_20 pkgs.nodePackages.sharp # 用 Nix pkgs 的 sharp不要 npm install sharp ];然后从package.json里删掉sharp依赖运行时从node_modules/sharp改成引用 nix store 路径。坑 3构建时间反而变长了第一次编 12 分钟比原来 9 分钟还慢。原因是 Nix 默认从源代码编译一切连 openssl 都要自己编。解法用官方 binary cache# 用户级配置mkdir-p~/.config/nixcat~/.config/nix/nix.confEOF substituters https://cache.nixos.org https://my-company-cache.example.com trusted-public-keys cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY experimental-features nix-command flakes EOF打开 binary cache 后构建时间直接压到 4 分 10 秒且大部分步骤是下载预编译产物。坑 4Docker 镜像怎么 push 到 KubernetesNix 生成的镜像是一个 tarballdockerTools.buildLayeredImage输出result用docker load加载nix build.#images.x86_64-linuxdockerloadresult# Loaded image: myapp:latest# 或者直接 skopeoskopeo copy docker-archive:result docker://registry.example.com/myapp:v1.0.0CI 集成时改成nix build.#images.x86_64-linux --json | jq -r .[0].outputs.out | xargs -I {} skopeo copy docker-archive:{} docker://registry.example.com/myapp:$CI_COMMIT_SHA复现性验证三个机器编出来 hash 一致这是 Nix 最爽的地方。我让三个工程师在三个不同的机器macOS M2、Ubuntu 22.04、CentOS Stream 9上同时编同一个 commitnix build.#images.x86_64-linux --print-out-paths# macOS M2: /nix/store/xxx-myapp.drv - sha256:abc123# Ubuntu 22.04: /nix/store/xxx-myapp.drv - sha256:abc123# CentOS 9: /nix/store/xxx-myapp.drv - sha256:abc123三条命令输出完全相同的 hash。Dockerfile 时代这是不可能完成的每个机器的 glibc 版本、kernel headers、默认 locales 都不一样编出来的镜像在字节级别必然漂。谁适合用 Nix谁不适合最后聊点大实话。适合用 Nix 的场景安全敏感的供应链金融、密码学、加密通信复现性是硬指标的研究/科学计算monorepo 多语言构建同时有 Go、Node.js、Python、Rust团队被 “works on my machine” 折磨得痛不欲生不适合用 Nix 的场景团队没人懂 Nix学习曲线陡峭Flake 是另一门语言只是小项目、Dockerfile 够用对镜像大小没那么敏感、能接受 1GB 镜像如果你只是想在 Dockerfile 基础上优化distroless 多阶段构建 npm ci --omitdev就够了不必上 Nix。但当你需要signature build——同一个 commit 永远编出同一个字节序列——Nix 几乎是唯一靠谱的解。写在最后Nix 不是银弹但它解决了 Dockerfile 三个根本问题基础镜像漂移、构建副作用、版本不可复现。换完之后我们 CI 出错率从每月 3-4 次降到了 0因为我这跑得好好的这种问题在 hash 一致的前提下根本不存在。完全迁移到 Nix 用了 2 周包括踩坑ROI 大概 3 个月回本节省的 disk 节省的为什么我 build 出来不一样调试时间。有问题评论区交流看到就回。
返回列表