Docker 镜像瘦身进阶:用 distroless/scratch 把 Go 服务打到 10MB,以及没 shell 怎么调试

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%。
  • :nonroot tag + 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 的两个高频坑,基本每个人都踩过:

  1. HTTPS 请求报证书错误 :因为没有 ca-certificates.crt。解法就是上面那行 COPY 把证书从构建阶段拷进来。
  2. 时区永远是 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,连 shlscat 都没有。第一次线上排查时会很崩溃。有三个实战办法:

办法一(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,另外用同一份代码基于 alpinedistroless: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,证书和时区两个坑够你排半天。

小结

  • 多阶段构建只是第一步,基础镜像换成 distrolessscratch 才是把镜像打到 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 留给愿意手动补依赖的极限场景。
相关推荐
JavaPub-rodert1 小时前
Docker 深入理解:从容器原理到生产环境最佳实践
运维·docker·容器
lvshuocool1 小时前
golang 多版本管理工具 -- g
开发语言·后端·golang
aiot189189352182 小时前
核芯物联蓝牙AOA高精度定位生态合作案例分享金库定位踩坑实录
大数据·运维·网络·人工智能·蓝牙aoa
TomEval2 小时前
【接口自动化】13 - 接口数据驱动与报告
运维·windows·自动化
程序员无隅2 小时前
Codex 实战:用 AI 写运维脚本
运维·人工智能
陈聪.2 小时前
Kubernetes 集群网络插件迁移:从 Flannel 到 Calico 实战总结
云原生·kubernetes
Soari2 小时前
Linux 内核对比:PREEMPT_DYNAMIC vs PREEMPT_RT
linux·运维·ubuntu
商业看点解说3 小时前
数据中心运维管理软件平台推荐
运维
网硕互联的小客服3 小时前
服务器可以远程连接,但是为什么无法Ping通?如何解决处理?
运维·服务器