news 2026/7/28 17:06:45

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
我用 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,看一眼 layer:

dockerhistorymyapp:latest# 1.2GB 总计# - node_modules 占了 540MB# - apt-get 装的 build-essential + python3 占了 380MB# - 基础镜像 180MB# - 剩下的产物代码

痛点:python3build-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_ENV=production" "PATH=/bin:/nix/store/.../bin" ]; }; maxLayers = 100; }; in { images.x86_64-linux = runtimeImage; }; }

编出来 80MB。看着缩水 93%,但请注意这里有个关键点:npmDepsHash必须准确。


踩坑记录(4 条)

坑 1:npmDepsHash 怎么都对不上

第一次编,Nix 提示:

error: hash mismatch in fixed-output derivation specified: sha256-xxxxxx= got: sha256-yyyyyy=

原因是npmDepsHash是把package-lock.json+node_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.conf<<EOF 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 秒,且大部分步骤是下载预编译产物。

坑 4:Docker 镜像怎么 push 到 Kubernetes

Nix 生成的镜像是一个 tarball(dockerTools.buildLayeredImage输出result),用docker load加载:

nix build.#images.x86_64-linuxdockerload<result# Loaded image: myapp:latest# 或者直接 skopeo:skopeo copy docker-archive:result docker://registry.example.com/myapp:v1.0.0

CI 集成时改成:

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)上同时编同一个 commit:

nix 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 --omit=dev就够了,不必上 Nix。但当你需要"signature build"——同一个 commit 永远编出同一个字节序列——Nix 几乎是唯一靠谱的解。


写在最后

Nix 不是银弹,但它解决了 Dockerfile 三个根本问题:基础镜像漂移、构建副作用、版本不可复现。换完之后我们 CI 出错率从每月 3-4 次降到了 0,因为"我这跑得好好的"这种问题在 hash 一致的前提下根本不存在。

完全迁移到 Nix 用了 2 周(包括踩坑),ROI 大概 3 个月回本(节省的 disk + 节省的"为什么我 build 出来不一样"调试时间)。

有问题评论区交流,看到就回。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/28 17:06:23

AI精准优化RNA翻译效率:从9个核苷酸位点突破生物技术瓶颈

你有没有想过,为什么有些疫苗研发出来,效果就是不如预期?问题可能出在你看不见的“翻译”环节上。 我们通常认为,只要设计出能编码目标蛋白的RNA序列,交给细胞去生产就行了。但现实是,大量的RNA分子在进入细胞后,就像一封写满了正确指令却无法被机器读取的信件,被直接…

作者头像 李华
网站建设 2026/7/28 17:05:07

HDU 6690 Rikka with Segment Tree(递归)

大佬博客 题意可以从上面的博客中看题解也完全可以 思路就是定义3个函数F,G,HF,G,HF,G,H 他们都是关于长度为1到n1到n1到n的线段树的某种答案的和。 然后思考一下线段树分成两个线段树的过程&#xff0c;就可以递归计算答案了。 所以这个题的算法就是递归普及减难度 对不起要熟…

作者头像 李华
网站建设 2026/7/28 17:02:36

shiro 使用bean来配置权限信息

在此之前&#xff0c;可以先修改之前的权限配置。之前在applicationContext.xml中权限配置是&#xff1a;<bean id"shiroFilter" class"org.apache.shiro.spring.web.ShiroFilterFactoryBean"><property name"securityManager" ref&quo…

作者头像 李华
网站建设 2026/7/28 17:02:19

三相电压不平衡的危害诊断与工程解决方案

1. 三相电压不平衡的典型表现与危害识别当配电系统中三相电压幅值差异超过允许范围时&#xff0c;我们就会面临电压不平衡问题。在实际运维中&#xff0c;我常通过以下现象快速判断问题存在&#xff1a;设备异常发热&#xff1a;电动机绕组温升明显&#xff0c;特别是中性线电流…

作者头像 李华
网站建设 2026/7/28 17:01:49

终极OpenCore安装指南:5步掌握黑苹果专业引导技术

终极OpenCore安装指南&#xff1a;5步掌握黑苹果专业引导技术 【免费下载链接】OpenCore-Install-Guide Repo for the OpenCore Install Guide 项目地址: https://gitcode.com/gh_mirrors/op/OpenCore-Install-Guide OpenCore是一款专业的macOS引导加载器&#xff0c;专…

作者头像 李华
网站建设 2026/7/28 16:59:51

专科生应对AIGC检测:降AI率工具与技术解析

1. 项目概述&#xff1a;专科生如何应对AIGC检测时代2026年毕业季即将到来&#xff0c;一个名为"千笔降AIGC助手"的平台正在专科院校中快速传播。这个工具直击一个痛点问题&#xff1a;当Turnitin等检测系统开始全面部署AIGC内容识别功能后&#xff0c;使用过AI辅助写…

作者头像 李华