news 2026/9/16 22:52:54

Docker多阶段构建实战:从1GB镜像到几十MB的优化指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Docker多阶段构建实战:从1GB镜像到几十MB的优化指南

说实话,我第一次在团队看到同事上传的镜像时,差点没绷住。一个内部小工具,代码量不到两千行,打出来的镜像 1.2GB。问了下 Dockerfile 内容,果然还是老套路:FROM ubuntu,装一堆编译依赖,直接把源码 COPY 进去,顺手把 go build 的中间缓存也留在了镜像里。这个现象不是个例,我见过很多项目直到 CI 磁盘告警、镜像推送越来越慢,才开始认真思考怎么给镜像“减肥”。而不管是后端 Java、Go,还是前端 Node 项目,最优解法基本都是同一个:Docker 多阶段构建。

多阶段构建不是什么新功能,Docker 17.05 就开始支持了,但到现在依然有大量项目没用起来,或者用得很糙。这篇文章我不打算只贴一个 Dockerfile 模板就完事,我会把多阶段构建的原理、语法细节、不同语言的实战写法、缓存优化技巧,还有我踩过的坑和排查思路,全部一次性讲清楚。无论你是刚接触 Docker 的新手,还是已经在生产环境维护镜像的老手,这篇都能给你一些可以参考的东西。

1. 为什么需要多阶段构建:先搞清楚它到底解决了什么问题

1.1 传统单阶段构建的三大痛点

咱们先回到最原始的问题:不用多阶段构建会怎样?最典型的 Dockerfile 写法是这样的:

FROM ubuntu:22.04 RUN apt-get update && apt-get install -y build-essential COPY . /app WORKDIR /app RUN make build CMD ["./app"]

这种写法最大的问题是“什么都往一个镜像里塞”。你为了编译代码,装了 GCC、Make、各种 header 文件,编译完这些工具链根本不运行,但已经写进了镜像层里。最终镜像体积可能比编译出来的二进制大几十倍。仓库里存的是几百 MB 甚至上 GB 的镜像,实际上需要的东西就几十 MB。

再一个痛点是安全。编译工具链里有大量潜在漏洞组件,等于平白无故扩大了攻击面。而且很多项目会把源码、测试文件、CI 配置一起打进镜像里,镜像一旦被拉取,这些东西就相当于公开了。我见过有人把数据库密码写进构建脚本里还打进了镜像,最后镜像推到公共仓库,几个小时就被扫描工具扒了出来。

第三个痛点是构建环境漂移。你用 ubuntu 做基础镜像,今天 apt 源里 GCC 版本是 11,明天可能变成了 12,别人在另外一台机器上构建,可能装到的依赖版本全不一样。最后“在我机器上是好的,在容器里就不行”这种经典问题就来了。单阶段构建把所有动作都堆在一个环境里,环境漂移的风险天然存在。

1.2 多阶段构建的工作原理:一个 Dockerfile,多个 FROM

多阶段构建的思路其实很朴素:一个 Dockerfile 里可以有多个 FROM 指令,每个 FROM 都会开启一个独立的构建阶段。前几个阶段负责编译、打包、生成产物,最后一个阶段负责运行。最终镜像只保留最后一个阶段的内容,之前所有阶段的空间都会被丢弃。

文件如何跨阶段传递?关键就是 COPY --from=xxx 这条指令。你可以把前面某个阶段里生成的文件,直接拷贝到最终阶段里,比如把编译好的二进制、构建好的静态文件拷贝过去。最终镜像里只有这个二进制和你显式指定的运行时环境,干干净净。

用生活类比来理解:多阶段构建就像在中央厨房先把菜做好,然后只把成品菜端到分店卖。分店里不用摆下整个中央厨房的锅碗瓢盆、食材仓库和厨师团队,只需要一个加热设备和成品菜就够了。传统单阶段构建则是把整个中央厨房原封不动搬进分店,后厨还继续开着火,门店能不拥挤吗?

1.3 多阶段构建的实际效果:从 1GB 到几十 MB 并不夸张

我拿自己的项目举例。之前用 Go 写了一个内部 API 服务,单阶段构建用了 golang:1.22 作为基础镜像,装了各种调试工具,最终镜像 850MB。改成多阶段构建后,编译阶段用 golang 镜像,运行阶段用 distroless 镜像,最终产物只有 28MB。没错,从 850MB 降到 28MB,体积缩小了 30 倍。

Java 项目更明显。一个 Spring Boot 应用,单阶段用 maven 镜像直接跑,镜像动辄 500MB 以上。多阶段构建后,编译阶段用 maven 镜像,运行阶段用 jre 镜像,体积能控制在 150MB 左右。再配合 jlink 裁掉用不到的模块,能压到 80MB 以内。

前端项目也一样。React 应用构建出来一堆静态文件,单阶段如果直接用 node 镜像跑,镜像 1GB 都不奇怪。多阶段构建把 npm run build 的产物 COPY 到 nginx 镜像里,最终镜像只有几十 MB。

体积缩小带来的收益是连锁反应:拉取时间变短、启动变快、磁盘占用减少、安全扫描面积变小。这也是为什么多阶段构建值得每一个团队认真实践。

2. 多阶段构建的核心语法与关键细节

2.1 最基本的阶段命名和 COPY --from

多阶段构建的写法门槛其实很低,核心语法就两条:

# 给阶段命名,用 AS 后跟别名 FROM node:20-alpine AS build # 从指定阶段拷贝文件,用 COPY --from=阶段名 COPY --from=build /app/dist /usr/share/nginx/html

第一阶段叫 build,后面可以从 build 阶段拷贝文件。阶段名本质上就是一个指针,指向那个阶段最终文件系统状态。COPY --from 支持从当前 Dockerfile 前面的任何阶段拷贝,也支持从外部镜像拷贝,比如:

COPY --from=nginx:1.25-alpine /etc/nginx/nginx.conf /etc/nginx/nginx.conf

这个能力很实用,有时候你想复用某个基础镜像里已有的配置文件或者二进制,不需要先 RUN 一堆 apt install 自己装,直接 COPY 过来就行。

要注意的点是,阶段名必须在 COPY --from 之前已经定义。Dockerfile 是按顺序执行的,你不可能从后面定义的阶段往前面拷贝。

2.2 每个阶段的上下文是独立的,别想当然共享环境

刚接触多阶段构建的人最容易犯的一个错误是:以为前面阶段的目录、环境变量、文件,后面阶段会自动继承。事实并非如此。每个 FROM 都是全新的文件系统起点,你是基于什么基础镜像,这个阶段就只有什么。两个阶段之间唯一的桥梁就是你显式写的 COPY --from。

这意味着,如果你在编译阶段设置了某个环境变量,比如 GOOS=linux,到了运行阶段这个变量是不存在的,你需要重新声明。如果你在编译阶段安装了 CA 证书,运行阶段如果用的是 scratch 镜像,那 CA 证书也得 COPY 过去。别指望环境能“自动延续”。

2.3 ARG 和 ENV 在阶段间的作用域规则

ARG 和 ENV 的作用域是很多人搞混的地方。ARG 是构建参数,ENV 是环境变量,两者在阶段间传递规则完全不同。

ARG 在 Dockerfile 里如果声明在第一个 FROM 之前,它默认对所有阶段可见,但不作为构建参数传给 RUN 指令,除非你在该阶段内部再声明一次同样的 ARG。举个实际例子:

ARG VERSION=1.0 FROM golang:1.22 AS build ARG VERSION RUN echo "version=$VERSION"

外层的 ARG VERSION 只在外层作用域生效,进入 build 阶段后,如果你不再次声明 ARG VERSION,RUN 指令里就拿不到这个变量。这是 Docker 的一个设计限制:每个阶段都是一个隔离的构建环境,外层 ARG 只是默认值,真正要使用必须在阶段内重新声明。

ENV 则不同,它在当前阶段以及后续阶段都生效,但它不会跨越 FROM 边界。也就是说,你在 build 阶段设置了 ENV GOOS=linux,运行阶段不会继承这个 GOOS。如果需要运行阶段也有某个环境变量,必须在运行阶段再设置一次。

2.4 阶段缓存与构建顺序的关系

Docker 构建缓存是按指令一层层缓存的。一条指令的输入没变,输出就会复用缓存。多阶段构建中,阶段之间的缓存是独立的,每个阶段都有自己独立的层缓存。但这带来一个实际影响:只要前一阶段某条指令变了,后面的指令缓存全部失效。所以在写 Dockerfile 时,指令顺序极其讲究。

依赖下载这种重操作要放在最前面,源码复制放在后面。比如 Node 项目,先 COPY package.json,再 RUN npm ci,最后才 COPY . .。因为源码经常变,而 package.json 很少变。如果先把整个项目 COPY 进去再 npm ci,那每次源码改动都会导致依赖重新下载,构建时间翻几倍。这个优化思路在多阶段构建里面尤为重要,因为编译阶段的缓存失效了,后面所有阶段都跟着受影响。

3. 多阶段构建实战:四种典型场景的完整 Dockerfile

3.1 Java Spring Boot:maven 编译加 jre 运行

Java 项目是多阶段构建收益最明显的场景之一。一个 Spring Boot 项目如果用 maven 镜像全程构建,镜像老大不小。正确做法是 maven 镜像做编译,jre 镜像做运行。

# 第一阶段:编译打包 FROM maven:3.9-eclipse-temurin-17 AS build WORKDIR /app COPY pom.xml . # 提前下载依赖,利用构建缓存 RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests # 第二阶段:运行 FROM eclipse-temurin:17-jre WORKDIR /app # 从编译阶段拷贝 jar 包 COPY --from=build /app/target/*.jar app.jar EXPOSE 8080 # 以非 root 用户运行,降低风险 USER nobody ENTRYPOINT ["java", "-jar", "app.jar"]

这里最关键的优化是 RUN mvn dependency:go-offline。这一行会把 pom.xml 里声明的所有依赖先下载到本地仓库,并生成一个独立的镜像层。只要 pom.xml 没变,这一层就会走缓存,后续源码改动后重新构建时,依赖下载这步会被跳过,构建速度提升非常明显。如果哪天你的 pom.xml 改了,这层缓存失效,依赖就需要重新下载,但源码 COPY 那步还是可以继续走缓存。

运行阶段用 JRE 而不是 JDK,是因为编译工作已经在第一阶段完成了,运行阶段只需要解析字节码,不需要 javac 编译器。eclipse-temurin:17-jre 这类镜像就是专门为运行 Java 应用准备的,体积比 JDK 镜像小很多。加 USER nobody 是为了避免容器内以 root 身份运行,减少安全隐患。

3.2 Node.js 前端:npm 构建加 nginx 托管

前端项目的 Docker 化思路和后端略有不同。现在前端项目构建产物基本是一堆静态文件,托管在 nginx 上即可。多阶段构建可以让我们只把构建产物交给 nginx,源码、node_modules、构建工具全部排除在最终镜像之外。

# 第一阶段:构建静态资源 FROM node:20-alpine AS build WORKDIR /app COPY package.json package-lock.json ./ # npm ci 比 npm install 更严格,锁定依赖版本 RUN npm ci COPY . . RUN npm run build # 第二阶段:nginx 托管 FROM nginx:1.25-alpine # 把构建产物复制到 nginx 默认站点目录 COPY --from=build /app/dist /usr/share/nginx/html # 如果有自定义 nginx 配置,也可以从构建阶段或外部拷贝 COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]

npm ci 和 npm install 的区别值得多说一句。npm ci 严格按照 package-lock.json 安装依赖,版本完全锁定,而且会先删掉 node_modules 再安装,保证构建环境的可复现。这一点在 CI 里尤其重要,因为它能避免“本地没问题,构建机就报错”的版本漂移问题。

前端项目还有一个特殊的优化点:dist 目录通常很小,几百 KB 到几 MB 而已,但 node_modules 可能动辄几百 MB。通过多阶段构建,node_modules 完全不会进入最终镜像。nginx 镜像本身也就几十 MB,最终镜像体积非常健康。

3.3 Go 服务:静态编译加 scratch/distroless

Go 是静态编译语言,非常适合做多阶段构建。因为 Go 能编译出几乎不依赖外部库的静态二进制,最终镜像甚至可以用 scratch,也就是一个空镜像。

# 第一阶段:编译 FROM golang:1.22 AS build WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . # CGO_ENABLED=0 放弃 cgo,生成纯静态二进制 # -ldflags="-s -w" 去掉符号表和调试信息 RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /out/app ./cmd/server # 第二阶段:运行,从 scratch 开始 FROM scratch # CA 证书在 scratch 里不存在,如果程序要访问外部 HTTPS 服务需要带上 COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ COPY --from=build /out/app /app # 非 root 用户 USER 10001:10001 EXPOSE 8080 ENTRYPOINT ["/app"]

这里有两个细节容易被忽略。第一个是 CGO_ENABLED=0。只要程序里没有强制依赖 cgo 的库,关闭 cgo 后编译器会生成纯静态二进制,不需要链接任何动态库,scratch 镜像才能跑。第二个是 CA 证书。如果程序需要调用外部的 HTTPS API,scratch 镜像里没有任何证书文件,必须从基础镜像里把 ca-certificates.crt 拷贝过来。如果不拷贝,程序会在 TLS 握手时报 x509 certificate signed by unknown authority。

USER 10001:10001 是数字形式的 UID,因为 scratch 镜像里根本没有 /etc/passwd 文件,写用户名反而执行不了。

如果觉得 scratch 太极致、不方便调试,可以用 distroless 镜像替代。distroless 是 Google 维护的瘦身版基础镜像,包含运行时基础库和 glibc,但没有 shell、没有包管理器、没有系统工具。相比 scratch,distroless 对动态链接的二进制兼容更好;相比 alpine,它又更纯粹,而且同样是非 root 运行。

3.4 Python 应用:依赖层与运行层的拆分

Python 项目的多阶段构建相对特殊,因为 Python 通常是解释执行,没有明显的“编译”阶段。但多阶段构建依然有用,主要目的是把 pip 安装的依赖和最终运行环境分开,同时避免源码、测试文件、缓存文件进入最终镜像。

# 第一阶段:安装依赖到临时目录 FROM python:3.12-slim AS builder WORKDIR /app COPY requirements.txt . RUN pip install --prefix=/install -r requirements.txt # 第二阶段:运行 FROM python:3.12-slim WORKDIR /app # 把依赖从 /install 拷贝到系统目录 COPY --from=builder /install /usr/local COPY app/ ./app/ # 非 root 用户 RUN useradd -m appuser && chown -R appuser:appuser /app USER appuser EXPOSE 8000 CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

关键点在于 pip install --prefix=/install。这个方法把依赖安装到一个指定目录,然后在第二阶段拷贝过去。好处是最终镜像里不包含 pip 的缓存文件,也不包含任何构建中间产物。这里还可以继续优化,如果项目里有 requirements.txt 之外的本地包依赖,需要相应调整 COPY 路径。

Python 基础镜像选型上,我推荐 slim 变体,它带常见系统依赖但体积小很多。alpine 虽然更小,但 musl libc 和 glibc 的差异偶尔会踩坑,有些 pip 包在 alpine 上装不起来或用不了。做生产镜像时,不要为了追求小体积牺牲兼容性。

4. 多阶段构建的进阶优化:缓存、构建目标与安全性

4.1 用好 BuildKit 的缓存挂载,构建速度再翻一倍

如果你还在用传统 docker build,一定要尽快切换到 BuildKit。BuildKit 是 Docker 的新一代构建引擎,从 Docker 23.0 开始已经是默认构建器。它提供了很多传统构建器没有的能力,其中最实用的就是 RUN --mount=type=cache。

普通多阶段构建里,即使你把依赖下载放在独立的层,只要依赖文件版本更新,缓存依然会失效,依赖需要全部重新下载。使用 BuildKit 的缓存挂载,指定目录会被挂载成一个持久化缓存,即使构建层失效,缓存也不会被删除。举个例子:

FROM maven:3.9-eclipse-temurin-17 AS build WORKDIR /app COPY pom.xml . RUN --mount=type=cache,target=/root/.m2 mvn dependency:go-offline COPY src ./src RUN --mount=type=cache,target=/root/.m2 mvn package -DskipTests

这里把 /root/.m2 目录挂载成缓存。即使 pom.xml 改了导致依赖层失效,之前下载过的依赖仍然在缓存里,新的依赖只需要增量下载,不需要全部重来。Go 项目同理:

RUN --mount=type=cache,target=/go/pkg/mod go mod download

前端项目对应的是 npm 缓存目录 /root/.npm。

BuildKit 还有另一个实用功能是 RUN --mount=type=secret,可以把敏感信息通过临时文件挂载到构建环境里,而不会写进镜像层。比如构建时要用到私有仓库的 token,可以这样做:

RUN --mount=type=secret,id=github_token \ GITHUB_TOKEN=$(cat /run/secrets/github_token) \ npm ci

构建时用 --secret id=github_token,src=./.token 传入。这样做敏感信息不会残留在镜像层里,比用 --build-arg 传密码安全得多。

4.2 利用 --target 参数调试单个阶段

多阶段构建的每个阶段其实都可以单独构建和调试。如果你只想构建中间的某个阶段,不需要完成最终镜像,可以用 --target 参数指定。

docker build --target build -t myapp:build .

这个用法在实际排障中非常有用。比如最终镜像启动失败,你怀疑问题出在编译阶段产出的二进制,可以直接构建 build 阶段,然后临时写一个 Dockerfile 基于这个阶段运行,看二进制能不能正常工作。不用每次都要完成整个多阶段流程才能在容器里实验。

还有一个高阶用法:一个 Dockerfile 里定义多个运行目标,用 --target 切换。典型的场景是同一个后端项目,开发环境想用带热加载和调试工具的镜像,生产环境想用精简的运行镜像。你可以在一个 Dockerfile 里写好 dev、test、prod 三个阶段,然后用 --target 选择构建哪个镜像。这样版本管理和维护成本都会降低。

4.3 安全加固:最小化基础镜像与权限控制

多阶段构建天然比单阶段构建安全,因为它剥离了构建工具链和大部分非必要文件。但就算用了多阶段构建,最终阶段如果还是从 ubuntu:latest 开始,镜像依然臃肿且攻击面不小。我建议把最终阶段的基础镜像也细化一下。

  • Java 运行时,用 eclipse-temurin 的 jre 变体,不要用完整 JDK
  • Go 静态二进制,用 scratch 或 distroless
  • 前端静态文件,用 nginx:alpine,体积小且足够稳定
  • Python 应用,用 python:slim 并注意依赖兼容

权限控制是另一个容易被忽略的点。很多项目的基础镜像默认是 root 用户运行,这导致容器一旦被攻破,攻击者直接拥有宿主机 root 权限的风险很高。在多阶段构建的最终阶段,尽量加上 USER 指令切换成非 root 用户。可以参考前面的各场景 Dockerfile,已经包含了 USER 相关配置。

4.4 镜像体积优化技巧:除了多阶段还能做什么

多阶段构建解决了“编译工具链进镜像”的问题,但镜像是还可以再瘦身的。这里有几个和构建协同使用的优化点:

减少镜像层数。多个 RUN 指令会生成多个镜像层,每层都会永久保存。可以把相关命令合并到同一条 RUN 里,用 && 连接,并在最后清理缓存文件。比如 apt 安装完顺手把 /var/lib/apt/lists 删掉。前端 npm ci 完顺手删掉 npm 缓存目录。

注意 COPY 的粒度。尽量不要 COPY . . 一股脑把所有文件拷进去。Docker 会把整个构建上下文打包发送给守护进程,不必要的文件既拖慢构建速度,也容易把敏感文件带进镜像。可以用 .dockerignore 文件过滤掉 node_modules、target、.git、*.md 等不需要的文件。这个文件对构建的影响不亚于 Dockerfile 本身,建议每个项目都要维护。

多阶段的层数不是越少越好。从镜像体积角度看,层越少体积越小(合并优化效果有限),但从缓存利用率和构建可维护性角度看,适度分阶段是好事情。核心矛盾在于“最终镜像体积”和“构建缓存效率”两个目标。最佳平衡点是根据团队实际构建频率和代码变更模式来确定。

5. 常见问题与排查技巧实录

5.1 常见问题速查表

症状可能原因排查思路解决方案
构建缓存一直不生效,每次全量重来COPY 指令顺序靠前,源码变化导致后续缓存失效docker build --progress=plain查看哪些层走了缓存把依赖相关 COPY 和安装提前,项目源码放最后
COPY --from 报错找不到阶段阶段名拼错或者提前引用后面定义的阶段检查 Dockerfile 各阶段的 AS 命名是否一致且唯一统一阶段命名规范,确保 COPY 指令在该阶段定义之后
镜像体积减不下来最终阶段基础镜像太大,或者 COPY 了多余文件用 docker history 看每层大小换更小的基础镜像,检查 COPY 的源路径是否包含多余文件
容器启动报证书错误scratch/distroless 镜像没有 CA 证书确认程序是否访问外部的 HTTPS 接口从认证过的镜像阶段 COPY 证书文件
docker build 很慢,依赖下载每次都重来没有使用 BuildKit 缓存挂载检查 docker buildx 是否启用用 RUN --mount=type=cache,target=... 挂载依赖目录
多阶段构建的 ARG 拿不到值外层 ARG 没有在各阶段内部重新声明检查变量作用域在每个阶段内再次声明 ARG
最终镜像里还有源码没有正确使用 .dockerignore 或 COPY 了过多文件检查构建上下文和 COPY 指令细化 .dockerignore,COPY 只拉取需要的目录
容器以 root 运行,有安全风险最终阶段没有 USER 指令切换用户docker inspect 查看容器的 User在最终阶段加 USER 指令,或使用数字 UID

5.2 排查技巧:用 docker history 定位大文件

镜像体积没降下来时,第一步不是猜,而是看每一层到底占了多少空间。

docker history --no-trunc=true <镜像名>

这个命令会按时间倒序列出镜像的所有层以及每层大小。哪一层体积异常大,问题就出在哪一层对应的 Dockerfile 指令。比如你看到某层有 200MB,而上一条指令是 RUN pip install,基本就能断定是把安装在最终阶段跑了,应该移到构建阶段。或者看到了 COPY 一个 300MB 目录的层,那赶紧检查 COPY 的源路径和 .dockerignore。

5.3 本地复现 Docker build 异常环境的小技巧

如果你遇到构建时某个命令的报错,想进到该指令对应阶段的中间状态去排查,可以用一个临时的 Dockerfile 配合 --target 来重建现场。比如我的 Java 项目在 maven 打包那一步失败,想看看容器里 /root/.m2 的情况,可以临时把该阶段的 ENTRYPOINT 覆盖成 sleep,然后 docker exec 进去看:

docker build --target build -t app-build-debug . docker run -it --rm app-build-debug bash

这个技巧排查多阶段构建问题特别高效,尤其是在依赖下载、编译报错这类场景。不用反复改 Dockerfile,直接进容器手动跑指令,看到什么都能立刻定位。

5.4 一个真实的构建缓存踩坑案例

我某次调整前端项目 Dockerfile,发现改了源码后构建依然要 3 分钟以上,不对劲。用 buildkit 的 plain 模式跑了一次构建,看到 npm ci 这步每次都在重新执行。排查后发现问题出在这条指令:

COPY . . RUN npm ci

虽然第一眼是 COPY . . 放在 npm ci 之前,但 npm ci 本身不依赖生产代码,把 COPY . . 放前面导致任何源码改动都会触发 npm ci 缓存失效。修正方案是调整顺序:

COPY package.json package-lock.json . RUN npm ci COPY . .

调整后,源码改动后 npm ci 层能直接命中缓存,构建时间从 3 分钟降到了 40 秒以内。这个例子说明,多阶段构建不仅仅是把阶段分开那么简单,每个阶段内部的指令顺序也需要精心设计。

写在最后的个人体会

多阶段构建给我带来的最大收益,不是某个项目从 1GB 变成 30MB 的“爽感”,而是它逼着你去思考容器里真正需要什么。每次写 Dockerfile 时先问一句:“这个阶段是为构建服务,还是为运行服务?最终产物到底是什么?”想清楚之后,镜像体积、构建速度、安全性这些问题会跟着解决掉大半。

如果你还没开始用多阶段构建,建议拿手头的一个旧项目练手。不用追求一步到位,先在原有 Dockerfile 基础上拆出编译阶段和运行阶段,把产物 COPY 过去,体验一下从 1GB 到几百 MB 的过程。熟悉之后再逐步引入 BuildKit 缓存、dockerignore 瘦身、非 root 运行等进阶手段。改完之后把前后的镜像体积和构建时间记下来,这个数据会让你和你的团队都重新审视 Dockerfile 的价值。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 22:51:32

用Inno Setup将MySQL、JDK和Spring Boot JAR打包成一键安装包

做Java桌面应用或者内部系统交付的时候&#xff0c;我猜不少人都被“部署环境”折磨过。客户电脑上没有JDK&#xff0c;要么自己手动下一个&#xff0c;要么让客户装&#xff1b;MySQL更麻烦&#xff0c;安装包一步步点&#xff0c;root密码、字符集、端口&#xff0c;稍不注意…

作者头像 李华
网站建设 2026/9/16 22:50:46

高精度电流检测:LTS6-NP与R7KA8D2KFLCAC闭环霍尔方案实战指南

1. 项目概述&#xff1a;这不是“测电流”&#xff0c;而是重构高精度电能计量的底层逻辑你手头刚拆开一个工业级变频器控制板&#xff0c;发现主回路旁赫然贴着两颗黑色小方块——LTS 6-NP 和 R7KA8D2KFLCAC。它们不带散热片、不接大线缆&#xff0c;却稳稳坐在功率MOSFET驱动…

作者头像 李华
网站建设 2026/9/16 22:49:27

企业级智能体效能管理:可度量、可审计、可追责的落地指南

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

作者头像 李华
网站建设 2026/9/16 22:48:08

Mac Mouse Fix:让普通鼠标好过触控板的 macOS 鼠标优化完整指南

Mac Mouse Fix&#xff1a;让普通鼠标好过触控板的 macOS 鼠标优化完整指南 【免费下载链接】mac-mouse-fix Mac Mouse Fix - Make Your $10 Mouse Better Than an Apple Trackpad! 项目地址: https://gitcode.com/GitHub_Trending/ma/mac-mouse-fix 如果你一直在用普通…

作者头像 李华
网站建设 2026/9/16 22:47:11

OmDet模型ONNX/TensorRT推理实战:动态路由与多尺度融合优化

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

作者头像 李华
网站建设 2026/9/16 22:46:41

Windows蓝屏代码全解析:13个高频停止代码与自救排查指南

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

作者头像 李华