Featured image of post Go 自建最小镜像:scratch 5MB 级别,一条龙时区与 CA 证书

Go 自建最小镜像:scratch 5MB 级别,一条龙时区与 CA 证书

用 scratch + 多阶段构建把 Go 程序打成 5MB 级别 Docker 镜像,包含时区、CA 证书、scratch 引导、Windows 编译 Linux 二进制、docker-compose 部署

Go 程序最爽的一件事是能编译成单个静态二进制——配合 Docker 的 scratch 基础镜像(一个空镜像),最终产物能小到 5MB 级别。这篇把整个流程整理清楚:Windows 下编译 Linux 二进制、Dockerfile 多阶段构建、scratch 引导、时区与 CA 证书一条龙处理。

阅读对象:Go 后端工程师、SRE、追求极致镜像体积的容器化实践者 覆盖范围:scratch 基础镜像 + 多阶段构建 + 时区(Asia/Shanghai)+ CA 证书(x509: certificate signed by unknown authority 修复)+ Windows 交叉编译 + docker-compose 部署

一、为什么 Go 适合自建最小镜像

Go 程序的独特优势:

  • 静态链接CGO_ENABLED=0 后无任何动态库依赖
  • 无运行时:不依赖 JVM / Python 解释器
  • 编译产物单一:一个二进制文件搞定
  • Docker scratch 支持:scratch 是 Docker 保留的特殊"空"镜像,没有 /bin/sh、没有 libc、没有包管理器

结果:最终的镜像只有 5~20MB——比 alpine 还小一个数量级。

镜像大小
openjdk:8-jre-slim Java 应用~ 250 MB
node:18-alpine Node 应用~ 180 MB
python:3.11-slim Python 应用~ 150 MB
alpine Go 应用~ 50 MB
scratch Go 应用~ 5-20 MB

冷启动时间、镜像拉取速度、磁盘占用全面占优——Go 在云原生时代的统治力不是没道理的。

When to use

  • 内部工具 / Sidecar / Agent / 接入层网关
  • 对冷启动敏感(Serverless / K8s 弹性扩容)
  • 不适用:需要 bash / ps / curl 调试场景(scratch 里啥都没有)

二、scratch 镜像的 Dockerfile

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# ============ 阶段 1:在 golang:alpine 上编译 ============
FROM golang:alpine as build
RUN apk --no-cache add tzdata

# ============ 阶段 2:scratch 镜像拷贝二进制 + 时区 + CA ============
FROM scratch as final

# 1. 拷贝时区数据(解决 date 命令和 Go time 包时区问题)
COPY --from=build /usr/share/zoneinfo/Asia/Shanghai /usr/share/zoneinfo/Asia/Shanghai

# 2. 拷贝 CA 证书(解决 HTTPS 请求 x509 错误)
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt

# 3. 设置时区环境变量
ENV TZ=Asia/Shanghai

# 4. 工作目录
WORKDIR /app

# 5. 业务二进制(编译时 CGO_ENABLED=0)
COPY main /app/main

# 6. 启动
ENTRYPOINT ["/app/main"]

为什么多阶段构建?

  • golang:alpine 编译时需要工具链(~ 350MB),但运行时不需要
  • 编译完只把 main 二进制拷到 scratch避免编译工具链污染最终镜像

三、Windows 下编译 Linux 二进制

Go 跨平台编译是它最爽的特性之一。在 Windows CMD 下:

1
2
3
4
set CGO_ENABLED=0
set GOOS=linux
set GOARCH=amd64
go build -ldflags="-s -w" -installsuffix cgo -o main .

参数解释

  • CGO_ENABLED=0禁用 CGO,生成纯静态二进制
  • GOOS=linux目标操作系统 Linux
  • GOARCH=amd64目标 CPU 架构 x86_64
  • -ldflags="-s -w"去掉符号表和调试信息,体积更小(约 30%)

更激进的可选优化:

1
2
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
  go build -ldflags '-s -w --extldflags "-static -fpic"' -o main .

实测:一个 “hello 世界” 程序

  • 默认编译:~ 1.14 MB
  • -s -w 后:~ 850 KB
  • 加上 -trimpath(去掉构建路径):~ 800 KB

四、scratch 镜像调试技巧

scratch 没有 shell、没有 ps、没有 ls——容器内调试几乎不可能。常用技巧:

4.1 临时换成 alpine 调试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 改 Dockerfile 最后一行
FROM alpine:latest
RUN apk add --no-cache tzdata ca-certificates
COPY --from=build /usr/share/zoneinfo/Asia/Shanghai /usr/share/zoneinfo/Asia/Shanghai
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
ENV TZ=Asia/Shanghai
COPY main /app/main
CMD ["/app/main"]

# 构建
docker build -t myapp:debug .

# exec 进去调试
docker run -it --rm myapp:debug sh

4.2 制造一个 0 字节 scratch 镜像

有时候只想演示 FROM scratch 能跑(不是 pull 出来):

1
tar cv --files-from /dev/null | docker import - scratch

这样就能在 Dockerfile 里写 FROM scratch 而不报错了。

五、docker-compose 部署

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
version: "3"
services:
  greet:
    image: go-scratch:1.0.0
    container_name: greet
    restart: always
    ports:
      - 8888:8888
    volumes:
      - /home/project/greet/etc/greet-api.yaml:/app/etc/greet-api.yaml
      - /home/project/greet/logs:/app/logs
      - /home/project/greet/main:/app/main
      - /etc/timezone:/etc/timezone:ro       # 时区同步
      - /etc/localtime:/etc/localtime:ro
    command: ./main -f etc/greet-api.yaml

注意:scratch 镜像里没有 ./main 之外的可执行文件——chmod +x main 必须做。

服务器启动:

1
2
3
4
5
6
7
# 1. 上传 main 二进制
chmod +x main
scp main user@server:/home/project/greet/main/

# 2. 启动
cd /home/project/greet
docker-compose up -d

六、验证与冒烟测试

1
2
3
4
5
6
7
# 1. 看时区
docker run --rm go-scratch:1.0.0 sh -c 'date'   # scratch 没 sh,用下面方式
# 改用 alpine 包一层:
docker run --rm alpine:latest sh -c 'echo "now: $(date)"'

# 2. 业务接口
curl -i http://container-ip:8888/from/you

预期返回:

1
2
3
4
5
6
HTTP/1.1 200 OK
Content-Type: application/json
Date: Fri, 21 May 2021 05:02:01 GMT
Content-Length: 28

{"message":"hello,go-zero!"}

七、典型坑位

7.1 x509: certificate signed by unknown authority

症状:Go 程序发 HTTPS 请求失败。

原因:scratch 镜像没有 CA 证书——OpenSSL 默认信任的根证书列表缺失。

修复(上面 Dockerfile 已写):

1
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt

或在 Go 代码里显式指定:

1
2
3
4
5
6
7
8
9
// 方案 A:用 ca-certificates.crt
caCert, _ := ioutil.ReadFile("/etc/ssl/certs/ca-certificates.crt")
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)

// 方案 B:跳过证书校验(**绝不用于生产**)
tr := &http.Transport{
    TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}

7.2 时区错乱

症状:Go 日志时间戳和宿主机差 8 小时。

修复(上面 Dockerfile 已写):

1
2
COPY --from=build /usr/share/zoneinfo/Asia/Shanghai /usr/share/zoneinfo/Asia/Shanghai
ENV TZ=Asia/Shanghai

Go 代码

1
2
3
4
5
// 旧写法:time.Now() 永远返回 UTC
// 新写法:time.LoadLocation
loc, _ := time.LoadLocation("Asia/Shanghai")
now := time.Now().In(loc)
fmt.Println(now.Format("2006-01-02 15:04:05"))

7.3 静态文件找不到

scratch 镜像里 os.Stat 返回 no such file——可能是:

  • 编译时没把静态文件 embed
1
2
3
4
import "embed"

//go:embed static/*
var staticFiles embed.FS
  • 或者用 COPY 拷进镜像但路径写错

7.4 内存 / CPU 限制不生效

docker run -m 512m 对 scratch 镜像不生效——因为没有 cgroup 感知。

修复:在 Go 代码里显式读取:

1
2
3
4
import "github.com/docker/go-units"

// 或用 uber-go/automaxprocs
import _ "go.uber.org/automaxprocs"

八、多架构构建(ARM64 + AMD64)

M1 Mac / 国产 ARM 服务器越来越多,单架构镜像不够用:

1
2
3
4
5
6
7
8
# 1. 创建 buildx builder
docker buildx create --name multiarch --use

# 2. 多架构构建并推送
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t dockerhub.example.com/library/greet:1.0.0 \
  --push .

实测

  • 单 amd64 构建:~ 30 秒
  • amd64 + arm64 构建:~ 90 秒(首次拉基础镜像)
  • 后续增量构建:~ 20 秒

九、CI/CD 集成

GitHub Actions 示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
name: build-go-image
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: dockerhub.example.com
          username: ${{ secrets.REGISTRY_USER }}
          password: ${{ secrets.REGISTRY_PASS }}
      - uses: docker/build-push-action@v5
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: true
          tags: dockerhub.example.com/library/greet:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

gha cache 让第二次构建快 80%——只重编变化的层。

十、最佳实践清单

  • 永远用多阶段构建:编译环境和运行时彻底隔离
  • CGO_ENABLED=0 必加:避免任何动态库依赖
  • CA 证书必拷:HTTPS 请求的根证书不能少
  • 时区三选一:环境变量 TZ / 软链 /etc/localtime / embed 时区文件
  • -ldflags="-s -w" 必加:省 30% 体积
  • -trimpath 可选:去掉绝对路径,构建可重现
  • scratch 调试:用 docker debug(Docker Desktop 自带)或者临时换 alpine
  • 多架构:用 docker buildx 同时构建 linux/amd64linux/arm64
  • 静态资源 embed:不要依赖 bind mount,单一二进制就是单一二进制

2024+ 视角补充

本文写于 2024-03,2024-2026 期间 Go 容器化关键演进:

  • Go 1.22+ → 1.23 → 1.24(2024-2026):range over intiter 包(迭代器)、for-range loop variable 修复(for 循环变量每个 iteration 独立,修复经典 for 闭包 bug
  • Go 1.23 for-range 行为变更:每个 iteration 独立变量,老代码可能行为不一致——升级必测
  • Go 1.24(2025-02):泛型类型参数方法增强;map 并发安全改进
  • scratch 镜像仍是稳态选择——但 distrolessgcr.io/distroless/static)成为 2024+ 推荐:
    • distroless 只含运行时依赖(ca-certificates、tzdata、/etc/passwd)——比 scratch 多 5MB,但更安全(无 root 提权风险)
    • Google 内部推荐:所有 Go 服务 2024+ 默认 distroless
  • Chainguard Images 1.0+(2024):零 CVE 容器镜像,比 distroless 更安全——商业支持
  • Wolfi OS 1.0+(2024):Chainguard 出的"通用容器 OS"——基础镜像新选择
  • 多架构构建(ARM64 + AMD64) 是 2024+ 标准
  • WebAssembly(Wasm) 部署:Go 1.21+ 编译 Wasi Target,边缘计算 / FaaS 场景爆发
  • TinyGo 0.32+嵌入式 / IoT 场景——比标准 Go 编译产物更小(10-50KB 级别)

实战建议(2025-2026 视角)

  • 生产服务distroless/staticChainguard Images(vs scratch 更安全)
  • 极致体积scratch + 多阶段构建(本文方案仍 OK)
  • 多架构docker buildx linux/amd64 + linux/arm64 是 2024+ 默认
  • 边缘 / FaaSGo 1.24+ + Wasi + Spin / Fermyon Cloud
  • 嵌入式 / IoTTinyGo 0.32+

下一步

使用 Hugo 构建
主题 StackJimmy 设计