从零写出生产级 Dockerfile:核心指令 + 层缓存 + 多阶段构建,告别臃肿镜像
本文首发于 栏轩·阁欢迎访问阅读原文获取更好的阅读体验。一、Dockerfile 是什么Dockerfile 是一个文本文件里面写的是怎么构建一个镜像的指令。FROM alpine:latest RUN echo hello /tmp/test CMD [cat, /tmp/test]每一行指令最终会变成镜像里的一层。Docker 按顺序执行这些指令最终产出可以运行的镜像。做一个 Dockerfile 需要什么Docker 已安装✅一个文本编辑器一个想容器化的应用没有就用个脚本练手二、核心指令先跑起来1.FROM— 基础镜像一切从这里开始每个 Dockerfile 必须以FROM开头。FROM alpine:latest # Alpine Linux极小 (~7MB) FROM python:3.12-slim # Python 运行环境 FROM nginx:alpine # Nginx Alpine FROM ubuntu:22.04 # Ubuntu 完整系统 FROM scratch # 空镜像从零开始基础镜像决定了你的镜像起点有多大基础镜像大小适用场景alpine~7 MB静态二进制、轻量服务python:3.12-slim~50 MBPython 应用nginx:alpine~27 MBWeb 服务ubuntu~80 MB需要完整系统工具2.RUN— 构建时执行命令RUN apk add --no-cache curl RUN pip install -r requirements.txt RUN echo 构建时执行 /tmp/test关键点RUN在构建时执行每一行RUN都会增加一层。为什么合并 RUN 和清理缓存很重要先看一个实验# ❌ 两个 RUN先创建文件再删除 FROM alpine:latest RUN dd if/dev/zero of/bigfile bs1M count10 # 创建 10MB 文件 RUN rm /bigfile # 删除这个文件构建结果23.4 MB原因用docker history看每一层SIZE 层内容 10.5MB 第 1 个 RUN创建 /bigfile10MB 文件在这层留下了 4.1kB 第 2 个 RUNrm /bigfile只是记了个删了文件还在上一层每一层都是一个独立的快照。第 2 层的删除只是在第 2 层标记了已删除但第 1 层的 10MB 文件依然存在。第 2 层记了已删除 ← 这只是个标记 第 1 层10MB 文件 ← 文件还在 基础镜像文件只是被挡住了不是真的没了。这就是为什么镜像比你想象的大。# ✅ 一个 RUN创建并删除文件没进过镜像 FROM alpine:latest RUN dd if/dev/zero of/bigfile bs1M count10 rm /bigfile构建结果12.9 MB差了一倍SIZE 层内容 4.1kB 同一个 RUN 创建又删除文件从来没进入任何一层放到apt的场景也是一样的道理# ❌ 错误写法 RUN apt-get update # 把包列表下载到 /var/lib/apt/lists/~30MB RUN apt-get install -y curl # 装 curl RUN rm -rf /var/lib/apt/lists/* # 在不同层删除包列表 # 实际30MB 的包列表还在上一层最终镜像白白多了 30MB # ✅ 正确写法 RUN apt-get update apt-get install -y curl rm -rf /var/lib/apt/lists/* # 下载→安装→删除 在同一层包列表没进过任何一层原则同一个逻辑的操作放在一个RUN里用连接。3.CMD— 容器启动时执行的命令CMD [python, app.py] # ✅ 推荐写法JSON 数组格式 CMD python app.py # ❌ shell 格式不推荐CMD和RUN最大的区别指令什么时候执行执行几次RUN构建时执行1 次构建时CMD运行时执行docker run每次启动容器都执行测试验证FROM alpine:latest RUN echo RUN构建时执行 CMD [echo, CMD运行时执行]# 构建时RUN 执行dockerbuild-ttest.# 运行时CMD 执行dockerrun--rmtest# 输出CMD运行时执行4.WORKDIR— 工作目录WORKDIR /app # 之后的 COPY、RUN、CMD 都在 /app 目录下执行WORKDIR等价于cd但更好——目录不存在时会自动创建。WORKDIR /app # 自动创建 /app COPY app.py . # 复制到 /app/app.py RUN pwd # 输出 /app CMD [python, app.py] # 从 /app 启动5.COPY— 把文件放进镜像# 把本地的 index.html 复制到镜像的 /usr/share/nginx/html/ COPY index.html /usr/share/nginx/html/ # 配合 WORKDIR 使用更简洁 WORKDIR /usr/share/nginx/html COPY index.html . # 复制到当前工作目录COPYvsADD指令区别COPY只复制文件不做其他事。推荐优先用ADD复制文件 自动解压 tar 支持 URL。只在需要解压时用三、层缓存 — 为什么顺序很重要Docker 的构建是分层的。每一层构建完会缓存起来下次同一层没有变化就直接用缓存。错误写法改代码就要重装依赖COPY . . RUN pip install -r requirements.txt # ← 代码改了依赖也要重装正确写法依赖放前面代码放后面COPY requirements.txt . # ← requirements.txt 不常变走缓存 RUN pip install -r requirements.txt # ← 走缓存不用重装 COPY . . # ← 只重新复制代码验证效果# 第一次构建正常安装dockerbuild-tmyapp.# pip install... 耗时 20s# 第二次构建代码改了依赖没改dockerbuild-tmyapp.# COPY requirements.txt → CACHED# RUN pip install → CACHED ← 走缓存# COPY . → 重新复制代码# 总耗时 1秒原则把不常变的东西放前面常变的东西放后面。四、多阶段构建 — 镜像瘦身的核心技巧“编译环境要大没关系运行环境要小”。用多个FROM每个FROM是一个阶段。最后只取需要的产物。场景Go 语言应用# 阶段 1编译 FROM golang:alpine AS build # 364 MB包含 Go 编译器 WORKDIR /src COPY server.go . RUN go build -o /server server.go # 阶段 2运行 FROM alpine:latest # ~7 MB只有系统 COPY --frombuild /server /server CMD [/server]关键指令COPY --from从其他阶段复制文件而不是从宿主机。COPY --frombuild /server /server # 从 build 阶段复制 COPY --fromnginx:alpine /usr/share/nginx/html/index.html /index.html # 从其他镜像复制最终效果镜像大小golang:alpine仅编译用364 MB丢弃alpine:latest基础系统7 MB最终产物21 MB编译需要的 Go 编译器、工具链全部被丢掉了只留编译好的二进制。适用于任何语言# Python 也可以多阶段 FROM python:3.12-slim AS builder COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt FROM python:3.12-slim COPY --frombuilder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages COPY app.py . CMD [python, app.py]多阶段构建的核心思想每个阶段各司其职最终产物只保留运行时需要的东西。五、进阶指令1.ARGvsENV— 两种变量指令作用时间作用域覆盖方式ARG构建时只在docker build时存在--build-arg VERSION2.0ENV运行时容器启动后仍然存在-e APP_ENVdevARG VERSION1.0 # 默认值 1.0构建时可覆盖 ENV APP_ENVproduction # 运行时的环境变量 # 如果需要把 ARG 的值带到运行时赋值给 ENV ARG VERSION ENV APP_VERSION$VERSION# 构建时修改 ARGdockerbuild --build-argVERSION2.0-tmyapp.# 运行时修改 ENVdockerrun-eAPP_ENVdevelopment myapp2.ENTRYPOINTvsCMD— 入口 vs 默认参数两个都是容器启动时执行的命令区别在于能不能被覆盖。ENTRYPOINT [echo] # 主程序固定不可变 CMD [Hello, World!] # 默认参数可以被覆盖运行方式效果docker run image执行echo Hello, World!docker run image Hi执行echo Hi覆盖了 CMDdocker run --entrypoint bash image覆盖 ENTRYPOINT组合模式# ENTRYPOINT 是这个容器是干什么的 # CMD 是默认怎么干 ENTRYPOINT [python] CMD [app.py]3.HEALTHCHECK— 健康检查Docker 默认只看进程是否活着不管服务能不能用。HEALTHCHECK让 Docker 主动检查你的服务。HEALTHCHECK --interval10s --timeout3s --retries2 \ CMD curl -f http://localhost:5000/ || exit 1参数说明--interval每隔多久检查一次--timeout每次检查超时时间--retries连续失败几次算不健康--start-period启动后等多久再开始检查查看健康状态dockerinspect 容器名--format{{.State.Health.Status}}# starting → healthy / unhealthy4.EXPOSE— 声明端口只是文档EXPOSE 8080仅仅是个说明告诉用户这个容器监听 8080 端口。不真的暴露端口。要暴露还是要加-p。5.USER— 别用 root生产环境不要用 root 运行应用。但USER app不会自动创建用户必须先创建再切换FROM alpine:latest # 第一步创建用户 RUN addgroup -S app adduser -S app -G app # 第二步切换到该用户 USER app常见错误只USER不创建USER nonexistent # ← 这个用户不存在构建不会报错但运行时直接挂掉docker: Error response from daemon: unable to find user nonexistent不同基础镜像创建用户的命令基础镜像创建用户命令Alpineadduser -S app -G appUbuntu/Debianuseradd -r -s /bin/false app通用RUN addgroup -S app adduser -S app -G app六、完整的生产级 Dockerfile实际项目下面两个是我实际生产在用的 Dockerfile分别对应 Java 后端和 Next.js 前端。Java / Spring Boot 后端# ── 阶段 1编译 ── # Maven JDK 21 的构建环境镜像很大但只是临时用 FROM maven:3.9-eclipse-temurin-21 AS build # 设置工作目录之后所有命令都在 /app 下执行 WORKDIR /app # 先复制 pom.xml利用层缓存pom.xml 不改就不重装依赖 COPY pom.xml . # 复制 Maven 私服配置如果有私有仓库 COPY .mvn/settings.xml settings.xml # 离线下载所有依赖go-offline 会下载 pom.xml 定义的所有 jar 包但只下载不编译 RUN mvn dependency:go-offline -B -s settings.xml # pom.xml 和 settings.xml 的层缓存命中后才复制源码 COPY src ./src # 真正编译打包跳过测试 RUN mvn clean package -DskipTests -B -s settings.xml # ── 阶段 2运行 ── # 只用 JREJava 运行环境没有编译器小得多 FROM eclipse-temurin:21-jre WORKDIR /app # 从 build 阶段复制打好的 jar 包 COPY --frombuild /app/target/ROOT.jar app.jar # 声明容器内端口只起文档作用真要暴露还得 docker run -p EXPOSE 8080 # 容器启动命令JSON 数组格式正确接收 SIGTERM 信号 ENTRYPOINT [java, -jar, /app/app.jar]用了什么技巧技巧对应指令多阶段构建FROM maven:... AS build→FROM eclipse-temurin:21-jre层缓存加速COPY pom.xml在前COPY src在后依赖只装一次JSON 格式ENTRYPOINT [java, -jar, app.jar]✅产物分离编译阶段 2GB运行阶段只用 JRE小得多Next.js 前端# ── 阶段 1构建 ── # Node.js 22 的编译环境装依赖 打包 FROM node:22-alpine AS builder WORKDIR /app # 先复制依赖描述文件利用层缓存加速 COPY package.json package-lock.json ./ # npm ci 比 npm install 更快更严格完全按 lock 文件安装 RUN npm ci # 复制所有源码这层常变但上面的依赖层已经缓存了 COPY . . # 删除 postbuild 脚本避免构建时触发额外操作然后构建 RUN npm pkg delete scripts.postbuild npm run build # ── 阶段 2运行 ── FROM node:22-alpine AS runner WORKDIR /app # 设置生产环境变量 ENV NODE_ENVproduction # 只复制运行需要的文件不要 node_modules 里的 devDependencies COPY --frombuilder /app/.next/standalone ./ COPY --frombuilder /app/.next/static ./.next/static COPY --frombuilder /app/public ./public # 声明端口文档作用 EXPOSE 3000 # 启动 Node.js 服务 CMD [node, server.js]用了什么技巧技巧对应指令多阶段构建builder装依赖 构建runner只跑产物层缓存package.jsonpackage-lock.json先复制npm ci走缓存按需复制只复制运行需要的文件standalone、static、publicnpm ci比npm install更快更严格锁定版本七、总结指令速查表指令作用执行时机FROM指定基础镜像必需第一行RUN执行命令构建时构建时CMD容器启动命令运行时COPY复制文件到镜像构建时WORKDIR设置工作目录构建时ARG构建时变量构建时ENV运行时环境变量构建时 运行时ENTRYPOINT容器入口程序运行时HEALTHCHECK健康检查运行时EXPOSE声明端口文档—USER切换用户构建时 运行时Dockerfile 最佳实践原则说明不常变的放前面利用层缓存加速构建每个 RUN 只做一件事但同一件事的多个命令用合并减少层数多阶段构建编译环境和运行环境分离镜像体积差 10 倍以上不用 root 运行建一个普通用户USER app不要用ADD除非要解压优先用COPY.dockerignore过滤掉node_modules、.git等无用文件尽量用 Alpine基础镜像越小越好

相关新闻

机械臂Agent开发:代码结构优化与通信协议设计实践

机械臂Agent开发:代码结构优化与通信协议设计实践

1. 机械臂Agent开发中的代码结构优化实践在机械臂控制系统的开发过程中,随着功能模块的不断增加,原始的代码结构往往会变得臃肿不堪。我最近在重构一个三轴机械臂的Agent控制程序时,深刻体会到良好的代码结构对项目可维护性的重要性。以常见的…

2026/10/8 18:20:20 阅读更多 →
LLM推理成本优化:从黑盒调用到白盒调优的工程实践

LLM推理成本优化:从黑盒调用到白盒调优的工程实践

1. 项目概述:当LLM推理成本成为业务瓶颈最近和几个负责AI产品线的朋友聊天,大家不约而同地提到了同一个痛点:大模型(LLM)的推理成本。无论是提供在线问答服务、内容生成,还是作为智能体(Agent&a…

2026/10/13 11:39:57 阅读更多 →
如何用NS-USBloader一站式管理你的Switch游戏:5大功能全解析

如何用NS-USBloader一站式管理你的Switch游戏:5大功能全解析

如何用NS-USBloader一站式管理你的Switch游戏:5大功能全解析 【免费下载链接】ns-usbloader Awoo Installer and GoldLeaf uploader of the NSPs (and other files), RCM payload injector, application for split/merge files. 项目地址: https://gitcode.com/gh…

2026/10/3 12:03:44 阅读更多 →

最新新闻

SpringBoot打包全攻略:从可执行Jar原理到部署实战

SpringBoot打包全攻略:从可执行Jar原理到部署实战

一开始接触SpringBoot打包时,我犯过一个很低级的错误:在IDE里点了几下Build,拿到target目录下的jar直接扔到服务器上执行,结果报了“no main manifest attribute”,当时整个人是懵的。后来才知道,SpringBoo…

2026/10/13 11:40:13 阅读更多 →
AI助手会话记录批量导出实战:从API到浏览器脚本的完整方案

AI助手会话记录批量导出实战:从API到浏览器脚本的完整方案

1. 把需求掰开揉碎:你要的批量导出到底是什么时不时就有人来问:能不能把AI助手左侧那一长串会话记录一次性导出?注意,这里的“会话”指的是左边列表里的每一个独立条目,而不是某个会话里的几十轮来回。每条会话都有自己…

2026/10/13 11:39:13 阅读更多 →
OneLive私有化直播系统搭建全攻略:从推流到播放的完整实践

OneLive私有化直播系统搭建全攻略:从推流到播放的完整实践

第一次把 OneLive 这个名字写在架构图上,是在帮一个朋友做线上分享活动的时候。他当时需要的不是某个直播平台的账号,而是一套能装在自有服务器上、由自己完全控制推拉流、录制和权限的直播项目。OneLive 这个名字就这么随手起了:一场直播、一…

2026/10/13 11:39:13 阅读更多 →
Open Code Review:从形式评审到协作基础设施的范式升级

Open Code Review:从形式评审到协作基础设施的范式升级

1. “open-code-review”不是个工具,而是一套被低估的协作范式你有没有遇到过这样的场景:团队里新来一位同事,第一天就提了三份PR,每份都改了二十多个文件,commit message写的是“fix bug”;或者自己花三天…

2026/10/13 11:39:13 阅读更多 →
基于人脸识别的照片视频管理器:C#稳定版64位实现

基于人脸识别的照片视频管理器:C#稳定版64位实现

简介:这是一款用C#编写的本地照片与视频管理工具,面向需要按人物整理个人影像资料的普通用户与开发者。它支持jpg、jpeg、png、bmp等图片格式以及avi、mp4、mpg、mpeg等视频格式,核心能力包括人脸库录入、照片视频人脸自动识别,以…

2026/10/13 11:39:13 阅读更多 →
AI编程智能体分级:决策权归属决定项目成败

AI编程智能体分级:决策权归属决定项目成败

1. 为什么“智能体分级”不是技术炫技,而是项目落地的生死线最近帮某高校实验室做一套教学辅助系统时,团队最初信心满满:用最新开源大模型Agent框架,两周就能跑通自动出题、批改、反馈闭环。结果卡在第三天——学生提交一道开放性…

2026/10/13 11:39:13 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →