Docker 与 Node.js 的最佳实践
ARG 和 .npmrc 不会出现在最终镜像中,但可以在 Docker 守护进程未标记的镜像列表中找到 - 确保删除它们
其余的都在这里
其他好书
收集、策划和撰写者: Yoni Goldberg、Bruno 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" ]
🔗 更多示例和进一步解释。
✅ 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"]
🔗 更多示例和进一步解释
✅ 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"]
🔗 更多示例和进一步解释
✅ 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
🔗 更多示例和进一步解释
✅ 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
🔗 更多示例和进一步解释
✅ 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)
✍🏽 代码示例 – 使用 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
🔗 更多示例和进一步解释
✅ 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
✍🏽 代码示例 – 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"]
🔗 更多示例和进一步解释
✅ 8. 规划高效缓存
📘 TL;DR:如果操作正确,从缓存重建整个 Docker 镜像几乎可以瞬间完成。更新较少的指令应该放在 Dockerfile 的顶部,而经常更改的指令(例如应用程序代码)应该放在底部。
🚩 缺点: Docker 构建将会非常漫长,即使进行微小的更改也会消耗大量资源
✍🏽 代码示例 – 先安装依赖项,再编写代码
COPY "package.json" "package-lock.json" "./"
RUN npm ci
COPY ./app ./app"
✍🏽 反模式 – 动态标签
#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
✍🏽 代码示例 – 首先安装“系统”包
建议创建一个包含所有系统软件包的基础 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"]
🔗 更多示例和进一步解释
✅ 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
🔗 更多示例和进一步解释
✅ 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
✍🏽 代码示例 – 使用多阶段构建进行安全构建
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
🔗 更多示例和进一步解释
✅ 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]
🔗 更多示例和进一步解释
✅ 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
🔗 更多示例和进一步解释
✅ 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
🔗 更多示例和进一步解释
其他好书
- 我们的 Node.js 最佳实践存储库
- YouTube:Bret Fisher 在 DockerCon 上的 Docker 和 Node.js 最佳实践
- Yoni Goldberg 撰写的 Node.js 测试最佳实践
- Node.js 安全最佳实践