[{"data":1,"prerenderedAt":1067},["ShallowReactive",2],{"doc-docker-dockerfile-best-practice":3,"same-cat-docker-dockerfile-best-practice":1066},{"id":4,"title":5,"author":6,"body":7,"category":1047,"date":1048,"description":1049,"draft":1050,"extension":1051,"meta":1052,"navigation":619,"path":1053,"seo":1054,"slug":1055,"stem":1056,"summary":1057,"tags":1061,"__hash__":1065},"docs\u002Fdocs\u002Fdocker\u002Fdockerfile-best-practice.md","Dockerfile 最佳实践:10 个让镜像更小、构建更快的写法","元析技术团队",{"type":8,"value":9,"toc":1012},"minimark",[10,14,23,26,34,41,90,97,107,129,131,135,164,168,214,217,219,223,231,237,264,270,290,293,307,318,325,327,331,338,341,347,358,360,371,376,384,390,392,400,406,412,427,434,436,440,443,457,495,502,504,512,518,521,536,547,549,557,571,580,586,594,596,600,896,911,913,917,921,942,946,956,967,972,988,998,1000,1003,1008],[11,12,13],"h2",{"id":13},"校验环境",[15,16,17,18,22],"p",{},"Docker 27.1.1 (BuildKit 默认开启),Debian 12 宿主机,Node.js 20 项目。所有镜像 ",[19,20,21],"code",{},"docker buildx build"," 实测,dive 0.12.0 检查浪费层。",[24,25],"hr",{},[11,27,29,30,33],{"id":28},"_1-永远写-dockerignore否则-copy-会把垃圾全拷进去","1. 永远写 ",[19,31,32],{},".dockerignore","(否则 COPY . 会把垃圾全拷进去)",[15,35,36,37,40],{},"你是不是每次 ",[19,38,39],{},"COPY . \u002Fapp"," 就完事?那下面这些东西会全部进镜像:",[42,43,44,51,57,67,79],"ul",{},[45,46,47,50],"li",{},[19,48,49],{},".git\u002F","(几十 MB 起步)",[45,52,53,56],{},[19,54,55],{},"node_modules\u002F","(宿主机的 Mac\u002FWin 二进制进去没用还占地)",[45,58,59,62,63,66],{},[19,60,61],{},".env","、",[19,64,65],{},".env.local","(把本地密钥打进镜像 = 事故)",[45,68,69,62,72,62,75,78],{},[19,70,71],{},"npm-debug.log",[19,73,74],{},".DS_Store",[19,76,77],{},"dist\u002F","(你本来想在镜像里重新 build 的)",[45,80,81,62,84,62,87],{},[19,82,83],{},"__pycache__",[19,85,86],{},".pytest_cache",[19,88,89],{},"coverage\u002F",[91,92,94,95],"h3",{"id":93},"nodejs-项目真实可抄的-dockerignore","Node.js 项目真实可抄的 ",[19,96,32],{},[98,99,104],"pre",{"className":100,"code":102,"language":103},[101],"language-text","# Git\n.git\n.gitignore\n.github\n.vscode\n.idea\n\n# 依赖目录(在镜像里 npm ci 全新安装)\nnode_modules\n\n# 构建产物(在镜像里 npm run build 重新生成)\ndist\nbuild\n.output\n.nuxt\n.next\n\n# 本地密钥和日志\n.env\n.env.*\n!.env.example\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# 系统垃圾\n.DS_Store\nThumbs.db\n\n# 测试和文档(如果镜像里不用)\ntests\ncoverage\ndocs\nREADME.md\n","text",[19,105,102],{"__ignoreMap":106},"",[15,108,109,110,113,114,113,117,113,120,113,123,113,125,128],{},"Python 项目同理:把 ",[19,111,112],{},"venv\u002F"," ",[19,115,116],{},".venv\u002F",[19,118,119],{},"__pycache__\u002F",[19,121,122],{},"*.pyc",[19,124,86],{},[19,126,127],{},".mypy_cache"," 全忽略。",[24,130],{},[11,132,134],{"id":133},"_2-多阶段构建一个-dockerfile-里-from-两次前编译后运行","2. 多阶段构建:一个 Dockerfile 里 FROM 两次,前编译后运行",[15,136,137,141,142,145,146,149,150,153,154,62,157,62,160,163],{},[138,139,140],"strong",{},"概念",":同一个 Dockerfile 里可以 ",[19,143,144],{},"FROM ... AS builder"," 做一个构建阶段,\n再 ",[19,147,148],{},"FROM ... AS runner"," 做运行阶段,最后只把 builder 阶段里",[138,151,152],{},"编译好的产物"," COPY 到 runner。\nbuilder 阶段的 ",[19,155,156],{},"apt install",[19,158,159],{},"gcc",[19,161,162],{},"node_modules\u002FdevDependencies"," 一概不会进最终镜像。",[91,165,167],{"id":166},"体积对比以典型-nuxt-3-项目为例","体积对比(以典型 Nuxt 3 项目为例)",[169,170,171,186],"table",{},[172,173,174],"thead",{},[175,176,177,182],"tr",{},[178,179,181],"th",{"align":180},"left","写法",[178,183,185],{"align":184},"center","最终镜像大小",[187,188,189,198,206],"tbody",{},[175,190,191,195],{},[192,193,194],"td",{"align":180},"单阶段:node:20 + 全部 node_modules",[192,196,197],{"align":184},"~1.2 GB",[175,199,200,203],{},[192,201,202],{"align":180},"多阶段:node:20 AS builder + node:20-slim AS runner(只拷生产依赖)",[192,204,205],{"align":184},"~180 MB",[175,207,208,211],{},[192,209,210],{"align":180},"多阶段 + distroless(追求极致)",[192,212,213],{"align":184},"~120 MB",[15,215,216],{},"1.2GB → 180MB,拉取速度快 6 倍,registry 存起来也省钱。",[24,218],{},[11,220,222],{"id":221},"_3-run-指令的两个原则","3. RUN 指令的两个原则",[91,224,226,227,230],{"id":225},"原则-a同类命令写一行用-连避免缓存层浪费","原则 A:同类命令写一行,用 ",[19,228,229],{},"&&"," 连,避免缓存层浪费",[15,232,233,236],{},[138,234,235],{},"错误写法 👎","(3 层,中间层全是缓存,最后一层也没删缓存):",[98,238,242],{"className":239,"code":240,"language":241,"meta":106,"style":106},"language-dockerfile shiki shiki-themes github-light github-dark","RUN apt-get update\nRUN apt-get install -y curl git\nRUN rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*\n","dockerfile",[19,243,244,252,258],{"__ignoreMap":106},[245,246,249],"span",{"class":247,"line":248},"line",1,[245,250,251],{},"RUN apt-get update\n",[245,253,255],{"class":247,"line":254},2,[245,256,257],{},"RUN apt-get install -y curl git\n",[245,259,261],{"class":247,"line":260},3,[245,262,263],{},"RUN rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*\n",[15,265,266,269],{},[138,267,268],{},"正确写法 👍","(1 层,装完立刻删掉 apt 的索引缓存,这一层结束就不剩垃圾):",[98,271,273],{"className":239,"code":272,"language":241,"meta":106,"style":106},"RUN apt-get update \\\n  && apt-get install -y --no-install-recommends curl git ca-certificates \\\n  && rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*\n",[19,274,275,280,285],{"__ignoreMap":106},[245,276,277],{"class":247,"line":248},[245,278,279],{},"RUN apt-get update \\\n",[245,281,282],{"class":247,"line":254},[245,283,284],{},"  && apt-get install -y --no-install-recommends curl git ca-certificates \\\n",[245,286,287],{"class":247,"line":260},[245,288,289],{},"  && rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*\n",[15,291,292],{},"注意:",[42,294,295,301],{},[45,296,297,300],{},[19,298,299],{},"--no-install-recommends"," = 不装推荐的软依赖包,Debian 能省很多空间",[45,302,303,306],{},[19,304,305],{},"\u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*"," = apt-get update 下载的索引,镜像里完全没用,每次 apt-get 装完必须删",[91,308,310,311,314,315],{"id":309},"原则-b不要在-run-里写-apt-upgrade-apt-dist-upgrade","原则 B:不要在 RUN 里写 ",[19,312,313],{},"apt upgrade"," \u002F ",[19,316,317],{},"apt dist-upgrade",[15,319,320,321,324],{},"基础镜像(Debian\u002FUbuntu\u002FNode 官方)每周都会修安全补丁打新 tag。你要做的是 ",[19,322,323],{},"FROM node:20-bookworm-slim"," 然后重新 build,\n不是每次 Dockerfile 里 upgrade(会让构建变慢,而且结果不可复现)。如果真的要追 CVE,升级基础镜像 tag。",[24,326],{},[11,328,330],{"id":329},"_4-指令顺序-缓存命中率顺序","4. 指令顺序 = 缓存命中率顺序",[15,332,333,334,337],{},"BuildKit 的缓存机制:只要某一层没变化,后面的层都直接命中缓存不重跑。\n所以 ",[138,335,336],{},"「最不常变的指令」放最前面,「改得最频繁的」放最后面","。",[15,339,340],{},"Node.js 项目的最佳顺序(从上到下):",[98,342,345],{"className":343,"code":344,"language":103},[101],"① FROM node:20-bookworm-slim AS runner       (几乎不变)\n② RUN apt-get update && install 系统包        (偶尔变)\n③ WORKDIR \u002Fapp                               (不变)\n④ COPY package*.json .\u002F                      (只有 package.json \u002F lock 变才重跑)\n⑤ RUN npm ci --omit=dev                      (依赖变才重跑,90% 的代码修改不会触发)\n⑥ COPY . .                                   (每次改代码都重跑 → 放最后)\n⑦ RUN npm run build                          (上面 COPY 触发就跑)\n⑧ CMD [\"node\", \".output\u002Fserver\u002Findex.mjs\"]\n",[19,346,344],{"__ignoreMap":106},[15,348,349,350,353,354,357],{},"反过来如果你把 ",[19,351,352],{},"COPY . ."," 写在第 3 位,那你改任何一行 src\u002F 下的代码,第 5 步的 ",[19,355,356],{},"npm ci"," 都要重新装一遍,慢到哭。",[24,359],{},[11,361,363,364,367,368],{"id":362},"_5-copy-优先不用-add","5. ",[19,365,366],{},"COPY"," 优先,不用 ",[19,369,370],{},"ADD",[15,372,373,375],{},[19,374,370],{}," 除了能 COPY 还多两个魔法:",[42,377,378,381],{},[45,379,380],{},"能把 URL 当源(会自动下载)",[45,382,383],{},"能把 tar.gz 自动解压",[15,385,386,387,337],{},"这两个魔法反而是坑(URL 下载的文件没法做 checksum、解压有时会覆盖权限)。\n除非你明确就是要解压一个本地 tar,否则",[138,388,389],{},"全写 COPY",[24,391],{},[11,393,395,396,399],{"id":394},"_6-不要用-latest-标签","6. 不要用 ",[19,397,398],{},"latest"," 标签",[15,401,402,405],{},[19,403,404],{},"FROM node:latest"," 看起来省事,半年后重新 build 可能就从 Node 20 变成 Node 22 了,\n某个不兼容的改动让你线上挂了还找不到原因。",[15,407,408,411],{},[138,409,410],{},"写死主版本 + 发行版代号",":",[98,413,415],{"className":239,"code":414,"language":241,"meta":106,"style":106},"FROM node:20-bookworm-slim\n#        ↑主版本 ↑Debian 代号(bookworm=12)\n",[19,416,417,422],{"__ignoreMap":106},[245,418,419],{"class":247,"line":248},[245,420,421],{},"FROM node:20-bookworm-slim\n",[245,423,424],{"class":247,"line":254},[245,425,426],{},"#        ↑主版本 ↑Debian 代号(bookworm=12)\n",[15,428,429,430,433],{},"Node 官方 Docker 镜像 tag 命名:",[19,431,432],{},"\u003C主版本>-\u003CDebian 代号>-slim","。slim 是精简版(无构建工具),alpine 是更小的 musl libc(某些 Node 原生模块会崩,谨慎用)。",[24,435],{},[11,437,439],{"id":438},"_7-永远不要用-root-用户跑容器进程","7. 永远不要用 root 用户跑容器进程",[15,441,442],{},"容器默认 root=宿主机 root。一旦你的应用被 RCE 拿到 shell,容器又恰好有 volume 挂载了宿主机目录,等于直接拿到宿主机 root。",[15,444,445,446,449,450,453,454,411],{},"做法:在 Dockerfile 最后阶段创建一个非 root 用户,",[19,447,448],{},"USER"," 切换过去。\nNode 官方 slim 镜像自带 ",[19,451,452],{},"node"," 用户。Python 镜像没有就自己 ",[19,455,456],{},"useradd",[98,458,460],{"className":239,"code":459,"language":241,"meta":106,"style":106},"# Node 镜像内置 node 用户(uid=1000,gid=1000)\nRUN mkdir -p \u002Fapp && chown -R node:node \u002Fapp\nWORKDIR \u002Fapp\nUSER node\nCOPY --chown=node:node --from=builder \u002Fapp\u002Fdist .\u002Fdist\nCMD [\"node\", \"dist\u002Fmain.js\"]\n",[19,461,462,467,472,477,483,489],{"__ignoreMap":106},[245,463,464],{"class":247,"line":248},[245,465,466],{},"# Node 镜像内置 node 用户(uid=1000,gid=1000)\n",[245,468,469],{"class":247,"line":254},[245,470,471],{},"RUN mkdir -p \u002Fapp && chown -R node:node \u002Fapp\n",[245,473,474],{"class":247,"line":260},[245,475,476],{},"WORKDIR \u002Fapp\n",[245,478,480],{"class":247,"line":479},4,[245,481,482],{},"USER node\n",[245,484,486],{"class":247,"line":485},5,[245,487,488],{},"COPY --chown=node:node --from=builder \u002Fapp\u002Fdist .\u002Fdist\n",[245,490,492],{"class":247,"line":491},6,[245,493,494],{},"CMD [\"node\", \"dist\u002Fmain.js\"]\n",[15,496,497,498,501],{},"K8s 侧还要配合 PodSecurityContext ",[19,499,500],{},"runAsNonRoot: true",",双保险。",[24,503],{},[11,505,507,508,511],{"id":506},"_8-加-healthcheck不要等-oom-僵死了-orchestrator-还不知道","8. 加 ",[19,509,510],{},"HEALTHCHECK",",不要等 OOM \u002F 僵死了 orchestrator 还不知道",[15,513,514,515,517],{},"容器进程还活着但已经不响应请求(死锁\u002F卡死)的场景靠 ",[19,516,510],{}," 才能发现。\nK8s 的 liveness\u002Freadiness 探针是另一个层次,容器层加 HEALTHCHECK 本地跑 docker compose 也能看。",[15,519,520],{},"Node\u002FExpress 示例:",[98,522,524],{"className":239,"code":523,"language":241,"meta":106,"style":106},"HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \\\n  CMD curl -fsS http:\u002F\u002F127.0.0.1:3000\u002Fhealth || exit 1\n",[19,525,526,531],{"__ignoreMap":106},[245,527,528],{"class":247,"line":248},[245,529,530],{},"HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \\\n",[245,532,533],{"class":247,"line":254},[245,534,535],{},"  CMD curl -fsS http:\u002F\u002F127.0.0.1:3000\u002Fhealth || exit 1\n",[15,537,538,539,542,543,546],{},"含义:每 30 秒测一次,5 秒超时,启动 10 秒后开始,连续失败 3 次把容器标 unhealthy。\n别忘了 ",[19,540,541],{},"apt-get"," 时装 ",[19,544,545],{},"curl","(slim 默认不带)。",[24,548],{},[11,550,552,553,556],{"id":551},"_9-cmd-用-exec-形式数组别用-shell-形式","9. ",[19,554,555],{},"CMD"," 用 exec 形式(数组),别用 shell 形式",[15,558,559,562,563,566,567,570],{},[138,560,561],{},"shell 形式 👎","(会在外面套一层 ",[19,564,565],{},"\u002Fbin\u002Fsh -c \"...\"",",信号 15 SIGTERM 发不到你的 Node\u002FPython 进程,导致 ",[19,568,569],{},"docker stop"," 等 10 秒硬杀):",[98,572,574],{"className":239,"code":573,"language":241,"meta":106,"style":106},"CMD node dist\u002Fmain.js\n",[19,575,576],{"__ignoreMap":106},[245,577,578],{"class":247,"line":248},[245,579,573],{},[15,581,582,585],{},[138,583,584],{},"exec 形式 👍","(PID=1 就是你的应用,SIGTERM 直接到):",[98,587,588],{"className":239,"code":494,"language":241,"meta":106,"style":106},[19,589,590],{"__ignoreMap":106},[245,591,592],{"class":247,"line":248},[245,593,494],{},[24,595],{},[11,597,599],{"id":598},"_10-完整示例nodejsnuxt-3多阶段-dockerfile","10. 完整示例:Node.js(Nuxt 3)多阶段 Dockerfile",[98,601,603],{"className":239,"code":602,"language":241,"meta":106,"style":106},"# ================ 构建阶段:装 devDep,编译产物 ================\nFROM node:20-bookworm AS builder\n\n# 装构建阶段系统包(有些 native 模块需要 python3\u002Fmake\u002Fgcc)\nRUN apt-get update \\\n  && apt-get install -y --no-install-recommends python3 make g++ \\\n  && rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*\n\nWORKDIR \u002Fapp\n\n# 先只拷锁文件 → 利用缓存,只有依赖变才重新 ci\nCOPY package*.json .\u002F\nRUN npm ci\n\n# 再拷全量代码 → 编译\nCOPY . .\nRUN npm run build\n\n# 最后再装一遍生产依赖(只留 dependencies,去掉 devDep)\nRUN npm prune --omit=dev\n\n# ================ 运行阶段:只拿产物 + 生产依赖 ================\nFROM node:20-bookworm-slim AS runner\n\nLABEL org.opencontainers.image.title=\"yuanxi-docs\"\nLABEL org.opencontainers.image.vendor=\"郑州元析智能科技有限公司\"\n\nENV NODE_ENV=production\nENV PORT=3000\n\n# 装 curl(给 HEALTHCHECK 用) + ca-certificates(HTTPS 请求正常)\nRUN apt-get update \\\n  && apt-get install -y --no-install-recommends curl ca-certificates tini \\\n  && rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*\n\nWORKDIR \u002Fapp\n\n# 用内置的非 root 用户,改一下 \u002Fapp 的所有者\nRUN mkdir -p \u002Fapp && chown -R node:node \u002Fapp\nUSER node\n\n# 从 builder 阶段拷:node_modules(已 prune)+ .output(构建产物)\nCOPY --chown=node:node --from=builder \u002Fapp\u002Fnode_modules .\u002Fnode_modules\nCOPY --chown=node:node --from=builder \u002Fapp\u002F.output    .\u002F.output\n\nHEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \\\n  CMD curl -fsS http:\u002F\u002F127.0.0.1:3000\u002Fapi\u002Fhealth || exit 1\n\nEXPOSE 3000\n\n# tini 做 PID 1 转发信号(解决 Node 子进程僵尸和信号问题)\nENTRYPOINT [\"\u002Fusr\u002Fbin\u002Ftini\", \"--\"]\nCMD [\"node\", \".output\u002Fserver\u002Findex.mjs\"]\n",[19,604,605,610,615,621,626,630,635,640,645,650,655,661,667,673,678,684,690,696,701,707,713,718,724,730,735,741,747,752,758,764,769,775,780,786,791,796,801,806,812,817,822,827,833,839,845,850,856,862,867,873,878,884,890],{"__ignoreMap":106},[245,606,607],{"class":247,"line":248},[245,608,609],{},"# ================ 构建阶段:装 devDep,编译产物 ================\n",[245,611,612],{"class":247,"line":254},[245,613,614],{},"FROM node:20-bookworm AS builder\n",[245,616,617],{"class":247,"line":260},[245,618,620],{"emptyLinePlaceholder":619},true,"\n",[245,622,623],{"class":247,"line":479},[245,624,625],{},"# 装构建阶段系统包(有些 native 模块需要 python3\u002Fmake\u002Fgcc)\n",[245,627,628],{"class":247,"line":485},[245,629,279],{},[245,631,632],{"class":247,"line":491},[245,633,634],{},"  && apt-get install -y --no-install-recommends python3 make g++ \\\n",[245,636,638],{"class":247,"line":637},7,[245,639,289],{},[245,641,643],{"class":247,"line":642},8,[245,644,620],{"emptyLinePlaceholder":619},[245,646,648],{"class":247,"line":647},9,[245,649,476],{},[245,651,653],{"class":247,"line":652},10,[245,654,620],{"emptyLinePlaceholder":619},[245,656,658],{"class":247,"line":657},11,[245,659,660],{},"# 先只拷锁文件 → 利用缓存,只有依赖变才重新 ci\n",[245,662,664],{"class":247,"line":663},12,[245,665,666],{},"COPY package*.json .\u002F\n",[245,668,670],{"class":247,"line":669},13,[245,671,672],{},"RUN npm ci\n",[245,674,676],{"class":247,"line":675},14,[245,677,620],{"emptyLinePlaceholder":619},[245,679,681],{"class":247,"line":680},15,[245,682,683],{},"# 再拷全量代码 → 编译\n",[245,685,687],{"class":247,"line":686},16,[245,688,689],{},"COPY . .\n",[245,691,693],{"class":247,"line":692},17,[245,694,695],{},"RUN npm run build\n",[245,697,699],{"class":247,"line":698},18,[245,700,620],{"emptyLinePlaceholder":619},[245,702,704],{"class":247,"line":703},19,[245,705,706],{},"# 最后再装一遍生产依赖(只留 dependencies,去掉 devDep)\n",[245,708,710],{"class":247,"line":709},20,[245,711,712],{},"RUN npm prune --omit=dev\n",[245,714,716],{"class":247,"line":715},21,[245,717,620],{"emptyLinePlaceholder":619},[245,719,721],{"class":247,"line":720},22,[245,722,723],{},"# ================ 运行阶段:只拿产物 + 生产依赖 ================\n",[245,725,727],{"class":247,"line":726},23,[245,728,729],{},"FROM node:20-bookworm-slim AS runner\n",[245,731,733],{"class":247,"line":732},24,[245,734,620],{"emptyLinePlaceholder":619},[245,736,738],{"class":247,"line":737},25,[245,739,740],{},"LABEL org.opencontainers.image.title=\"yuanxi-docs\"\n",[245,742,744],{"class":247,"line":743},26,[245,745,746],{},"LABEL org.opencontainers.image.vendor=\"郑州元析智能科技有限公司\"\n",[245,748,750],{"class":247,"line":749},27,[245,751,620],{"emptyLinePlaceholder":619},[245,753,755],{"class":247,"line":754},28,[245,756,757],{},"ENV NODE_ENV=production\n",[245,759,761],{"class":247,"line":760},29,[245,762,763],{},"ENV PORT=3000\n",[245,765,767],{"class":247,"line":766},30,[245,768,620],{"emptyLinePlaceholder":619},[245,770,772],{"class":247,"line":771},31,[245,773,774],{},"# 装 curl(给 HEALTHCHECK 用) + ca-certificates(HTTPS 请求正常)\n",[245,776,778],{"class":247,"line":777},32,[245,779,279],{},[245,781,783],{"class":247,"line":782},33,[245,784,785],{},"  && apt-get install -y --no-install-recommends curl ca-certificates tini \\\n",[245,787,789],{"class":247,"line":788},34,[245,790,289],{},[245,792,794],{"class":247,"line":793},35,[245,795,620],{"emptyLinePlaceholder":619},[245,797,799],{"class":247,"line":798},36,[245,800,476],{},[245,802,804],{"class":247,"line":803},37,[245,805,620],{"emptyLinePlaceholder":619},[245,807,809],{"class":247,"line":808},38,[245,810,811],{},"# 用内置的非 root 用户,改一下 \u002Fapp 的所有者\n",[245,813,815],{"class":247,"line":814},39,[245,816,471],{},[245,818,820],{"class":247,"line":819},40,[245,821,482],{},[245,823,825],{"class":247,"line":824},41,[245,826,620],{"emptyLinePlaceholder":619},[245,828,830],{"class":247,"line":829},42,[245,831,832],{},"# 从 builder 阶段拷:node_modules(已 prune)+ .output(构建产物)\n",[245,834,836],{"class":247,"line":835},43,[245,837,838],{},"COPY --chown=node:node --from=builder \u002Fapp\u002Fnode_modules .\u002Fnode_modules\n",[245,840,842],{"class":247,"line":841},44,[245,843,844],{},"COPY --chown=node:node --from=builder \u002Fapp\u002F.output    .\u002F.output\n",[245,846,848],{"class":247,"line":847},45,[245,849,620],{"emptyLinePlaceholder":619},[245,851,853],{"class":247,"line":852},46,[245,854,855],{},"HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \\\n",[245,857,859],{"class":247,"line":858},47,[245,860,861],{},"  CMD curl -fsS http:\u002F\u002F127.0.0.1:3000\u002Fapi\u002Fhealth || exit 1\n",[245,863,865],{"class":247,"line":864},48,[245,866,620],{"emptyLinePlaceholder":619},[245,868,870],{"class":247,"line":869},49,[245,871,872],{},"EXPOSE 3000\n",[245,874,876],{"class":247,"line":875},50,[245,877,620],{"emptyLinePlaceholder":619},[245,879,881],{"class":247,"line":880},51,[245,882,883],{},"# tini 做 PID 1 转发信号(解决 Node 子进程僵尸和信号问题)\n",[245,885,887],{"class":247,"line":886},52,[245,888,889],{},"ENTRYPOINT [\"\u002Fusr\u002Fbin\u002Ftini\", \"--\"]\n",[245,891,893],{"class":247,"line":892},53,[245,894,895],{},"CMD [\"node\", \".output\u002Fserver\u002Findex.mjs\"]\n",[15,897,898,899,902,903,906,907,910],{},"构建命令:",[19,900,901],{},"docker buildx build -t yuanxi-docs:latest .","\n体积检查:",[19,904,905],{},"docker images yuanxi-docs","\n浪费层检查:",[19,908,909],{},"dive yuanxi-docs:latest","(看每层 wasted bytes)",[24,912],{},[11,914,916],{"id":915},"常见问题-faq","常见问题 FAQ",[91,918,920],{"id":919},"q1-多阶段构建里我想拷-builder-里的某个目录copy-from-写了但文件没出现","Q1. 多阶段构建里我想拷 builder 里的某个目录,COPY --from 写了但文件没出现?",[15,922,923,924,927,928,930,931,934,935,938,939,337],{},"检查 builder 里的 ",[19,925,926],{},"WORKDIR"," 和你 ",[19,929,352],{}," 落在哪里。最稳的写法:两个阶段都 ",[19,932,933],{},"WORKDIR \u002Fapp",",builder 阶段把产物生成到 ",[19,936,937],{},"\u002Fapp\u002Fdist",",然后 ",[19,940,941],{},"COPY --from=builder \u002Fapp\u002Fdist .\u002Fdist",[91,943,945],{"id":944},"q2-alpine-镜像不是更小吗为什么你示例里用-bookworm-slim","Q2. Alpine 镜像不是更小吗?为什么你示例里用 bookworm-slim?",[15,947,948,949,62,952,955],{},"Alpine 用 musl libc,不是 glibc。Node 里的原生模块(sharp\u002Fcanvas\u002Fbcrypt 等)如果官方只有 glibc 预编译,在 Alpine 里会报 ",[19,950,951],{},"ELF load command",[19,953,954],{},"invalid pointer"," 这类奇葩错误,排错成本极高。\n**你的依赖全是纯 JS 没有 native addon?用 Alpine 没问题,体积再砍 40%。**否则 bookworm-slim 最省心。",[91,957,959,960,962,963,966],{"id":958},"q3-npm-ci-还是-npm-install","Q3. ",[19,961,356],{}," 还是 ",[19,964,965],{},"npm install","?",[15,968,969,970,411],{},"Dockerfile 里永远 ",[19,971,356],{},[42,973,974,983],{},[45,975,976,978,979,982],{},[19,977,356],{}," 必须有 ",[19,980,981],{},"package-lock.json","(或 npm-shrinkwrap.json),且会严格按 lockfile 装,版本和你本地开发完全一致,不漂移。",[45,984,985,987],{},[19,986,965],{}," 会在 semver 范围内挑新版,今天 build 和下个月 build 出来的镜像依赖不一样,玄学 bug 从这来。",[15,989,990,991,994,995,337],{},"对应 pnpm 是 ",[19,992,993],{},"pnpm install --frozen-lockfile",",yarn 是 ",[19,996,997],{},"yarn install --immutable",[24,999],{},[11,1001,1002],{"id":1002},"更正记录",[42,1004,1005],{},[45,1006,1007],{},"2026-08-09:首版发布。所有 Dockerfile 写法 Docker 27.1.1 实跑,Node 20 项目构建通过。",[1009,1010,1011],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":106,"searchDepth":254,"depth":254,"links":1013},[1014,1015,1020,1023,1029,1030,1032,1034,1035,1037,1039,1040,1046],{"id":13,"depth":254,"text":13},{"id":28,"depth":254,"text":1016,"children":1017},"1. 永远写 .dockerignore(否则 COPY . 会把垃圾全拷进去)",[1018],{"id":93,"depth":260,"text":1019},"Node.js 项目真实可抄的 .dockerignore",{"id":133,"depth":254,"text":134,"children":1021},[1022],{"id":166,"depth":260,"text":167},{"id":221,"depth":254,"text":222,"children":1024},[1025,1027],{"id":225,"depth":260,"text":1026},"原则 A:同类命令写一行,用 && 连,避免缓存层浪费",{"id":309,"depth":260,"text":1028},"原则 B:不要在 RUN 里写 apt upgrade \u002F apt dist-upgrade",{"id":329,"depth":254,"text":330},{"id":362,"depth":254,"text":1031},"5. COPY 优先,不用 ADD",{"id":394,"depth":254,"text":1033},"6. 不要用 latest 标签",{"id":438,"depth":254,"text":439},{"id":506,"depth":254,"text":1036},"8. 加 HEALTHCHECK,不要等 OOM \u002F 僵死了 orchestrator 还不知道",{"id":551,"depth":254,"text":1038},"9. CMD 用 exec 形式(数组),别用 shell 形式",{"id":598,"depth":254,"text":599},{"id":915,"depth":254,"text":916,"children":1041},[1042,1043,1044],{"id":919,"depth":260,"text":920},{"id":944,"depth":260,"text":945},{"id":958,"depth":260,"text":1045},"Q3. npm ci 还是 npm install?",{"id":1002,"depth":254,"text":1002},"docker","2026-08-09","生产环境的 Dockerfile 不是 FROM 一个 node 然后 COPY . 然后 npm run build 就完了。本文讲多阶段构建、.dockerignore、RUN 链式合并、非 root 用户、HEALTHCHECK、.dockerignore 真实示例,最后附 Node.js 完整多阶段 Dockerfile,镜像从 1.2GB 缩到 180MB。",false,"md",{},"\u002Fdocs\u002Fdocker\u002Fdockerfile-best-practice",{"title":5,"description":1049},"dockerfile-best-practice","docs\u002Fdocker\u002Fdockerfile-best-practice",[1058,1059,1060],"90% 的镜像体积浪费来自两个东西:node_modules 里的 devDependencies 和 OS 层的 apt 缓存\u002F源码。多阶段构建=构建镜像专门装依赖编译、运行镜像只拷产物,体积直接砍掉 70%-90%。","Dockerfile 指令顺序决定缓存命中率。越不容易变的写前面(FROM → 装系统包 → COPY package*.json → 装依赖 → COPY 源码),改一行代码只重跑最后 2 步,不是从 apt 开始重来。","10 条硬规则:.dockerignore 必写、RUN apt 一行写完 && rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*、不 latest 标签、非 root 运行、HEALTHCHECK、COPY 比 ADD 好、明确 WORKDIR、单容器单进程、CMD 数组写法、镜像构建完成 dive 扫一遍。",[1062,1063,1064,32],"Dockerfile","镜像优化","多阶段构建","_GFxilJbrKVNUY1wMVqEYW34SFRcEhPH4emWwVXFYzU",[],1794088893186]