在 Docker 多阶段构建出现之前,想要得到一个小体积的运行时镜像通常要费不少功夫。常见的做法是先在同一个镜像里安装编译器、构建依赖和源码,执行完构建后再用 RUN 命令手动删除不需要的目录。但 Docker 分层存储的特性决定了这些删除操作并不会真正减小镜像体积,反而会让构建历史变得混乱。多阶段构建的出现改变了这一局面,而其中最核心的指令之一就是 COPY --from。

COPY --from 允许当前构建阶段从之前已经完成的阶段中复制文件,不再局限于从宿主机构建上下文读取内容。这样一来,编译阶段可以放心使用完整工具链,运行时阶段只复制最终产物,构建流程更清晰,镜像体积也能得到明显控制。
一、COPY --from 的两种引用方式与基础语法
COPY --from 的语法并不复杂,它是在标准 COPY 指令基础上增加了 --from 参数。这个参数的值可以是阶段索引,也可以是阶段名称。阶段索引从 0 开始计数,对应 Dockerfile 中第一个 FROM 指令;阶段名称则通过 AS 关键字来定义。下面是一个最简单的 Go 项目示例,builder 阶段负责编译,最终镜像只复制编译好的二进制文件。
FROM golang:1.22 AS builder WORKDIR /app COPY . . RUN go build -o server . FROM alpine:3.20 WORKDIR /app COPY --from=builder /app/server . CMD ["./server"]
这里最终镜像基于 alpine,不会包含 Go 工具链和源码,因此体积通常比直接使用 golang 镜像小很多。使用 builder 这个别名后,即使后面调整了构建阶段的顺序,只要别名不变,COPY --from=builder 依然能正确找到源阶段。数字索引虽然也能工作,但可读性差,而且一旦在 Dockerfile 前面插入新的阶段,索引就会整体偏移,维护起来更容易出错。
除了引用前序阶段,COPY --from 也可以引用其他已经完成的命名阶段。实际编写时建议始终使用 AS 定义清晰的名字,例如 build、deps、assets,这样每个复制操作的意图一目了然,也能减少阶段顺序变动带来的风险。
二、常见实战:编译型服务与前端静态资源分离
编译型语言是 COPY --from 最典型的应用场景。以 Java Maven 项目为例,构建阶段需要完整的 JDK 和 Maven,而运行时只需要 JRE。通过多阶段构建可以把两者彻底分开。
FROM maven:3.9-eclipse-temurin-21 AS build WORKDIR /project COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn package -DskipTests FROM eclipse-temurin:21-jre WORKDIR /app COPY --from=build /project/target/app.jar app.jar EXPOSE 8080 ENTRYPOINT ["java","-jar","app.jar"]
这个示例里,build 阶段先复制 pom.xml 并下载依赖,再复制源码进行打包,这样的层次安排还能利用 Docker 层缓存。只要 pom.xml 不变,依赖下载层就不会重复执行,后续源码变化时构建速度会更快。最终阶段只复制 app.jar,不包含 Maven 缓存和源码,镜像体积可以缩小几百 MB。
前端项目同样适合这种模式。Node 镜像负责安装依赖和构建静态资源,Nginx 镜像只接收 dist 目录。下面是一个典型的 Vue 或 React 项目构建示例。
FROM node:20-alpine AS builder WORKDIR /workspace COPY package*.json ./ RUN npm ci COPY . . RUN npm run build FROM nginx:1.27-alpine COPY --from=builder /workspace/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/nginx.conf EXPOSE 80
最终 Nginx 镜像不会携带 node_modules 和构建工具,只包含编译后的静态文件,既减小了体积,也降低了生产环境的攻击面。这种构建与运行分离的思路在微服务架构中非常常见,能让部署更轻量。
三、权限、路径和目录复制容易忽略的细节
COPY --from 在复制文件时会尽量保留源文件的元数据,包括权限和属主信息。如果构建产物原本以 root 身份生成,直接复制到最终镜像后也会保持 root 权限。如果容器计划以非 root 用户运行,就需要在复制时调整属主。Docker 提供了 --chown 参数,可以放在 --from 前面一起使用。
FROM alpine:3.20 RUN addgroup -S app && adduser -S app -G app WORKDIR /app COPY --chown=app:app --from=builder /build/output/server . USER app CMD ["./server"]
注意 --chown 必须在 --from 之前,如果位置写反,Docker 会报错。生产环境中建议始终明确指定文件属主,避免因为权限不足导致进程无法启动,或者因为文件权限过宽带来安全隐患。
路径解析也是容易出错的地方。COPY --from 里的源路径是相对于指定阶段的文件系统根目录,而不是宿主机路径。如果写成了宿主机绝对路径,构建会直接失败。复制目录时,目标路径结尾是否带斜杠也会影响结果:源目录带斜杠表示复制目录内容,不带斜杠则会把整个目录复制到目标路径下。例如 COPY --from=builder /app/dist/ /usr/share/nginx/html/ 和 COPY --from=builder /app/dist /usr/share/nginx/html 最终结构不同,前者是 html 下直接放静态文件,后者会多出一层 dist 目录。这个细节在排查静态资源 404 问题时经常被忽略。
四、从外部镜像复制与构建缓存的关系
COPY --from 不仅可以引用当前 Dockerfile 中已定义的阶段,还能从任意镜像中复制文件。这个特性在某些场景下非常实用,例如需要从官方镜像提取默认配置文件,或者从工具镜像中取一个独立可执行文件。
FROM alpine:3.20 COPY --from=nginx:1.27-alpine /etc/nginx/nginx.conf /etc/nginx/nginx.conf COPY --from=busybox:1.36 /bin/busybox /bin/busybox CMD ["/bin/busybox","httpd","-f","-p","8080"]
这种用法让 Dockerfile 具备了类似依赖抽取的能力。比如你想基于 Alpine 做一个带 Nginx 配置的应用镜像,又不想安装完整 Nginx 包,就可以从官方 nginx 镜像中把配置文件和相关二进制复制过来。但要注意,从外部镜像复制的内容不会自动包含其依赖库,如果复制了动态链接的可执行文件,还需要确保目标镜像中存在对应的动态链接器。
在构建缓存方面,多阶段构建的每个阶段都有独立的缓存。当前阶段执行 COPY --from 时,如果源阶段的内容发生变化,当前层会重新构建。BuildKit 会跟踪阶段之间的依赖关系,当源阶段重算后,依赖它的 COPY 层也会失效。为了充分利用缓存,建议把不常变化的依赖复制和下载步骤放在前面,把经常变化的源码复制放在后面。如果源阶段变化频繁,还可以考虑使用 BuildKit 的缓存挂载,把包管理器的缓存目录挂载到构建过程中,进一步缩短构建时间。
最后总结一下,COPY --from 是多阶段构建里连接不同阶段的桥梁。理解它的阶段引用、路径规则、权限处理和缓存行为,可以帮你写出更简洁、安全、高效的 Dockerfile,也能避免很多镜像构建时看似奇怪的问题。
Docker多阶段构建COPY --from镜像瘦身修改时间:2026-09-22 00:12:20