Docker 使用 Node.js 的最佳实践 ARG 和 .npmrc 不会出现在最终镜像中,但可以在 Docker 守护进程未标记的镜像列表中找到 - 确保删除它们 其余部分在这里 其他好读物

2025-05-25

Docker 与 Node.js 的最佳实践

ARG 和 .npmrc 不会出现在最终镜像中,但可以在 Docker 守护进程未标记的镜像列表中找到 - 确保删除它们

其余的都在这里

其他好书

收集、策划和撰写者: Yoni GoldbergBruno Scheufler、Kevyn Bruyere 和 Kyle Martin

欢迎阅读我们在 Node.js 领域中体现的 Docker 最佳实践综合列表。

请注意,每一条建议都附有详细信息和代码示例的链接。完整列表可在我们的Node.js 最佳实践库中找到。它涵盖了基础知识,并深入到战略决策,例如限制容器内存的大小和位置、如何防止机密信息粘附到镜像中、是否需要进程管理器作为顶级进程,或者 Node 是否可以充当 PID1?

🏅 非常感谢Bret Fisher,我们从他那里学到了很多有见地的 Docker 最佳实践

✅ 1 使用多阶段构建以获得更精简、更安全的 Docker 镜像

📘 TL;DR:使用多阶段构建仅复制必要的生产环境构件。许多构建时依赖项和文件并非应用程序运行所必需的。使用多阶段构建,这些资源可以在构建期间使用,而运行时环境仅包含必需的内容。多阶段构建是摆脱超重和安全威胁的简单方法。

🚩 否则:更大的图像将需要更长的时间来构建和交付,仅构建工具可能包含漏洞,并且仅适用于构建阶段的秘密可能会泄露。

✍🏽 代码示例 - 用于多阶段构建的 Dockerfile



FROM node:14.4.0 AS build

COPY . .
RUN npm install && npm run build

FROM node:slim-14.4.0

USER node
EXPOSE 8080

COPY --from=build /home/node/app/dist /home/node/app/package.json /home/node/app/package-lock.json ./
RUN npm install --production

CMD [ "node", "dist/app.js" ]


Enter fullscreen mode Exit fullscreen mode

🔗 更多示例和进一步解释

替代文本

✅ 2. 使用 'node' 命令引导,避免使用 npm start

📘 TL;DR:用于CMD ['node','server.js']启动你的应用,避免使用不向代码传递操作系统信号的 npm 脚本。这可以避免子进程、信号处理、优雅关闭和进程相关的问题。

🚩 错误:当没有信号传递时,你的代码将永远不会收到关闭通知。否则,它将失去正常关闭的机会,甚至可能丢失当前请求和/或数据。

✍🏽 代码示例 - 使用 Node 进行引导




FROM node:12-slim AS build


WORKDIR /usr/src/app
COPY package.json package-lock.json ./
RUN npm ci --production && npm cache clean --force

CMD ["node", "server.js"]


Enter fullscreen mode Exit fullscreen mode

🔗 更多示例和进一步解释

替代文本

✅ 3. 让 Docker 运行时处理复制和正常运行时间

📘 TL;DR:使用 Docker 运行时编排器(例如 Kubernetes)时,可以直接调用 Node.js 进程,无需中间进程管理器或复制进程的自定义代码(例如 PM2、Cluster 模块)。运行时平台拥有最高的数据量和可见性,可用于做出布局决策——它最了解需要多少个进程、如何分布它们以及在崩溃时该如何处理。

🚩 否则:容器因资源不足而持续崩溃,进程管理器会无限期地重启它。如果 Kubernetes 意识到这一点,它可能会将其迁移到其他可用实例。

✍🏽 代码示例 – 无需中间工具直接调用 Node.js



FROM node:12-slim

# The build logic comes here

CMD ["node", "index.js"]


Enter fullscreen mode Exit fullscreen mode

🔗 更多示例和进一步解释

替代文本

✅ 4. 使用 .dockerignore 防止机密泄露

TL;DR:包含一个 .dockerignore 文件,用于过滤掉常见的机密文件和开发工件。这样做可以防止机密信息泄露到镜像中。此外,构建时间也会显著缩短。此外,请确保不要递归复制所有文件,而是明确选择要复制到 Docker 的文件。

否则:常见的个人机密文件(如.env、.aws 和 .npmrc)将与有权访问该图像的任何人共享(例如 Docker 存储库)

✍🏽 代码示例 – Node.js 的良好默认 .dockerignore



**/node_modules/
**/.git
**/README.md
**/LICENSE
**/.vscode
**/npm-debug.log
**/coverage
**/.env
**/.editorconfig
**/.aws
**/dist


Enter fullscreen mode Exit fullscreen mode

🔗 更多示例和进一步解释

替代文本

✅ 5. 生产前清理依赖项

📘 TL;DR:虽然在构建和测试生命周期中有时需要 DevDependencies,但最终交付到生产环境的镜像应该是最小化的,并且没有开发依赖项。这样做可以保证只交付必要的代码,并最大限度地减少潜在攻击(即攻击面)。使用多阶段构建时(参见专用项目),可以通过先安装所有依赖项,最后运行“npm ci --production”来实现。

🚩 否则:许多臭名昭著的 npm 安全漏洞都是在开发包中发现的(例如eslint-scope

✍🏽 代码示例 – 生产环境安装



FROM node:12-slim AS build
WORKDIR /usr/src/app
COPY package.json package-lock.json ./
RUN npm ci --production && npm clean cache --force

# The rest comes here


Enter fullscreen mode Exit fullscreen mode

🔗 更多示例和进一步解释

替代文本

✅ 6. 优雅地关机

📘 TL;DR:处理进程的 SIGTERM 事件并清理所有现有连接和资源。这应该在响应正在进行的请求时完成。在 Dockerized 运行时中,关闭容器并非罕见事件,而是日常工作中经常发生的情况。实现这一点需要一些周到的代码来协调几个移动部件:负载均衡器、保持连接、HTTP 服务器和其他资源。

🚩 否则:立即关闭意味着无法回应成千上万的失望用户

✍🏽 代码示例 – 将 Node.js 设置为根进程允许将信号传递给代码




FROM node:12-slim

# Build logic comes here

CMD ["node", "index.js"]
#This line above will make Node.js the root process (PID1)



Enter fullscreen mode Exit fullscreen mode

✍🏽 代码示例 – 使用 Tiny 进程管理器将信号转发到 Node




FROM node:12-slim

# Build logic comes here

ENV TINI_VERSION v0.19.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
RUN chmod +x /tini
ENTRYPOINT ["/tini", "--"]

CMD ["node", "index.js"]
#Now Node will run a sub-process of TINI which acts as PID1



Enter fullscreen mode Exit fullscreen mode

🔗 更多示例和进一步解释

替代文本

✅ 7. 使用 Docker 和 v8 设置内存限制

📘 TL;DR:始终使用 Docker 和 JavaScript 运行时标志配置内存限制。Docker 限制有助于做出周全的容器放置决策,而 --v8 的 max-old-space 标志有助于及时启动 GC 并防止内存利用率过低。实际上,将 v8 的旧空间内存设置为略小于容器限制的值。

🚩 缺点:需要 docker 定义来执行周全的扩展决策,并防止其他进程资源匮乏。如果不同时定义 v8 的限制,容器资源将无法充分利用——如果没有明确的指示,容器在使用率达到其主机资源的 50-60% 时就会崩溃。

✍🏽 代码示例 – Docker 的内存限制



docker run --memory 512m my-node-app


Enter fullscreen mode Exit fullscreen mode

✍🏽 代码示例 – Kubernetes 和 v8 的内存限制



apiVersion: v1
kind: Pod
metadata:
  name: my-node-app
spec:
  containers:
  - name: my-node-app
    image: my-node-app
    resources:
      requests:
        memory: "400Mi"
      limits:
        memory: "500Mi"
    command: ["node index.js --max-old-space-size=450"]


Enter fullscreen mode Exit fullscreen mode

🔗 更多示例和进一步解释

替代文本

✅ 8. 规划高效缓存

📘 TL;DR:如果操作正确,从缓存重建整个 Docker 镜像几乎可以瞬间完成。更新较少的指令应该放在 Dockerfile 的顶部,而经常更改的指令(例如应用程序代码)应该放在底部。

🚩 缺点: Docker 构建将会非常漫长,即使进行微小的更改也会消耗大量资源

✍🏽 代码示例 – 先安装依赖项,再编写代码



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


Enter fullscreen mode Exit fullscreen mode

✍🏽 反模式 – 动态标签



#Beginning of the file
FROM node:10.22.0-alpine3.11 as builder

# Don't do that here!
LABEL build_number="483"

#... Rest of the Dockerfile



Enter fullscreen mode Exit fullscreen mode

✍🏽 代码示例 – 首先安装“系统”包

建议创建一个包含所有系统软件包的基础 Docker 镜像。如果您确实apt需要使用、yum或类似的工具安装软件包apk,这应该是首要步骤之一。您肯定不想每次构建 Node 应用时都重新安装 make、gcc 或 g++。
不要仅仅为了方便而安装软件包,这是一个生产环境的应用。



FROM node:10.22.0-alpine3.11 as builder

RUN apk add --no-cache \
    build-base \
    gcc \
    g++ \
    make

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

FROM node as app
USER node
WORKDIR /app
COPY --from=builder /app/ "./"
RUN npm prune --production

CMD ["node", "dist/server.js"]


Enter fullscreen mode Exit fullscreen mode

🔗 更多示例和进一步解释

替代文本

✅ 9. 使用明确的图像引用,避免使用latest标签

📘 TL;DR:指定明确的镜像摘要或版本标签,切勿提及“最新”。开发人员通常认为指定latest标签就能获得存储库中最新的镜像,但事实并非如此。使用摘要可以确保服务的每个实例都运行完全相同的代码。

此外,引用镜像标签意味着基础镜像可能会发生变化,因为镜像标签无法确保确定性安装。如果希望进行确定性安装,可以使用 SHA256 摘要来引用精确镜像。

🚩 否则:基础镜像的新版本可能会在部署到生产环境中时带来重大更改,从而导致应用程序出现意外行为。

✍🏽 代码示例 - 正确与错误



$ docker build -t company/image_name:0.1 .
# 👍🏼 Immutable
$ docker build -t company/image_name
# 👎 Mutable
$ docker build -t company/image_name:0.2 .
# 👍🏼 Immutable
$ docker build -t company/image_name:latest .
# 👎 Mutable
$ docker pull ubuntu@sha256:45b23dee
# 👍🏼 Immutable


Enter fullscreen mode Exit fullscreen mode

🔗 更多示例和进一步解释

替代文本

✅ 10. 优先选择较小的 Docker 基础镜像

📘 TL;DR:大型镜像更容易受到漏洞影响,并增加资源消耗。使用更精简的 Docker 镜像(例如 Slim 和 Alpine Linux 变体)可以缓解此问题。

🚩 否则:构建、推送和拉取镜像将花费更长时间,恶意行为者可能会使用未知的攻击媒介,并且会消耗更多资源。

🔗 更多示例和进一步解释

替代文本

✅ 11. 清除构建时机密,避免在参数中保留机密

📘 TL;DR:避免 Docker 构建环境中的机密信息泄露。Docker 镜像通常在多个环境(例如 CI 和镜像仓库)中共享,这些环境不像生产环境那样经过严格审查。一个典型的例子是 npm 令牌,它通常作为参数传递给 Dockerfile。此令牌在需要使用后仍会保留在镜像中很长时间,并允许攻击者无限期地访问私有 npm 镜像仓库。可以通过复制机密文件.npmrc(例如,使用多阶段构建将其删除,请注意,构建历史记录也应删除)或使用 Docker build-kit secret 功能(该功能不会留下任何痕迹)来避免这种情况。

🚩 否则:每个有权访问 CI 和 docker 注册表的人都将获得访问一些宝贵的组织机密的额外奖励

✍🏽 代码示例 - 使用 Docker 挂载的机密信息(实验性但稳定)



# syntax = docker/dockerfile:1.0-experimental

FROM node:12-slim
WORKDIR /usr/src/app
COPY package.json package-lock.json ./
RUN --mount=type=secret,id=npm,target=/root/.npmrc npm ci

# The rest comes here


Enter fullscreen mode Exit fullscreen mode

✍🏽 代码示例 – 使用多阶段构建进行安全构建



FROM node:12-slim AS build
ARG NPM_TOKEN
WORKDIR /usr/src/app
COPY . /dist
RUN echo "//registry.npmjs.org/:_authToken=\$NPM_TOKEN" > .npmrc && \
npm ci --production && \
rm -f .npmrc

FROM build as prod
COPY --from=build /dist /dist
CMD ["node","index.js"]

The ARG and .npmrc won't appear in the final image but can be found in the Docker daemon un-tagged images list - make sure to delete those

Enter fullscreen mode Exit fullscreen mode




🔗 更多示例和进一步解释

替代文本

✅ 12. 扫描图像以查找多层漏洞

📘 TL;DR:除了检查代码依赖关系外,漏洞还会扫描最终交付到生产环境的镜像。Docker 镜像扫描器不仅会检查代码依赖关系,还会检查操作系统二进制文件。这种端到端安全扫描覆盖范围更广,可以验证构建过程中是否有恶意人员注入恶意代码。因此,建议将此扫描作为部署前的最后一步运行。市面上有一些免费和商业扫描器也提供 CI/CD 插件。

🚩 否则:您的代码可能完全没有漏洞。但是,由于应用程序普遍使用的易受攻击的操作系统级二进制文件(例如 OpenSSL、TarBall)版本,您的代码仍然可能被黑客攻击。

✍🏽 代码示例 – 使用 Trivvy 扫描



sudo apt-get install rpm
$ wget https://github.com/aquasecurity/trivy/releases/download/{TRIVY_VERSION}/trivy_{TRIVY_VERSION}_Linux-64bit.deb
$ sudo dpkg -i trivy_{TRIVY_VERSION}_Linux-64bit.deb
trivy image [YOUR_IMAGE_NAME]

Enter fullscreen mode Exit fullscreen mode




🔗 更多示例和进一步解释

替代文本

✅ 13 清理 NODE_MODULE 缓存

📘 TL;DR:在容器中安装依赖项后,请删除本地缓存。为了加快将来的安装速度而重复依赖项毫无意义,因为以后不会再进行任何安装 - Docker 镜像是不可变的。只需一行代码就可以减少数十 MB 的空间(通常为镜像大小的 10% 到 50%)。

🚩 否则:由于一些文件永远不会被使用,最终交付生产的图像重量将增加 30%

✍🏽 代码示例 – 清理缓存



FROM node:12-slim AS build
WORKDIR /usr/src/app
COPY package.json package-lock.json ./
RUN npm ci --production && npm cache clean --force

The rest comes here

Enter fullscreen mode Exit fullscreen mode




🔗 更多示例和进一步解释

替代文本

✅ 14. 通用 Docker 实践

📘 TL;DR:这是与 Node.js 无直接关系的 Docker 建议的集合 - Node 的实现与其他语言没有太大区别:

✓ 优先使用 COPY 命令而不是 ADD 命令

TL;DR: COPY 更安全,因为它只复制本地文件,而 ADD 支持更复杂的提取,例如从远程站点下载二进制文件

✓ 避免更新基础操作系统

TL;DR:在构建过程中更新本地二进制文件(例如 apt-get update)会导致每次运行时创建不一致的镜像,并且需要提升权限。请使用经常更新的基础镜像。

✓ 使用标签对图像进行分类

TL;DR:为每个镜像提供元数据可能有助于运维人员更好地处理它。例如,包含维护者姓名、构建日期以及其他在需要推断镜像信息时可能有用的信息。

✓ 使用非特权容器

简而言之:特权容器拥有与主机 root 用户相同的权限和能力。这很少需要,并且根据经验,应该使用在官方 Node 镜像中创建的“node”用户。

✓ 检查并验证最终结果

TL;DR:有时,构建过程中很容易忽略一些副作用,例如机密信息泄露或不必要的文件。使用Dive等工具检查生成的镜像可以轻松识别此类问题。

✓ 执行完整性检查

简而言之:在拉取基础镜像或最终镜像时,网络可能会被误导并重定向下载恶意镜像。除非对内容进行签名和验证,否则标准 Docker 协议无法阻止这种情况。Docker Notary是实现此目的的工具之一。

🔗 更多示例和进一步解释

替代文本

✅ 15. 检查 Dockerfile

📘 TL;DR:检查 Dockerfile 的 Lint 代码是识别 Dockerfile 中与最佳实践不同的问题的重要步骤。通过使用专门的 Docker Linter 检查潜在缺陷,可以轻松识别性能和安全方面的改进,从而节省大量时间,避免生产代码中的安全问题。

🚩 错误: Dockerfile 创建者错误地将 Root 用户保留为生产用户,并且使用了来自未知源存储库的镜像。只需使用简单的 linter 检查即可避免这种情况。

✍🏽 代码示例 - 使用 hadolint 检查 Dockerfile



hadolint production.Dockerfile
hadolint --ignore DL3003 --ignore DL3006 <Dockerfile> # exclude specific rules
hadolint --trusted-registry my-company.com:500 <Dockerfile> # Warn when using untrusted FROM images

Enter fullscreen mode Exit fullscreen mode




🔗 更多示例和进一步解释

替代文本

其他好书

  1. 我们的 Node.js 最佳实践存储库
  2. YouTube:Bret Fisher 在 DockerCon 上的 Docker 和 Node.js 最佳实践
  3. Yoni Goldberg 撰写的 Node.js 测试最佳实践
  4. Node.js 安全最佳实践
文章来源:https://dev.to/nodepractices/docker-best-practices-with-node-js-4ln4
PREV
使用 Ollama、vLLM 或 Transformers 在本地安装 DeepSeek-R1 的分步指南
NEXT
GitHub 上 Stars 最多的 15 个开源低代码项目