Docker 镜像瘦身进阶:用 distroless/scratch 把 Go 服务打到 10MB,以及没 shell 怎么调试
你用多阶段构建把 Go 服务的镜像从 800MB 砍到了 300MB,已经很得意了。但拉出来一看,基础镜像还是 golang:1.22 或者 ubuntu------里面塞着一整套编译工具链、apt、bash、几百个系统库,而你的服务其实只需要一个静态编译好的二进制。真正的极限是:一个几 MB 的二进制,配一个近乎为空的基础镜像,总大小 10MB 出头。
这篇讲怎么用 scratch 和 Google 的 distroless 把镜像压到极致,以及一个绕不开的现实问题------镜像里没有 shell,线上出问题怎么进去看。
先看一个「以为很小、其实还很大」的写法
很多人多阶段构建的第二阶段是这样:
dockerfile
# 构建阶段
FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN go build -o server .
# 运行阶段------问题就在这
FROM debian:bookworm-slim
COPY --from=builder /app/server /server
CMD ["/server"]
debian:bookworm-slim 已经算「精简版」了,但仍有 ~80MB,里面有完整的 glibc、apt、shell、一堆你根本用不到的东西。对一个静态二进制来说,这些全是负担,也全是攻击面------每个系统包都可能有 CVE,镜像扫描一片红。
第一档:distroless,只有运行时依赖,没有 shell 和包管理器
Google 的 distroless 镜像只包含「运行你的程序所必需的东西」:glibc、CA 证书、时区数据,没有 shell、没有 apt、没有 ls/cat。Go、Java、Python、Node 各有对应变体。
dockerfile
FROM golang:1.22 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# CGO_ENABLED=0 关掉 cgo,生成不依赖 glibc 的静态二进制
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o server .
# distroless 的 static 变体:连 glibc 都没有,配静态二进制正好
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /app/server /server
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/server"]
几个关键点:
CGO_ENABLED=0:关闭 cgo,让 Go 生成纯静态 二进制,不依赖任何动态库。这样才能用distroless/static(最小的变体)。如果你的程序用了 cgo(比如某些 sqlite 驱动),得用distroless/base(带 glibc)。-ldflags="-s -w":去掉调试符号表和 DWARF 信息,二进制能再小 20%~30%。:nonroottag +USER nonroot:distroless 提供了一个非 root 用户(uid 65532),直接用它跑,满足「容器不要用 root」的安全基线。- distroless 自带 CA 证书,所以你的服务发 HTTPS 请求不会报
x509: certificate signed by unknown authority------这是很多人从scratch迁过来才发现的坑。
这套打出来,Go 服务通常 15~25MB。
第二档:scratch,真正的「空」镜像,极限 10MB
scratch 是 Docker 的保留空镜像------真的什么都没有,连 /tmp、CA 证书、时区数据都没有。它是所有镜像的「祖宗」。用它能到极限,但要手动补上程序需要的东西:
dockerfile
FROM golang:1.22 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o server .
FROM scratch
# scratch 里没有 CA 证书,发 HTTPS 会失败,必须手动拷进来
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# 没有时区数据,time.LoadLocation("Asia/Shanghai") 会报错,需要的话也拷
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
scratch 的两个高频坑,基本每个人都踩过:
- HTTPS 请求报证书错误 :因为没有
ca-certificates.crt。解法就是上面那行COPY把证书从构建阶段拷进来。 - 时区永远是 UTC :
time.LoadLocation("Asia/Shanghai")直接返回 error,因为没有/usr/share/zoneinfo。要么拷时区数据,要么在 Go 里用import _ "time/tzdata"把时区数据编译进二进制(更干净,不用改 Dockerfile)。
用 import _ "time/tzdata" 的话,连时区那行 COPY 都能省掉,二进制大个 ~450KB,但镜像自包含性更好。
核心难题:没 shell,docker exec 进不去,线上怎么调试
distroless 和 scratch 最大的「代价」就是------没有 shell 。你习惯的 kubectl exec -it pod -- bash 会直接报 exec: "bash": executable file not found,连 sh、ls、cat 都没有。第一次线上排查时会很崩溃。有三个实战办法:
办法一(K8s 首选):kubectl debug 临时容器。 不改镜像、不重启 Pod,往目标 Pod 里塞一个带完整工具的临时容器,和目标容器共享进程命名空间:
bash
# 往运行中的 pod 塞一个 busybox 临时容器,共享目标容器的进程空间
kubectl debug -it mypod \
--image=busybox:1.36 \
--target=myapp \
--share-processes
# 进去后能看到目标容器的进程、能访问它的网络
ps aux # 看到目标容器的进程
wget -qO- localhost:8080/health # 请求目标服务
--target 让临时容器共享目标容器的进程命名空间,--share-processes 让你 ps 时能看到对方进程。这是生产环境排查 distroless Pod 的标准姿势,不用给业务镜像塞任何调试工具。
办法二(纯 Docker):从外面看,不进去。 很多信息不用进容器也能拿:
bash
docker logs mycontainer # 日志
docker inspect mycontainer # 配置、挂载、环境变量
docker top mycontainer # 进程列表
# 直接从宿主机把容器里的文件拷出来看
docker cp mycontainer:/app/config.yaml ./config.yaml
办法三:准备一个 debug tag 镜像。 平时线上用 distroless,另外用同一份代码基于 alpine 或 distroless:debug(带一个 busybox shell)打一个 myapp:debug tag。真要深入排查时临时换成 debug 镜像跑一份。distroless 官方就提供 :debug 变体:
dockerfile
# 出问题时临时用这个变体,它带一个 busybox shell
FROM gcr.io/distroless/static-debian12:debug
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]
换成 :debug 后,kubectl exec -it pod -- sh 就能进去了。日常仍用不带 :debug 的版本保持最小攻击面。
三种基础镜像怎么选
| 基础镜像 | 大小(空镜像) | 有 shell | CA 证书 | 适用 |
|---|---|---|---|---|
| debian-slim | ~80MB | 有 | 有 | 需要包管理、图方便 |
| distroless/static | ~2MB | 无 | 有 | 静态二进制,推荐默认 |
| distroless/base | ~20MB | 无 | 有 | 需要 glibc(用了 cgo) |
| scratch | 0 | 无 | 无 | 极限瘦身,愿意手动补依赖 |
一般建议:默认用 distroless/static:nonroot ------够小、自带证书和非 root 用户、坑最少;追求极致或做 sidecar/init 小工具时再上 scratch。别一上来就 scratch,证书和时区两个坑够你排半天。
小结
- 多阶段构建只是第一步,基础镜像换成
distroless或scratch才是把镜像打到 10MB 级的关键。 - Go 端配合
CGO_ENABLED=0(静态编译)+-ldflags="-s -w"(去符号表),二进制本身就能瘦一大圈。 scratch记得手动拷ca-certificates.crt(否则 HTTPS 报证书错误),时区问题用import _ "time/tzdata"内置最省心。- distroless/scratch 没有 shell ,线上排查靠
kubectl debug临时容器(--target+--share-processes)、docker logs/inspect/cp,或临时切到:debug变体镜像。 - 一句话记忆点 :默认选
distroless/static:nonroot,小、安全、坑少;scratch留给愿意手动补依赖的极限场景。