Docker多阶段构建及适用镜像推荐
之前笔者遇到多阶段构建无法COPY文件的问题,特此写此博客重新学习多阶段构建并尝试复现问题。
目录
- 摘要
- 多阶段构建
- 实现原理
- 一、核心机制:**多个独立的构建阶段(Build Stage)**
- 二、阶段之间的通信:`COPY --from=...` 的实现机制
- 三、镜像层构建原理
- 四、缓存与重用
- 五、最终镜像输出机制
- 快速开始
- 🧱 使用多阶段构建
- 🏷️ 给构建阶段命名
- 🧪 停在指定构建阶段(用于调试)
- 📦 使用外部镜像作为阶段
- ♻️ 重用前一阶段作为新的阶段
- 🆚 传统构建器 与 BuildKit 的区别
- BuildKit是什么?
- 实际案例
- golang静态构建
- python和golang多阶段构建推荐镜像
- glibc 和 musl 是什么
- **glibc(GNU C Library)**
- **musl(musl libc)**
- **镜像体积 vs 安全性 二维对照图**
- 常见镜像标签解释
摘要
Docker 多阶段构建是一种优化镜像体积、提高构建安全性的方法,它允许在一个 Dockerfile 中定义多个 FROM
阶段。每个阶段可以使用不同的基础镜像,并且只将最终阶段需要的文件复制过来。
✅ 优势:
- 减小最终镜像体积:只保留运行时所需内容,去掉编译器、构建依赖等。
- 提高构建安全性:不会将源码、密钥等敏感内容带入最终镜像。
- 构建逻辑更清晰:分阶段构建更易维护、复用和调试。
🔧 基本语法:
# 构建阶段
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp# 运行阶段
FROM debian:bookworm-slim
COPY --from=builder /app/myapp /usr/local/bin/myapp
CMD ["myapp"]
🚀 Go 和 Python 项目的多阶段构建镜像建议
🟦 Go 项目:
Go 原生支持静态编译,非常适合多阶段构建。
-
构建阶段:
golang:1.21-alpine
或golang:1.21
-
运行阶段:
scratch
(完全空白,适用于纯静态编译)alpine
(更小巧,但要注意兼容性)debian:bookworm-slim
(更通用)
示例:
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN go build -o myappFROM scratch
COPY --from=builder /app/myapp /
ENTRYPOINT ["/myapp"]
🟨 Python 项目:
Python 通常需要解释器和依赖包,不像 Go 那么轻量。但也可以通过多阶段减少构建依赖带来的膨胀。
-
构建阶段:
python:3.12-slim
或python:3.12
-
运行阶段:
python:3.12-alpine
(注意有些依赖可能不兼容 alpine)python:3.12-slim
- 或自定义最小化环境
示例:
FROM python:3.12 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --upgrade pip && pip install -r requirements.txt --target /depsFROM python:3.12-slim
WORKDIR /app
COPY --from=builder /deps /usr/local/lib/python3.12/site-packages
COPY . .
CMD ["python", "main.py"]
多阶段构建
参考文档:
- dockerfile:https://docs.docker.com/reference/dockerfile/
- Multi-stage builds:https://docs.docker.com/build/building/multi-stage/
可以看到dockerfile现在功能更加完善了:
指令 | 描述 |
---|---|
ADD | 添加本地或远程的文件和目录。 |
ARG | 使用构建时变量。 |
CMD | 指定默认的执行命令。 |
COPY | 复制文件和目录。 |
ENTRYPOINT | 指定默认的可执行程序。 |
ENV | 设置环境变量。 |
EXPOSE | 描述应用程序监听的端口。 |
FROM | 从基础镜像创建一个新的构建阶段。 |
HEALTHCHECK | 在容器启动后检查其健康状态。 |
LABEL | 向镜像添加元数据。 |
MAINTAINER | 指定镜像的作者。(已弃用,推荐使用 LABEL) |
ONBUILD | 指定在该镜像被用作构建基础时自动触发的指令。 |
RUN | 执行构建时命令。 |
SHELL | 设置镜像的默认 shell。 |
STOPSIGNAL | 指定容器退出时使用的系统调用信号。 |
USER | 设置用户和用户组 ID。 |
VOLUME | 创建数据卷挂载点。 |
WORKDIR | 更改工作目录。 |
多阶段构建依赖COPY命令,废话不多说,对多阶段构建做个简单的介绍后迅速进入主题!
多阶段构建是 Dockerfile 提供的一种机制,允许你在一个构建过程中使用多个 FROM
语句定义多个阶段(stage),并通过 COPY --from=某阶段
将前一阶段构建好的文件复制到最终镜像中。
这个机制的核心思想是:
在不同的构建阶段中使用不同的镜像和依赖,仅将最终运行需要的部分提取到最后的镜像中。
⚙️ 原理详解
-
定义多个构建阶段
- 每个
FROM
都可以理解为一个“构建环境”。 - 可以为阶段命名(如:
FROM golang:1.21 AS builder
)。
- 每个
-
在某个阶段中编译或构建项目
- 安装构建依赖、编译源代码、打包资源等。
-
从之前的阶段中复制构建产物
- 使用
COPY --from=builder
将可执行文件、配置等复制到最终镜像中。
- 使用
-
最终镜像精简、安全
- 只包含运行时所需内容,体积更小,暴露面更少。
✅ 多阶段构建的好处
优点 | 说明 |
---|---|
🚀 镜像更小 | 构建依赖、源码不会进入最终镜像,减小体积(比如 Go 项目可降到 <20MB) |
🔒 更安全 | 避免将敏感信息(如源码、token、构建工具)暴露到最终镜像中 |
🧩 结构清晰 | 构建流程分阶段管理,逻辑更清晰、易读 |
🔁 复用阶段 | 多个阶段可以共用构建产物,提高效率 |
🔄 支持缓存 | 各阶段可利用 Docker 的缓存机制加速构建过程 |
🛠️ 构建环境与运行环境分离 | 避免在运行镜像中安装构建工具,提高性能和可靠性 |
实现原理
一、核心机制:多个独立的构建阶段(Build Stage)
Docker 在执行多阶段构建时,其本质是顺序执行多个完全独立的 Dockerfile 构建流程,每个阶段就像执行了一个完整的 Dockerfile(带自己的 FROM
基础镜像、上下文、文件系统等)。
- 每个
FROM
会创建一个新的构建根文件系统(rootfs)。 - 每个阶段之间的环境、依赖、文件系统完全隔离。
- 阶段之间无法直接“访问”,只能通过
COPY --from=
来显式地提取内容。
二、阶段之间的通信:COPY --from=...
的实现机制
COPY --from=stage
并不是把某阶段打包后再解压,而是通过 Docker 引擎的构建图(Build Graph)进行优化提取:
-
每个阶段构建的中间产物会被保存为一个临时镜像层(intermediate image layer)。
-
COPY --from=builder /app/bin /bin
实际上就是:- 将名为
builder
的阶段构建出的临时镜像的文件系统挂载到构建上下文中; - 从该挂载点中提取
/app/bin
并复制到当前阶段的/bin
;
- 将名为
-
不涉及实际的中间镜像保存或上传,只是层级文件系统(UnionFS)之间的底层拷贝。
💡 可以理解为 COPY --from
是一种跨阶段、受控的文件系统快照复制。
三、镜像层构建原理
Dockerfile 中的每条指令(如 RUN
, COPY
, ADD
)都会生成一个新的镜像层(layer):
- 多阶段构建中,每个阶段依然会生成自己的多层镜像。
- 最终产出的镜像只保留最后一个阶段的镜像层,前面的都不会出现在最终镜像中。
- Docker 引擎使用 内容可寻址存储(Content-addressable storage) 来优化这些层,避免重复构建。
四、缓存与重用
每个构建阶段都参与 Docker 的构建缓存机制:
- 如果某阶段的所有上层指令都没变,则该阶段可以命中缓存、跳过重新构建;
- 即使某阶段未命中缓存,其后继阶段只要对应
COPY --from
内容不变,也能复用旧产物。
五、最终镜像输出机制
- Docker 只会将最后一个阶段的文件系统及其层打包成最终镜像;
- 其他阶段是中间态,不会保留在最终镜像中,除非你显式使用
--target
生成中间镜像。
可以通过 docker build --target builder -t intermediate .
来查看中间阶段的镜像内容。
🧪 举个底层逻辑示例:
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN go build -o mainFROM scratch
COPY --from=builder /app/main /main
ENTRYPOINT ["/main"]
构建时发生了什么:
- Docker 创建构建阶段 1:基于
golang:1.21
,执行COPY
和RUN
,生成中间镜像builder
。 - Docker 创建阶段 2:基于
scratch
(空白镜像)。 COPY --from=builder
触发文件系统层挂载,从builder
的/app/main
拷贝到/main
。- 最终镜像只包含
scratch
+/main
,约几 MB 大小,构建层也独立存在缓存中。
快速开始
🧱 使用多阶段构建
使用多阶段构建时,可以在一个 Dockerfile 中使用多个 FROM
指令。每个 FROM
都可以使用不同的基础镜像,并开始一个新的构建阶段。你可以从某个阶段选择性地复制构建产物到另一个阶段,从而避免把你不希望包含在最终镜像中的内容带进去。
下面这个 Dockerfile 展示了两个独立的构建阶段:
一个用于构建 Go 二进制,另一个用于构建最终镜像,仅复制第一阶段的二进制文件。
# syntax=docker/dockerfile:1
FROM golang:1.24
WORKDIR /src
COPY <<EOF ./main.go
package mainimport "fmt"func main() {fmt.Println("hello, world")
}
EOF
RUN go build -o /bin/hello ./main.goFROM scratch
COPY --from=0 /bin/hello /bin/hello
CMD ["/bin/hello"]
你只需要这个单一的 Dockerfile,不需要额外的构建脚本,只需运行:
docker build -t hello .
最终构建出的镜像非常小,只包含一个 Go 编译出的二进制文件,不会包含任何构建工具或中间产物。
🛠 它是如何工作的?
第二个 FROM
指令使用了 scratch
作为基础镜像,开启了新的构建阶段。
COPY --from=0
会从第一个阶段中提取已经构建好的二进制文件。Go SDK 和所有中间产物都不会被包含在最终镜像中。
🏷️ 给构建阶段命名
默认情况下,各阶段没有名称,你需要通过阶段编号(第一个 FROM
是 0
)引用它们。但你也可以通过在 FROM
中添加 AS <名称>
的方式给阶段命名。这有助于在将来重排指令时不会影响 COPY
行为。
改进后的示例如下:
# syntax=docker/dockerfile:1
FROM golang:1.24 AS build
WORKDIR /src
COPY <<EOF /src/main.go
package mainimport "fmt"func main() {fmt.Println("hello, world")
}
EOF
RUN go build -o /bin/hello ./main.goFROM scratch
COPY --from=build /bin/hello /bin/hello
CMD ["/bin/hello"]
🧪 停在指定构建阶段(用于调试)
你不一定要构建完整的 Dockerfile。你可以通过 --target
参数指定要停止在哪个阶段:
docker build --target build -t hello .
这在以下场景中非常有用:
- 调试某个具体的构建阶段;
- 添加一个包含调试工具的调试阶段,但最终只构建精简的生产镜像;
- 使用一个测试阶段注入测试数据,生产阶段则使用真实数据。
📦 使用外部镜像作为阶段
你也可以用 COPY --from=镜像名
的方式,从另一个镜像中复制文件,而不一定是当前 Dockerfile 中定义的阶段。Docker 会自动拉取该镜像(如果本地没有)。
例如:
COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf
♻️ 重用前一阶段作为新的阶段
你可以通过再次使用 FROM
并引用前一个命名阶段,来接着使用已有的环境。例如:
# syntax=docker/dockerfile:1FROM alpine:latest AS builder
RUN apk --no-cache add build-baseFROM builder AS build1
COPY source1.cpp source.cpp
RUN g++ -o /binary source.cppFROM builder AS build2
COPY source2.cpp source.cpp
RUN g++ -o /binary source.cpp
🆚 传统构建器 与 BuildKit 的区别
传统构建器(Legacy Builder)会处理 Dockerfile 中 --target
之前的所有构建阶段,即使目标阶段不依赖于它们。
而 BuildKit 只会构建目标阶段及其依赖阶段。
示例 Dockerfile:
# syntax=docker/dockerfile:1
FROM ubuntu AS base
RUN echo "base"FROM base AS stage1
RUN echo "stage1"FROM base AS stage2
RUN echo "stage2"
使用 BuildKit 构建:
DOCKER_BUILDKIT=1 docker build --no-cache -f Dockerfile --target stage2 .
🔍 结果:只构建 base
和 stage2
,跳过了 stage1
(因为无依赖关系)。
使用传统构建器构建:
DOCKER_BUILDKIT=0 docker build --no-cache -f Dockerfile --target stage2 .
🔍 结果:会顺序执行 base
、stage1
和 stage2
,即使 stage2
不依赖 stage1
。
BuildKit是什么?
BuildKit 是 Docker 背后的 新一代构建引擎,由 Docker 官方开发,用于替代传统的构建器(Legacy Builder)。它最早在 Docker 18.09 中引入,并在后续版本中逐渐成为默认构建方式。
BuildKit 是一个模块化、并行化、高性能的容器镜像构建引擎,它改进了传统 Docker 构建过程中的效率低、缓存粒度粗、功能单一等问题。
可以把 BuildKit 看成是:
Dockerfile 的“编译器升级版”,让镜像构建更快、更智能、更可控。
🧩 BuildKit 的核心特性
特性 | 说明 |
---|---|
🧠 依赖分析与按需构建 | 只构建目标阶段及其依赖,跳过无关阶段(支持多阶段构建优化) |
🧱 并行构建步骤 | 多个 RUN 或 COPY 可以并发执行,显著提升构建速度 |
💾 更细粒度的缓存 | 比传统构建器支持更智能的缓存策略,跨项目缓存也更容易 |
📦 缓存导入/导出 | 支持将构建缓存导入/导出(如推送到 CI/CD 缓存服务器) |
🧰 支持前端构建器插件 | 支持多个 Dockerfile 前端解析器,如 Dockerfile V1、V2 |
🔐 更好的安全性 | 支持 rootless 模式、构建过程隔离更彻底 |
🧹 自动清理中间层 | 避免磁盘堆积,镜像空间更干净 |
🛠 如何启用 BuildKit?
-
临时启用:
DOCKER_BUILDKIT=1 docker build -t myapp .
-
永久启用:在 Docker 配置文件中设置(Linux/Mac):
# /etc/docker/daemon.json {"features": {"buildkit": true} }
然后重启 Docker:
```bash
systemctl restart docker
```
🧪 BuildKit 的 CLI 工具:buildx
Docker 官方提供了 docker buildx
子命令,作为 BuildKit 的前端工具,支持:
- 构建多平台镜像(如同时构建 amd64 和 arm64)
- 使用本地或远程缓存
- 控制构建上下文(甚至跨主机构建)
- 自定义构建器实例
示例:
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:multiarch --push .
📌 总结:BuildKit 与传统构建器对比
对比项 | Legacy Builder | BuildKit |
---|---|---|
并发构建 | ❌ 不支持 | ✅ 支持 |
缓存粒度 | 粗 | 细 |
多阶段跳过无关阶段 | ❌ 不行 | ✅ 可以 |
多平台构建 | ❌ 复杂 | ✅ 内建支持 |
缓存导入导出 | ❌ 不支持 | ✅ 支持 |
默认状态 | 一般默认关闭 | Docker Desktop 默认启用 |
启用方式 | 自动使用 | DOCKER_BUILDKIT=1 或配置文件启用 |
实际案例
给出一个golang http开启https的简单代码,这里证书我们用openssl签发:
# 1. 生成私钥
openssl genrsa -out server.key 2048# 2. 生成证书签署请求(CSR)
openssl req -new -key server.key -out server.csr -subj "/C=CN/ST=Beijing/L=Beijing/O=Example/OU=IT/CN=localhost"# 3. 使用自签名方式生成 X.509 证书,有效期 365 天
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
生成文件:
- server.key: 私钥
- server.crt: 自签证书
你也可以用交互式生成第二步的证书:openssl req -new -key server.key -out server.csr
Go 启动 HTTPS(支持 HTTP/2):
package mainimport ("fmt""log""net/http"
)func helloHandler(w http.ResponseWriter, r *http.Request) {fmt.Fprintf(w, "✅ Hello, HTTPS with HTTP/2! 👋\n")log.Printf("📥 Received %s request from %s via %s", r.Method, r.RemoteAddr, r.Proto)
}func main() {mux := http.NewServeMux()mux.HandleFunc("/", helloHandler)server := &http.Server{Addr: ":8443",Handler: mux,}log.Println("🚀 Starting HTTPS server on https://localhost:8443 ...")err := server.ListenAndServeTLS("cert/server.crt", "cert/server.key")if err != nil {log.Fatalf("❌ Failed to start server: %v", err)}
}
我们建立一个cert文件夹把证书copy进去:
编译后访问:https://localhost:8443/
现在我们编写一个dockerfile:
# 第一阶段:构建阶段
FROM golang:1.24 AS builderWORKDIR /appCOPY go.mod go.sum ./
RUN go mod downloadCOPY . .# 静态构建
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o myapp# 第二阶段
FROM scratchWORKDIR /appCOPY --from=builder /app/myapp /app/myapp
COPY --from=builder /app/cert /app/certENTRYPOINT ["/app/myapp"]
然后我们构建并运行:
docker build -t gohttp:1.0.0 .
docker run --name gohttp -p 8443:8443 gohttp:1.0.0
然后检查一下docker启动的镜像,发现服务正常启动:
看样子笔者之前遇到的无法copy目录的BUG是某个特定版本的docker导致的,最新版docker目前没有复现。
当我们尝试进入容器时:
docker exec -it gohttp bash
OCI runtime exec failed: exec failed: unable to start container process: exec: "bash": executable file not found in $PATH: unknown
这个错误跟 Docker 版本 没关系,主要是因为最终的镜像(scratch
或 distroless/static
)里根本没有 bash
这个命令,所以执行:
docker exec -it gohttp bash
就会报:
exec: "bash": executable file not found in $PATH
FROM scratch
→ 完全空,连/bin/sh
都没有,更别说bash
。FROM gcr.io/distroless/static
→ 也是极简,只包含运行你的应用需要的动态/静态库,不包含 shell。- 所以
exec
进去的时候,找不到你指定的bash
可执行文件。
解决办法
-
开发调试阶段(建议用大一点的基础镜像)
比如:
FROM debian:bookworm-slim
或者:
FROM golang:1.22-bullseye
这样镜像里会有
/bin/bash
或/bin/sh
,方便你docker exec
进去调试。 -
生产环境(scratch/distroless)
如果你为了减小体积用 scratch / distroless,那么就不能
exec
进去跑 bash 了,可以直接运行你的程序或用sh
(如果基础镜像有)。
例如:docker exec -it gohttp /app/myapp
或者用 busybox 作为调试工具镜像进入:
docker run -it --rm --network container:gohttp busybox sh
这样是用另一个带 shell 的容器共享目标容器的网络/文件系统环境来调试。
-
特殊情况
如果你就是想在 distroless 镜像中能
exec
进去调试,可以临时在构建时加工具:FROM gcr.io/distroless/base-debian12 # 生产环境请删掉 RUN apt-get update && apt-get install -y bash
但这会让 distroless 变“大”,失去它的轻量优势。
golang静态构建
当你用一些非常小的基础镜像(例如 scratch、distroless/static、busybox:glibc
这种),它们几乎没有运行时依赖库,所以 Go 程序必须静态构建,否则运行时会找不到需要的动态库。
什么是 Go 的静态构建?
-
静态构建:把程序依赖的所有运行时库(比如 libc、加密库等)都打包到一个单独的可执行文件里。
-
构建出来的二进制文件可以 直接在任何兼容架构的 Linux 上运行,不依赖系统预装的动态库。
-
在 Go 里,静态构建常见的配置就是:
CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o myapp
为什么小基础镜像需要静态构建?
-
像
scratch
、distroless/static
这类镜像,本质是空的:- 没有
glibc
/musl
- 没有任何动态链接库(
.so
文件)
- 没有
-
如果你的 Go 程序编译出来是 动态链接的,在这些镜像里就会运行失败:
./myapp: No such file or directory
其实它找不到的是动态库,而不是程序本身。
-
静态构建的程序则不依赖外部库,所以能在空镜像里正常运行。
如何开启 Go 静态构建?
在大多数场景下,只要:
CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o myapp
CGO_ENABLED=0
禁用 CGO,Go 就会用纯 Go 实现的运行时替代调用 C 代码,这样能彻底去掉对 libc 的依赖。GOOS=linux
指定目标系统为 Linux(即使你在 Mac/Windows 上交叉编译)。-ldflags="-s -w"
-s
去掉符号表,-w
去掉调试信息,减小二进制体积。
什么时候不能完全静态构建
-
如果你依赖某些 C 扩展库(比如使用 SQLite C 版本、调用系统 C 库函数等),禁用 CGO 会导致功能缺失或构建失败。
-
这时要么:
- 换成纯 Go 的库实现
- 选一个带动态库的基础镜像(如
distroless/base
、alpine
)
python和golang多阶段构建推荐镜像
这里给出推荐的 Python / Go 多阶段构建推荐镜像清单:
Go 多阶段构建推荐镜像
镜像名称 | 解释 | 适用场景 | 使用限制 |
---|---|---|---|
golang:<version> | 官方完整 Go 构建镜像,基于 Debian,包含编译工具链和常见库 | 开发调试、多阶段构建的第一阶段 | 镜像较大(>1GB),不适合直接用作生产镜像 |
golang:<version>-alpine | 基于 Alpine 的轻量版 Go 构建镜像(musl libc) | 需要更小的构建阶段镜像,且无 glibc 依赖 | musl 与 glibc 存在兼容性差异,某些依赖可能编译失败 |
gcr.io/distroless/static-debian12 | Google Distroless 静态运行时镜像,无动态库 | 纯静态构建(CGO_ENABLED=0 ),生产极简镜像 | 无 shell、无调试工具、无动态库,调试需额外方法 |
gcr.io/distroless/base-debian12 | Distroless 带最小运行时(glibc、证书等) | 需要 glibc 支持的 Go 程序(CGO 开启) | 无包管理器,仍不可直接 apt 安装 |
scratch | 空镜像,什么都没有 | 纯静态 Go 二进制(极限最小体积) | 无证书、无时区、无动态库,HTTPS 可能需手动内嵌 CA |
busybox:glibc | 含基础命令和 glibc 的极小镜像 | 需要极小镜像 + 基本调试命令 | 功能有限,不适合复杂依赖 |
debian:bookworm-slim | 官方 Debian 精简版 | 需要 apt 安装额外依赖、体积适中 | 不是最小,但兼容性最好 |
ubuntu:22.04 | 官方 Ubuntu 基础镜像 | 需要 Ubuntu 生态支持的依赖 | 体积较大,启动速度慢 |
Python 多阶段构建推荐镜像
镜像名称 | 解释 | 适用场景 | 使用限制 |
---|---|---|---|
python:<version> | 官方完整 Python 镜像(基于 Debian) | 开发调试、多阶段构建的第一阶段 | 镜像大(>900MB),不适合直接生产 |
python:<version>-slim | 基于 Debian 的精简版 Python | 生产环境常用,依赖安装速度快 | 缺少编译工具,安装需要编译的包可能失败 |
python:<version>-alpine | 基于 Alpine 的轻量 Python | 对镜像体积要求极高的场景 | Alpine + Python 有兼容性坑(musl 与某些 PyPI 包不兼容) |
gcr.io/distroless/python3 | Distroless Python 运行时 | 纯生产环境,安全加固(无 shell) | 不能直接安装 pip 包(需提前打包) |
debian:bookworm-slim + pyenv | 自定义 Python 安装 | 需要完全控制 Python 版本和构建方式 | 构建复杂、体积稍大 |
ubuntu:22.04 + pyenv | 基于 Ubuntu 的自定义环境 | 企业内部有 Ubuntu 标准的场景 | 镜像大、构建时间长 |
conda/miniconda3 | 最小化 Conda 环境 | 科学计算、数据分析依赖多的场景 | 镜像大(>400MB),启动稍慢 |
额外建议(Go / Python 通用)
-
多阶段构建思路
- 第一阶段(builder)用完整镜像(含编译工具链)
- 第二阶段(runtime)用极简镜像(scratch / distroless / slim)
-
调试与生产分离
- 调试用带 shell 的镜像(Debian/Ubuntu/Alpine)
- 生产用无 shell 的镜像(scratch / distroless)
-
安全性
- distroless 无包管理器,不容易被攻击扩展恶意程序
- scratch 更极端,但失去灵活性
-
证书问题(Go HTTPS / Python requests)
- scratch 和 distroless/static 没有 CA,需要提前内置
- distroless/base 和 slim 镜像自带 CA
glibc 和 musl 是什么
上文提到了glibc 和 musl 那么它们究竟是什么呢?
它们都是 C 标准库(libc) 的实现,几乎所有 Linux 应用程序在运行时都会依赖它。
即使你的程序是用 Go、Python、Java 写的,只要涉及到底层系统调用(比如网络、文件操作),最终都可能通过 libc 完成。
glibc(GNU C Library)
-
地位:Linux 世界里最广泛使用的 libc 实现,GNU/Linux 系统的默认选择。
-
特点:
- 功能齐全,兼容性强。
- 拥有大量扩展功能(不只是标准 C)。
- 性能不错,但代码基复杂、体积较大。
-
代表系统:
- Debian / Ubuntu / CentOS / Fedora / RedHat 都用 glibc。
-
优缺点:
- ✅ 稳定、兼容性最好,企业生产首选。
- ❌ 占用空间大(>2MB),启动速度略慢,构建出来的镜像更大。
musl(musl libc)
-
地位:轻量级 libc 实现,Alpine Linux 默认使用。
-
特点:
- 代码精简,专注小体积和简单实现。
- 静态链接更容易,适合容器场景。
- 体积小(<1MB)。
-
代表系统:
- Alpine Linux。
-
优缺点:
- ✅ 镜像体积小、加载快、静态构建简单。
- ❌ 某些 glibc 特有的功能不兼容(尤其是老旧闭源程序)。
- ❌ 某些 Python/Rust/Java 库在 musl 下可能编译或运行出错(比如复杂的 C 扩展)。
快速对比表
对比项 | glibc | musl |
---|---|---|
体积 | 较大(~2MB) | 较小(<1MB) |
兼容性 | 最好(闭源/老程序可直接跑) | 有些库不兼容 |
运行性能 | 高 | 中(部分场景更快) |
常用场景 | 传统 Linux、企业生产 | 容器、小镜像、静态构建 |
代表镜像 | debian , ubuntu , distroless/base | alpine , distroless/static |
镜像体积 vs 安全性 二维对照图
- X 轴:镜像大小(越右越大)
- Y 轴:安全性(越高越安全)
- scratch:极小、极安全(但功能极少)
- distroless/static:接近 scratch 的体积,安全性很高
- distroless/base:功能更多(带 glibc),体积略大,安全性仍高
- alpine:体积小但 musl 兼容性有坑,安全性中等
- slim 系列:体积适中,安全性中等
- debian / ubuntu:功能全,兼容性最好,但体积大、安全面大
安全性 ↑|| scratch| distroless/static| distroless/base|| alpine|| debian-slim / python-slim|| debian / ubuntu+----------------------------------------------→ 镜像体积
- 左上角(小体积,高安全):scratch, distroless/static
- 中上(中体积,高安全):distroless/base
- 中间(中体积,中安全):alpine
- 右下角(大体积,低安全):ubuntu / debian full
常见镜像标签解释
基本概念:Docker 镜像标签(TAG)是什么?
- 标签(Tag)就是镜像的名字后面跟的版本号或描述,比如
python:3.13-alpine3.21
,代表Python 3.13版本,基于 Alpine Linux 3.21 的镜像。 - 一个镜像标签其实对应多架构镜像清单(manifest list),包含了针对不同CPU架构的不同镜像,比如
linux/amd64
、linux/arm/v6
、windows/amd64
等。 - 镜像体积大小和安全漏洞报告是针对具体架构的镜像层统计的。
Python 镜像标签解析
-
alpine3.21
、3.13.6-alpine3.21
、3.13-alpine3.21
等- 这是基于 Alpine Linux 3.21 版本的镜像,非常小巧(15-16MB),适合体积敏感和轻量级场景。
3.13.6
是 Python 版本,alpine3.21
是基础操作系统版本。- 这种镜像大小小,没发现漏洞(None found),很安全。
-
3-alpine3.22
、3-alpine3.21
、3-alpine
- 3 代表 Python 3 的最新次版本,
alpine3.22
是操作系统版本,3-alpine
则可能指最新 alpine 版本的 Python 3。
- 3 代表 Python 3 的最新次版本,
-
slim-bullseye
、slim-bookworm
、slim
bullseye
和bookworm
是 Debian Linux 的发行版代号,slim
代表“瘦身”版基础镜像,体积较小但比 Alpine 大(40MB左右),且基于 Debian 系统,兼容性更好。- 漏洞数量显示存在(小部分),比 Alpine 镜像多,因为 Debian 镜像组件多,攻击面更大。
-
latest
、bullseye
、bookworm
latest
是默认最新稳定版本,基于 Debian 的完整版本,体积较大(300MB 以上),漏洞数量较多。bullseye
、bookworm
直接指定 Debian 发行版,适合对兼容性要求高的项目。
-
不同架构:每个标签下会有多个架构的镜像,比如:
linux/amd64
:常见的 x86_64 架构,桌面和服务器主流。linux/arm/v6
、linux/arm/v7
:用于较低功耗设备和嵌入式。windows/amd64
:Windows 平台镜像。
Go 镜像标签解析
-
tip-alpine3.22
、tip-alpine3.21
、tip-alpine
tip
通常表示 Go 的“最新开发版本”或“最新源码版本”,适合追踪 Go 语言最新变化。- 基于 Alpine Linux 3.21/3.22,体积小(约80MB),无漏洞报告。
-
带日期的标签,如
tip-20250801-alpine3.22
- 这是按具体日期构建的快照版本,便于回滚或重现某一天的镜像环境。
-
windowsservercore-ltsc2025
、windowsservercore-ltsc2022
、nanoserver-ltsc2025
等- 这些是 Windows 平台的镜像,分别基于不同 Windows Server 版本的核心镜像。
- 体积非常大(几GB),因为 Windows 镜像本身就大。
nanoserver
是更小更精简的 Windows 容器基础镜像。
-
bullseye
、bookworm
- 这是基于 Debian Linux 的不同发行版本,体积大约 270-300MB,漏洞较多。
-
latest
- 最新稳定版本的镜像,基于 Debian 或 Alpine,体积和漏洞数量依据基础镜像不同。
为什么会有这么多标签?
-
兼容性和适用场景不同:
- Alpine 版适合极简场景和体积敏感环境。
- Debian bullseye/bookworm 适合稳定且依赖更多系统库的软件。
- Windows 镜像适合 Windows 容器环境。
-
版本控制:
- 通过精确版本号(如
3.13.6
)固定 Python/Go 版本,保证构建可复现。 - 通过
tip
跟踪最新开发版本。
- 通过精确版本号(如
-
多架构支持:
- 镜像自动支持多平台,方便在不同硬件上运行同样的镜像名。
-
安全性差异:
- Alpine 由于基础库少,漏洞少。
- Debian 基础镜像更大,组件多,漏洞相对更多。
镜像大小的差异
- Alpine 体积小,通常15-80MB。
- Debian slim 版中等,40-300MB不等。
- Debian完整版和 Windows 镜像体积很大,几百MB到几GB。
- 镜像中包含的系统工具、库越多,体积越大。
另外你看到的奇奇怪怪的镜像其实是因为:
Debian 代号是用《玩具总动员》(Toy Story)里角色的名字来给版本命名的,这也是 Debian 的一个经典传统。
- 所有 Debian 稳定版的代号都是《玩具总动员》动画片中的角色名字。
- 这个传统从 Debian 1.1 “buzz”开始,后面都是以玩具总动员里的人物名字作为发行代号。
比如:
- Debian 1.1: buzz(巴斯光年)
- Debian 1.2: rex(霸王龙)
- Debian 3.0: woody(胡迪)
- Debian 11: bullseye(牛眼,一个玩具士兵)
- Debian 12: bookworm(书虫,动画中一个角色)
为什么用《玩具总动员》?
- 这是 Debian 创始人和社区的一种幽默和特色,避免用普通的数字或复杂名字,让版本更有趣味。
- 也便于记忆和区分不同版本。