4. Docker基础与Go镜像构建
Go 服务容器化的第一步,是把编译产物和运行环境打包成一个可复现、可分发、体积可控的镜像。Dockerfile 是这一过程的唯一声明式入口,它决定了镜像里有什么、以什么用户运行、暴露什么端口、启动时执行什么命令。对 Go 项目而言,Dockerfile 的写法与 Java、Node 等语言有显著差异:Go 编译产物是静态二进制,不依赖运行时解释器,因此可以做到极小镜像;但若构建阶段与运行阶段混在一起,又会把整个 Go 工具链和源码带进最终镜像,造成数百 MB 的浪费。本节围绕 Dockerfile 指令、多阶段构建、基础镜像选择三条主线,完成第一个 Go 服务镜像的构建。
Dockerfile 指令:从基础镜像到启动命令
一个 Go 服务的 Dockerfile 通常包含以下指令,按执行顺序排列:
- FROM:指定基础镜像,是每条构建阶段的起点。多阶段构建中会出现多个 FROM,每个 FROM 开启一个新阶段。
- WORKDIR:设置后续指令的工作目录,相当于 cd,但会持久保留在镜像层中。
- COPY:把宿主机文件复制进镜像。Go 项目通常先复制 go.mod 和 go.sum,再复制源码,以利用层缓存。
- RUN:在构建阶段执行命令,如 go mod download、go build。每条 RUN 生成一个镜像层。
- ENV:设置环境变量,如 CGO_ENABLED=0、GOOS=linux。
- EXPOSE:声明容器监听端口,属于元数据,不实际开放端口。
- USER:指定运行用户,出于安全考虑不应使用 root。
- ENTRYPOINT / CMD:容器启动时执行的命令。ENTRYPOINT 定义可执行文件,CMD 提供默认参数,两者常配合使用。
需要特别注意的是层缓存机制。Docker 按顺序执行指令,一旦某层内容变化,其后所有层都会失效重建。因此把变化频率低的指令(如 go mod download)放在前面,把变化频率高的源码复制放在后面,可以显著加快重复构建速度。
多阶段构建:构建与运行分离
多阶段构建的核心思想是:用一个阶段完成编译,用另一个阶段只保留运行所需产物。典型写法如下:
# 阶段一:构建
FROM golang:1.22-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/server ./cmd/server
# 阶段二:运行
FROM alpine:3.19
RUN apk add --no-cache ca-certificates tzdata
WORKDIR /app
COPY --from=builder /out/server /app/server
EXPOSE 8080
USER 10001
ENTRYPOINT ["/app/server"]
这里 COPY --from=builder 只把编译好的二进制从构建阶段复制到运行阶段,Go 工具链、源码、模块缓存全部留在 builder 阶段,不会进入最终镜像。构建参数 -trimpath 去掉文件路径信息,-ldflags="-s -w" 去掉符号表和调试信息,通常可让二进制体积减少 20%~30%。CGO_ENABLED=0 确保生成纯静态二进制,这是后续选择 scratch 或 alpine 的前提。
多阶段构建带来的收益是直接的:单阶段 golang 镜像约 800MB 以上,多阶段 + alpine 通常可控制在 15~25MB,多阶段 + scratch 可进一步压到 8~15MB。镜像越小,拉取越快,攻击面越小,CI/CD 流水线中的传输和部署成本也越低。
scratch 与 alpine 基础镜像选择
运行阶段的基础镜像有三种常见选择,需要根据服务实际依赖判断:
| 基础镜像 | 典型体积 | 包含内容 | 适用场景 |
|---|---|---|---|
| scratch | 0MB | 完全空镜像,无 shell、无证书、无时区 | 纯静态二进制、不需要调试、不需要 TLS 外部调用 |
| alpine | 约 5~8MB | musl libc、busybox shell、apk 包管理 | 需要 ca-certificates、tzdata、shell 调试 |
| distroless | 约 2~20MB | 仅运行时库,无 shell、无包管理 | 需要 glibc 兼容但不要 shell 的场景 |
选择 scratch 的条件是:二进制完全静态(CGO_ENABLED=0)、不调用外部 HTTPS 接口(否则需要手动复制 ca-certificates.crt)、不需要时区文件、不需要进入容器排查。若服务需要访问 HTTPS 外部服务或需要设置 TZ 环境变量,alpine 更省事,只需 apk add --no-cache ca-certificates tzdata 即可。若团队希望兼顾 glibc 兼容性和最小攻击面,可考虑 distroless。实际工程中,alpine 是最常用的折中方案,scratch 适合对体积和安全性要求极高的边缘服务。
构建第一个 Go 服务镜像
假设项目结构为 cmd/server/main.go,监听 8080 端口,提供 /healthz 健康检查。完整操作流程如下:
# 1. 初始化模块(若尚未初始化)
go mod init example.com/gosvc
go mod tidy
# 2. 本地验证编译
CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /tmp/server ./cmd/server
# 3. 构建镜像
docker build -t gosvc:v1 .
# 4. 查看镜像体积
docker images gosvc:v1
# 5. 运行容器
docker run -d --name gosvc -p 8080:8080 gosvc:v1
# 6. 验证服务
curl -i http://localhost:8080/healthz
构建完成后,用 docker images 观察体积是否符合预期。若使用 alpine 基础镜像,正常应在 15~25MB 区间;若明显偏大,通常是 COPY 了多余文件或未使用多阶段构建。运行后用 curl 检查 /healthz 是否返回 200,再用 docker logs gosvc 确认启动日志无报错。若服务需要访问外部 HTTPS,而基础镜像为 scratch,常见现象是 TLS 握手失败并提示 x509: certificate signed by unknown authority,此时需在构建阶段复制 /etc/ssl/certs/ca-certificates.crt 到运行阶段,或改用 alpine。
golang:1.22.5-alpine3.19),避免 latest 导致构建结果不可复现。同时为镜像打上 Git commit 短哈希作为 tag,便于回滚和追溯。
USER 10001 使用非 root UID,若基础镜像为 scratch,需确保该 UID 在镜像中无需对应 /etc/passwd 条目即可运行(Linux 内核按数字 UID 判断权限)。若服务需要写入文件,应提前在构建阶段创建目录并 chown 给该 UID。