← 返回博客
语言: English 中文
Dev 2026-08-07 8 分钟

Node 生产 Dockerfile 里到底该有什么,不该有什么

生成一个 Dockerfile 很容易。生成一个构建快、用非 root 用户运行、镜像体积小的 Dockerfile,是另一回事。网上大多数模板把容易的部分做对了,却跳过了生产里真正关键的部分。下面这几个决定,才真正影响 Node 服务的构建时间、镜像体积和运行时安全。

DockerfileNode.jsdocker-compose多阶段构建容器

Node 应用的 Dockerfile 很短。难点不在于写出来,而在于写一个不浪费磁盘、不慢慢重建、不以 root 运行的版本。人们常把 Docker 的痛苦,比如构建慢、镜像大、层缓存失效,归咎于 Docker 本身,其实大多来自文件开头那几个决定。本文讲这几个决定以及它们为什么重要。

有意识地选基础镜像

node:20 镜像方便但很大。它基于 Debian,自带一整套运行时多半用不到的构建工具链。node:20-alpine 小得多,是 Node 服务的合理默认,前提是你不依赖 glibc 专有的原生模块。

问题出在原生模块上。sharp、bcrypt 以及任何基于 node-gyp 的包,可能需要 Alpine 默认不带的建设工具。标准解法是多阶段构建:在完整的 node:20 镜像里编译原生模块,再把结果拷进更小的运行时镜像。如果在 Alpine 上遇到莫名其妙的段错误,先查 glibc 与 musl 的差异,而不是查你的代码。

别用 latest。它会在你脚下变动。固定到主版本,CI 里要可复现就固定到补丁版本。

依赖和源码分开缓存

构建时间上最常见的错误,是把 package.jsonsrc/ 放在同一层拷贝。每次改代码都会让依赖层失效,于是 Docker 每次构建都重装一遍。

解法是每个合格 Node Dockerfile 都会用的两步拷贝:

COPY package.json package-lock.json ./
RUN npm ci
COPY . .

按这个顺序,只要 lockfile 没变,npm ci 那层就被缓存。一行代码改动只会重建最后的 COPY . . 层,耗时几秒。把这个写错,会把 20 秒的重建变成 3 分钟。构建慢的时候,这是第一个要查的地方。

镜像里用 npm ci 而不是 npm installnpm ci 读 lockfile,先删 node_modules,再安装精确的那棵树。它比 npm install 更快、更可复现,后者允许解析出一棵新树。

多阶段构建让镜像变小

Node 服务运行时需要依赖,但不需要编译工具链、source map,也不需要构建时用到的 dev 依赖。多阶段构建把构建环境和运行时环境分开。

构建阶段安装全部依赖(含 devDependencies),需要的话编译 TypeScript。运行时阶段只拷贝 node_modules(生产依赖)、编译产物和 package.json。结果常常是单阶段构建一半的体积,并且不附带 typescripteslint 或测试框架。

如果用 Prisma,它的引擎二进制是平台相关的。要在构建阶段为运行时镜像的架构构建它,否则容器启动时会报找不到引擎。本站 Docker 工具专门带了一个 Prisma 选项,就是因为这里容易出错。

NODE_ENV 陷阱

NODE_ENV=production 会做两件让人意外的事。它让 Express、Koa 等框架跳过慢速的开发期检查,这是你想要的。它也会让 npm install 跳过 devDependencies,这在构建阶段有时不是你想要的,因为构建阶段需要 TypeScript 和测试运行器。

解法是只在最终的运行时阶段设置 NODE_ENV=production,或者在想排除 dev 依赖时显式用 npm ci --omit=dev。在构建阶段全局设了 NODE_ENV=production 然后纳闷 tsc 怎么没了,是一种常见又让人困惑的失败。

用非 root 用户运行

默认情况下,Node 镜像以 root 运行。如果你的应用有允许写文件的漏洞,攻击者就在容器内以 root 身份写文件。建一个专用用户并切换过去,是一次性的设置,能消除一整类提权风险。

RUN addgroup -S app && adduser -S app -G app
USER app

大多数编排器也能在平台层面用 runAsNonRoot 策略强制这点,但在 Dockerfile 里设好,意味着镜像默认就是安全的,即使在没强制的平台上也是。

docker-compose 用于开发,不用于生产

Compose 在本地开发很棒:一条命令把应用、Postgres、Redis 一起拉起来。但一个照搬生产的 compose 文件会招来麻烦。生产很少只跑一个节点,很少用本地文件系统存状态,也很少用同样的方式应用配置。

让 compose 留在开发。生产用同一个镜像,但通过编排器自己的机制来配置:环境变量、secrets、健康检查、重启策略。如果你的生产方案是“在一台大服务器上 docker-compose up”,那你有一个 compose 解决不了的扩展性和可靠性问题。

合理的开发 compose 文件定义应用、数据库,也许还有缓存,并给数据库加 volume 让数据能在重启后保留。它不定义生产副本、负载均衡或滚动发布,那些属于别处。

发布前该检查什么

构建镜像后跑 docker image inspect。看体积。如果一个 Node 服务镜像没有特殊原因却超过 400 MB,通常是基础镜像或缺少多阶段构建。跑 docker history <image> 看哪层大。依赖层大是正常的;源码层大往往说明 node_modules 是从宿主拷进去的,而不是在镜像里装的。

运行容器,确认它以非 root 用户启动。确认健康检查通过。确认改一行代码能触发快速重建,这才是层缓存是否设对的真正检验。

本站 Docker 工具生成的 Dockerfile 和 compose 文件已经把这些决定内置进去,你可以从一个合理基线出发,再针对自己的应用调整,而不必从通用模板重写。

主要参考资料

用于核对本文技术细节的标准与官方文档。

在本地生成 Dockerfile 和 compose 文件

本站的 Docker 工具为 Node 生成 Dockerfile 和 docker-compose.yml,可选 Postgres、Redis 和 Prisma 支持,并按下面的决定调好。全部在浏览器里运行,不上传任何内容。

打开 Docker 工具

相关文章

继续阅读同一主题领域的实践指南。

查看全部文章

Cookie 同意

我们使用 Cookie 来增强您的体验并展示相关广告。您可以自定义您的偏好。