使用 Codex 给 Node.js、Python、Java 等项目补 Dockerfile 时,最开始往往很简单:

FROM node:22

WORKDIR /app

COPY . .

RUN npm install

RUN npm run build

CMD ["npm", "start"]

本地能够正常构建,也能成功启动。

但随着项目持续迭代,很快可能出现:

  • Docker 镜像从几百 MB 涨到 2GB;

  • 每次只改一行代码,却重新安装全部依赖;

  • CI/CD 构建时间越来越长;

  • node_modules、测试文件、Git 历史一起进了镜像;

  • 开发依赖也被带到生产环境;

  • 镜像中存在缓存和临时文件;

  • 一个小版本发布要等待十几分钟。

这类问题通常不是 Docker 本身慢,而是 Docker Layer 没有被合理设计


一、为什么COPY . . 放太早会拖慢构建?

Docker 镜像由多个 Layer 组成。

例如:

COPY . .
RUN npm install
RUN npm run build

只要项目中任意文件发生变化:

README.md
src/index.ts
测试文件
配置文件

COPY . . 这一层的缓存就会失效。

后面的:

RUN npm install

也必须重新执行。

即使:

package.json
package-lock.json

完全没有变化。

所以一个常见优化是:

COPY package.json package-lock.json ./

RUN npm ci

COPY . .

RUN npm run build

这样只有依赖文件变化时,才需要重新安装依赖。

普通业务代码修改可以继续复用依赖层缓存。


二、Docker Layer顺序要按照“变化频率”设计

可以简单理解为:

越不容易变化的内容
放越前面

越经常变化的代码
放越后面

例如:

FROM node:22

WORKDIR /app

COPY package*.json ./

RUN npm ci

COPY tsconfig.json ./
COPY src ./src

RUN npm run build

依赖层通常比源码变化频率低。

只要:

package-lock.json

没有变化,Docker 就可能直接复用 npm ci 的缓存。

这对 CI/CD 提速非常明显。


三、不要把整个开发环境塞进生产镜像

TypeScript 项目构建时可能需要:

typescript
eslint
vitest
webpack
vite
各种类型声明

但应用真正运行时可能只需要:

Node.js
dist/
生产依赖

如果使用单阶段构建:

FROM node:22

COPY . .

RUN npm ci
RUN npm run build

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

生产镜像中仍然保留:

  • TypeScript;

  • 测试工具;

  • 构建工具;

  • 源代码;

  • 开发依赖。

这些东西运行时根本不需要。


四、使用Multi-stage Build

更适合生产环境的是多阶段构建。

第一阶段:负责构建

FROM node:22 AS builder

WORKDIR /app

COPY package*.json ./

RUN npm ci

COPY . .

RUN npm run build

这一阶段可以安装完整依赖。

第二阶段:负责运行

FROM node:22-slim AS runner

WORKDIR /app

COPY package*.json ./

RUN npm ci --omit=dev

COPY --from=builder /app/dist ./dist

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

最终生产镜像只包含:

Node运行环境
生产依赖
dist

构建工具不会被带进去。

这就是 Multi-stage Build。


五、为什么多阶段构建能明显减小镜像?

假设完整 node_modules

650MB

其中:

开发依赖:450MB
生产依赖:200MB

源码和构建缓存:

300MB

单阶段镜像可能超过:

1GB

多阶段以后最终只复制:

200MB生产依赖
+
80MB dist
+
基础运行环境

镜像体积可能直接下降一大截。

镜像越小通常意味着:

  • 上传镜像仓库更快;

  • Kubernetes拉取更快;

  • 部署启动更快;

  • CI缓存成本更低;

  • 攻击面更小。


六、一定要配置.dockerignore

很多项目有 .gitignore,却没有 .dockerignore

Docker执行:

COPY . .

之前会先把 Build Context 发送给 Docker。

如果项目目录里有:

node_modules
.git
coverage
dist
logs
.env
tmp

这些内容可能全部进入 Build Context。

可以创建:

.dockerignore

内容例如:

node_modules
.git
.gitignore
coverage
dist
*.log
.env
.env.*
tmp
README.md

具体内容需要根据项目调整。


七、node_modules尤其不要复制进镜像

如果本地已经存在:

node_modules

而没有 .dockerignore

COPY . .

就可能把本地依赖复制进去。

这会带来几个问题:

平台不一致

本地可能是:

Windows
macOS

Docker运行:

Linux

某些包含原生二进制的包可能直接失效。

镜像变大

本地 node_modules 可能几百MB。

覆盖容器内部安装结果

可能导致:

npm ci

安装完成后,又被本地目录覆盖。

因此通常应该:

node_modules

直接加入 .dockerignore


八、npm install和npm ci怎么选?

生产构建环境更常见:

RUN npm ci

因为 npm ci 会严格根据:

package-lock.json

安装依赖。

相比:

npm install

它更适合 CI:

  • 依赖版本更稳定;

  • 不随意修改 lock 文件;

  • 构建结果更容易复现;

  • 通常速度也更适合自动化环境。

如果项目使用:

pnpm
yarn

同样应该使用对应的锁文件安装模式。


九、不要随意删除Lock File

一个危险做法是:

RUN rm package-lock.json
RUN npm install

这样每次 Docker 构建可能安装不同的小版本依赖。

今天:

package A 1.2.3

过几天:

package A 1.2.7

即使业务代码完全没改,生产镜像中的依赖也可能发生变化。

Docker构建应该追求:

相同源码
+
相同Lock File
=
尽可能相同的构建结果

十、基础镜像也会影响体积

例如:

FROM node:22

和:

FROM node:22-slim

基础镜像大小可能差异明显。

有些项目还会使用 Alpine:

FROM node:22-alpine

不过 Alpine 使用 musl libc,一些原生模块可能存在兼容性差异。

因此不能简单认为:

Alpine一定最好

应该根据:

  • 原生依赖;

  • 调试需求;

  • 安全要求;

  • 镜像大小;

  • 兼容性;

综合选择。


十一、不要在镜像里保留包管理缓存

有些系统包安装:

RUN apt-get update
RUN apt-get install -y curl

可能留下大量缓存。

更合理:

RUN apt-get update \
    && apt-get install -y --no-install-recommends curl \
    && rm -rf /var/lib/apt/lists/*

这样缓存不会进入最终 Layer。

同理,Python、npm 等包管理工具产生的缓存,也应该根据实际需要清理。


十二、RUN命令为什么经常合并?

例如:

RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*

Docker 每条 RUN 都可能创建新 Layer。

即使后面删除文件:

旧Layer里可能仍然存在

所以通常会写成:

RUN apt-get update \
    && apt-get install -y --no-install-recommends curl \
    && rm -rf /var/lib/apt/lists/*

同一 Layer 中创建并删除临时内容,可以避免它永久占据旧 Layer。


十三、开发镜像和生产镜像不要混用

开发环境可能需要:

热更新
调试器
完整源码
测试工具
开发依赖

生产环境更关注:

镜像最小
启动快
依赖少
安全
稳定

可以分别使用:

Dockerfile.dev

Dockerfile

或者在同一个 Dockerfile 中设计多个 target:

development
builder
production

避免为了本地开发方便,把一堆调试工具带进生产环境。


十四、不要把Secret写进Dockerfile

错误示例:

ENV API_KEY="真实密钥"

或者:

COPY .env .env

镜像一旦构建完成,敏感信息可能存在于:

  • Layer;

  • 镜像历史;

  • 镜像仓库;

  • 构建缓存。

即使后面执行:

RUN rm .env

旧 Layer 中仍可能保留。

生产 Secret 应该通过:

环境变量
Secret Manager
Kubernetes Secret
部署平台配置

在运行时注入。


十五、Build Argument也不是安全保险箱

例如:

ARG API_KEY

然后:

RUN some-command --token=$API_KEY

某些情况下 Secret 仍可能进入:

  • 构建日志;

  • Layer;

  • Cache;

  • 镜像历史。

如果构建阶段确实需要私密凭证,应评估 BuildKit Secret 等专门机制,而不是简单使用普通 ARG。


十六、Docker BuildKit可以进一步优化缓存

BuildKit 支持 Cache Mount。

例如:

RUN --mount=type=cache,target=/root/.npm \
    npm ci

这样 npm 下载缓存可以在多次构建之间复用。

依赖仍然重新安装到当前镜像 Layer,但不需要每次从网络重新下载全部包。

对于大型 CI 项目可以明显减少构建时间。


十七、Monorepo尤其需要控制Build Context

假设项目:

apps/
  web/
  api/
  admin/

packages/
  shared/

只构建:

api

如果仍然:

COPY . .

可能把整个 Monorepo 都传进 Docker。

结果:

  • web变化导致api缓存失效;

  • admin改README也触发重构建;

  • Build Context巨大。

可以根据工作区结构只复制需要的文件。

例如:

COPY apps/api ./apps/api
COPY packages/shared ./packages/shared

同时配合 Workspace 工具优化依赖裁剪。


十八、镜像Tag不要只用latest

如果部署只使用:

my-api:latest

线上出问题后很难知道:

latest到底是哪次构建?

建议同时包含版本:

my-api:1.4.3
my-api:git-a84f20c

这样出现问题时可以快速回滚到:

my-api:git-7c12abc

而不是重新猜测某个 latest


十九、如何判断镜像为什么这么大?

可以先查看:

docker history your-image

观察每个 Layer 大概增加多少体积。

例如:

COPY . .             +750MB
RUN npm install      +600MB
RUN npm run build    +180MB

看到这种结果,很快就能判断问题主要出在哪里。

不要只是告诉 Codex:

帮我把镜像变小。

可以让它先解释 Layer。


二十、让Codex先做Docker Layer审查

可以输入:

请先不要修改Dockerfile。

分析当前Docker构建:

1. 当前基础镜像大小;
2. 每个COPY会复制什么;
3. 哪一步最容易导致缓存失效;
4. 是否把开发依赖带入生产;
5. 是否存在多余Build Context;
6. 是否缺少.dockerignore;
7. 哪些步骤可以使用Multi-stage Build;
8. 哪些Layer可能残留缓存或临时文件。

先分析,再重构 Dockerfile。


二十一、测试不能只看docker build成功

Dockerfile修改完成后,至少需要验证:

镜像是否能够启动
镜像大小变化
构建时间变化
依赖是否完整
健康检查是否正常
生产环境变量是否正常
非root权限是否可运行

还可以比较:

优化前:
镜像1.6GB
构建8分钟

优化后:
镜像420MB
构建2分钟

数据比一句:

Dockerfile已优化

更有价值。


二十二、把Docker规则写进AGENTS.md

可以加入:

# Docker规则

- 生产镜像必须评估Multi-stage Build
- 依赖文件必须在源码COPY之前复制
- node_modules禁止进入Build Context
- 项目必须维护.dockerignore
- 生产镜像不得携带无关开发依赖
- Lock File不得为了构建方便随意删除
- Dockerfile不得写入真实Secret
- 临时构建缓存不得进入最终镜像
- Monorepo构建必须控制Build Context范围
- Docker优化完成后必须比较镜像大小与构建时间

这样 Codex 后续修改容器配置时,就不会只关注:

能不能build

而会同时考虑镜像体积和CI性能。


二十三、Plus还是Pro?

如果主要处理:

  • 单个 Dockerfile;

  • 普通 Node.js / Python 服务;

  • 简单 CI 构建;

  • 中小型项目;

Plus 通常已经能覆盖大部分 Codex 开发任务。

如果长期维护:

  • 大型 Monorepo;

  • 多服务容器;

  • 复杂 CI/CD;

  • Kubernetes;

  • 多架构镜像;

  • 大量构建日志和部署问题;

则可以根据实际开发强度评估 Pro。

不过更大的使用空间不能自动让 Docker 镜像变小。

真正决定构建效率的仍然是:

Layer 是否稳定、Build Context 是否合理,以及最终运行镜像到底包含了什么。

总结

Codex 写 Dockerfile 后镜像越构建越大、CI越来越慢,通常不是 Docker 缓存失效,而是 Layer 顺序、Build Context 和生产依赖没有合理拆分。

通过:

依赖文件优先COPY
.dockerignore
Multi-stage Build
最小运行镜像
BuildKit Cache

可以显著降低镜像体积和重复构建时间。

真正高效的 Dockerfile,不只是:

docker build能成功

而是应该做到:

代码只改一行时,只重新构建真正受影响的部分;生产部署时,只携带运行这项服务真正需要的内容。

CSDN文章描述

本文介绍 Codex 编写 Dockerfile 时常见的镜像体积膨胀与缓存失效问题,并通过 Multi-stage Build、Docker Layer、.dockerignore、BuildKit 和最小运行时镜像优化 CI/CD 构建速度。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐