开发者生态
morning
我们将真实的 Node.js 生产 Docker 镜像从 1.2GB 减少到 78MB
2026-05-23
1 阅读
milkikomasiko
臃肿的 Docker 镜像对每个运送容器的团队来说都是无声的负担。慢 CI。部署缓慢。更大的攻击面。更大的登记账单。而且几乎总是一个下午就能修好。 I took a real Node.js + TypeScript service we ship to production, started from the naive Dockerfile most teams write, and walked it down from 1.2GB to 78MB.相同的应用程序,相同的行为,六个步骤,全部在同一台机器上测量。这正是推动这一趋势的原因。起点:1.2GB 这是大多数团队开始使用的 Dockerfile。有用。几乎每条生产线都存在浪费。从节点:22 WORKDIR /app COPY 。 。 RUN npm install RUN npm run build EXPOSE 3000 CMD [ "npm" , "start" ] 构建它并检查大小: $ docker build -t app:naive 。 $ docker images app:naive REPOSITORY TAG SIZE app naive 1.21GB 1.21GB 用于发布生成约 4MB 已编译 JavaScript 的服务。让我们修复它。第 1 步:将基本映像从 1.21GB 切换到 412MB Node:22 标签基于 Debian,包含运行时不需要的完整工具链。纤薄的版本去掉了大部分。 FROM 节点:22-slim 图像大小 节点:22 1.21GB 节点:22-slim 412MB 节点:22-alpine 178MB Alpine 更小,但它使用 musl libc 而不是 glibc。 Most pure-JS apps run fine on it, but anything with native modules ( bcrypt , sharp , node-gyp builds) needs extra care, and some packages have subtle musl bugs.我默认使用 slim,只有当我知道依赖树是干净的时候才使用 alpine。在这篇文章中,我们将继续使用 slim 来保持现实世界应用程序的现实。步骤 2:使用 .dockerignore ,412MB 到 388MB COPY 。 。 happily copies your node_modules , .git , build artifacts, local .env files, IDE folders, and test fixtures into the image. Even if a later step overwrites node_modules , the layer is already in the image history. Create a .dockerignore : node_modules npm-debug.log .git .gitignore .env* .vscode .idea coverage dist build *.md test __tests__ Dockerfile* .dockerignore Small win on size, big win on rebuild speed and security. Your local .env.development is no longer hiding inside a layer that gets pushed to a public registry. Step 3: Multi-stage build, 388MB to 198MB You need TypeScript, eslint, the test framework, and probably a hundred transitive dev dependencies to build the app.您不需要它们中的任何一个来运行它。 A multi-stage build compiles in one image and copies only the artifacts into a second, clean image: # ---- builder ---- FROM node:22-slim AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . 。 RUN npm run build RUN npm prune --omit=dev # ---- runtime ---- FROM node:22-slim WORKDIR /app COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/dist ./dist COPY --from=builder /app/package.json ./package.json EXPOSE 3000 CMD [ "node" , "dist/index.js" ] Two things doing real work here: npm ci instead of npm install . It uses package-lock.json directly, is faster, and is reproducible.在 CI 和 Docker 中使用它。总是。 npm prune --omit=dev strips dev dependencies after the build. On a typical TypeScript service, that is half of node_modules gone. Step 4: Layer caching that actually works, same size, 5x faster rebuilds This is not about size, but it is the single biggest CI win. Most Dockerfiles invalidate npm ci on every code change because they COPY . 。安装之前。顺序很重要。 Already shown above, but worth calling out: copy package*.json first, install, then copy the rest. Now npm ci is cached as long as your dependencies don’t change.复制 package*.json ./ RUN npm ci 复制 . 。 RUN npm run build On a project with ~600 dependencies, this took our