Skip to content

多阶段构建与镜像优化

本篇是 Docker + Go 系列教程的第二篇。在上一篇中,我们完成了 Go 应用的基础容器化,但当时使用的运行镜像是 golang:1.22,最终镜像体积高达 800MB 以上。本篇将带你通过多阶段构建把镜像压到 10MB 以内,并介绍镜像优化的各种策略、多平台构建、镜像安全扫描等内容。这是 Go 容器化最「爽」的部分——你会发现 Go 在镜像优化上的优势无与伦比。

一、为什么需要多阶段构建

1. 单阶段构建的问题

先回顾上一篇的 Dockerfile:

dockerfile
FROM golang:1.22
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o server .
CMD ["./server"]

构建后镜像大小:

bash
docker images
# REPOSITORY   TAG   SIZE
# gin-docker   1.0   856MB

856MB!这个镜像里装了什么?

  • Go 编译器(几百 MB)
  • Go 标准库源码
  • 所有下载的依赖模块($GOPATH/pkg/mod
  • 中间构建产物
  • Debian 操作系统(golang 镜像基于 Debian)
  • Git、gcc 等构建工具

而我们运行时真正需要的,只是一个编译好的二进制文件(十几 MB)。其余 99% 的内容都是「构建垃圾」,对运行毫无用处,却让镜像臃肿不堪。

2. 镜像过大的危害

镜像过大会带来一系列实际问题:

  • 拉取慢:CI/CD 流水线、生产环境部署每次都要拉几百 MB,浪费时间。
  • 存储贵:镜像仓库按容量收费,几百个镜像就是几十 GB。
  • 攻击面大:镜像里装了 gcc、git、shell 等工具,攻击者一旦入侵容器,就有大量工具可用。
  • 启动慢:容器调度时拉取镜像的时间与体积成正比。

3. 多阶段构建的核心思想

多阶段构建在 Docker 17.05+ 引入,允许在一个 Dockerfile 中定义多个 FROM 阶段,每个阶段可以有不同的基础镜像,最后只把需要的产物从一个阶段拷贝到另一个阶段。

核心思想:构建环境与运行环境分离。构建阶段用完整的环境编译出二进制,运行阶段用一个最小化的镜像只承载这个二进制。

text
[阶段1: 构建环境]         [阶段2: 运行环境]
golang:1.22 (800MB)  -->  alpine (5MB)
  - 源码                    - 二进制
  - 编译器                  - ca-certificates
  - 依赖模块                - tzdata
  - git/gcc
  产出: server 二进制       最终镜像: ~20MB

二、多阶段 Dockerfile 详解

1. 基本语法

多阶段构建用多个 FROM 指令分隔阶段,可以用 AS 给阶段命名:

dockerfile
# 阶段1:构建阶段,命名为 builder
FROM golang:1.22 AS builder
WORKDIR /build
COPY . .
RUN CGO_ENABLED=0 go build -o server .

# 阶段2:运行阶段
FROM alpine:3.19
WORKDIR /app
COPY --from=builder /build/server /app/server
CMD ["/app/server"]

COPY --from=builder 表示从名为 builder 的阶段拷贝文件。最终镜像只包含阶段2 的内容,阶段1 会被丢弃。

2. 阶段命名的意义

阶段命名让 COPY --from 更可读,也方便后续单独构建某个阶段:

bash
# 只构建到 builder 阶段
docker build --target builder -t myapp-builder .

这在调试构建问题时很有用。

3. 完整示例:优化版 Gin 应用

下面是一个完整的多阶段 Dockerfile,针对 Gin 应用做了层层优化:

dockerfile
# ============================================
# 阶段 1: 构建阶段
# ============================================
FROM golang:1.22-alpine AS builder

# 设置 Go 模块代理(国内用户加速)
ENV GOPROXY=https://goproxy.cn,direct \
    CGO_ENABLED=0 \
    GOOS=linux

WORKDIR /build

# 利用缓存:先拷依赖
COPY go.mod go.sum ./
RUN go mod download && go mod verify

# 拷源码
COPY . .

# 注入版本信息并静态编译
ARG VERSION=dev
ARG BUILD_TIME=unknown
RUN go build \
    -ldflags="-s -w -X main.version=${VERSION} -X main.buildTime=${BUILD_TIME}" \
    -o server \
    .

# ============================================
# 阶段 2: 运行阶段
# ============================================
FROM alpine:3.19

# 安装最小化运行依赖
RUN apk --no-cache add ca-certificates tzdata

# 创建非 root 用户
RUN addgroup -S app && adduser -S app -G app

WORKDIR /app

# 从构建阶段拷贝二进制
COPY --from=builder /build/server /app/server

# 切换用户
USER app

# 设置时区
ENV TZ=Asia/Shanghai

EXPOSE 8080

ENTRYPOINT ["/app/server"]

构建并查看大小:

bash
docker build -t gin-optimized:1.0 .
docker images gin-optimized
# REPOSITORY       TAG   SIZE
# gin-optimized    1.0   18MB

从 856MB 到 18MB,体积减小了 97.9%

三、镜像优化策略

下面详细讲解各种镜像优化手段。

1. 使用 scratch 基础镜像(零体积)

scratch 是一个特殊的基础镜像,它完全是空的,不包含任何东西。配合 Go 的静态编译,可以做出极致小的镜像。

dockerfile
FROM golang:1.22 AS builder
WORKDIR /build
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o server .

FROM scratch
COPY --from=builder /build/server /server
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
ENTRYPOINT ["/server"]

构建后大小:

bash
docker images
# REPOSITORY   TAG   SIZE
# scratch-app  1.0   7.2MB

7.2MB,几乎只是 Go 二进制本身的大小。

scratch 镜像的注意事项

  • 没有 shell:无法用 docker exec -it <容器> sh 进入调试。
  • 没有 ca-certificates:HTTPS 请求会失败,需要手动拷贝证书。
  • 没有 tzdata:时区相关功能不工作,需要注入时区数据或用 tzdata 包。
  • 没有 /tmp 目录:如果程序需要写临时文件,需要 COPY 或挂载。

适合对镜像体积有极致要求的场景(如边缘计算、嵌入式)。

2. 使用 alpine 基础镜像(5MB)

alpine 是一个基于 musl libc 和 BusyBox 的最小化 Linux 发行版,体积只有 5MB 左右,但提供了 apk 包管理器、shell、基础命令。

dockerfile
FROM alpine:3.19
RUN apk --no-cache add ca-certificates tzdata
COPY server /app/server
WORKDIR /app
ENTRYPOINT ["/app/server"]

alpine 是 Go 应用最常用的运行镜像,平衡了体积与可用性:

  • 可以用 docker exec sh 进入调试
  • 自带 ca-certificates 安装能力
  • 自带 tzdata 安装能力
  • /tmp 目录

alpine 的潜在坑:因为它用 musl libc 而不是 glibc,如果 Go 程序启用了 CGO(链接了 C 库),可能不兼容。解决方法是 CGO_ENABLED=0 静态编译。

3. 使用 distroless 镜像(更安全)

Google 维护的 distroless 镜像是一种「只有运行时、没有 shell 和包管理器」的镜像。基于 Debian,但去掉了所有不必要的工具,安全性更高。

dockerfile
FROM golang:1.22 AS builder
WORKDIR /build
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server .

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /build/server /server
USER nonroot
ENTRYPOINT ["/server"]

distroless 的优势:

  • 没有 shell:攻击者即使入侵也无法执行 sh,无法横向移动。
  • 没有包管理器:无法安装恶意工具。
  • 更小的攻击面:去掉了一切非必需内容。
  • 基于 glibc:与大多数 Linux 一致,兼容性好。

劣势:

  • 调试困难:不能 docker exec sh 进去(可以用 :debug 标签版本,包含 busybox shell)。
  • 体积比 alpine 稍大(约 2MB)。
基础镜像体积有 shell有包管理安全性适用场景
scratch0 MB最高极致优化、无调试需求
alpine5 MB是 (apk)通用,最常用
distroless~2 MB否*很高安全敏感的生产环境
debian80 MB+是 (apt)需要 glibc 兼容性
ubuntu30 MB+是 (apt)习惯 Ubuntu 的团队

distroless 的 :debug 标签包含 busybox shell,方便调试。

4. CGO_ENABLED=0 禁用 CGO

Go 默认在某些场景(如 net 包的域名解析)会使用 CGO 调用系统的 C 库。这会导致编译出的二进制依赖 glibc,无法在 alpine(musl libc)或 scratch 上运行。

解决方法是禁用 CGO,让 Go 完全用纯 Go 实现这些功能:

dockerfile
ENV CGO_ENABLED=0
RUN go build -o server .

或者在 go build 命令中指定:

bash
CGO_ENABLED=0 go build -o server .

禁用 CGO 后:

  • 二进制完全静态,不依赖任何 C 库
  • 可以在 scratch、alpine、distroless 上运行
  • 交叉编译更方便
  • 唯一影响:net 包默认用 Go 的纯实现做域名解析(Go 1.20+ 默认就是 Go resolver,影响很小)

5. -ldflags "-s -w" 去除调试信息

Go 编译器默认会在二进制中嵌入符号表和调试信息(DWARF),这些信息对运行无用,但占体积。通过 -ldflags 可以去除:

bash
go build -ldflags="-s -w" -o server .
  • -s:去除符号表(symbol table)
  • -w:去除 DWARF 调试信息

效果对比:

text
默认编译:        18.2 MB
-ldflags="-s -w": 11.8 MB

减小约 35%。注意:去除后无法用 gdbdlv 调试,panic 时的栈信息仍可用(Go 自己维护)。

6. -ldflags 注入版本信息

-ldflags -X 可以在编译时把字符串注入到变量的符号中,实现版本信息注入。这在 CI/CD 中非常常用。

定义变量:

go
package main

import "fmt"

var (
	version   = "dev"
	commit    = "none"
	buildTime = "unknown"
	goVersion = "unknown"
)

func main() {
	fmt.Printf("Version: %s\n", version)
	fmt.Printf("Commit: %s\n", commit)
	fmt.Printf("Build Time: %s\n", buildTime)
	fmt.Printf("Go Version: %s\n", goVersion)
}

编译时注入:

bash
go build \
  -ldflags="-s -w \
    -X 'main.version=v1.2.3' \
    -X 'main.commit=abc1234' \
    -X 'main.buildTime=2026-08-01T10:00:00Z' \
    -X 'main.goVersion=$(go version)'" \
  -o server .

运行:

bash
./server
# Version: v1.2.3
# Commit: abc1234
# Build Time: 2026-08-01T10:00:00Z
# Go Version: go version go1.22.0 linux/amd64

在 Dockerfile 中配合 ARG

dockerfile
ARG VERSION=v1.0.0
ARG COMMIT=unknown
ARG BUILD_TIME=unknown
RUN go build \
    -ldflags="-s -w -X main.version=${VERSION} -X main.commit=${COMMIT} -X main.buildTime=${BUILD_TIME}" \
    -o server .

构建:

bash
docker build \
  --build-arg VERSION=v1.2.3 \
  --build-arg COMMIT=$(git rev-parse --short HEAD) \
  --build-arg BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ) \
  -t myapp:v1.2.3 .

四、镜像瘦身前后对比

下面用同一个 Go 应用做对比,看不同优化策略的效果。

测试应用是一个 Gin 服务,编译后二进制 12MB。

方案运行镜像最终大小能否调试
单阶段 (golang:1.22)golang:1.22856 MB
多阶段 (alpine)alpine:3.1918 MB
多阶段 (alpine) + ldflagsalpine:3.1915 MB
多阶段 (scratch)scratch7.2 MB
多阶段 (distroless)distroless14 MB
多阶段 (distroless:nonroot)distroless14 MB

从 856MB 到 7.2MB,差距超过 100 倍。这就是 Go + 多阶段构建的威力。

实测对比命令:

bash
# 构建各种版本
docker build -f Dockerfile.single -t app:single .
docker build -f Dockerfile.alpine -t app:alpine .
docker build -f Dockerfile.scratch -t app:scratch .
docker build -f Dockerfile.distroless -t app:distroless .

# 查看大小
docker images | grep app

五、多平台构建:docker buildx

随着 ARM 服务器(如 AWS Graviton、阿里云倚天)和 Apple Silicon(M1/M2/M3)的普及,构建多架构镜像的需求越来越大。

1. 传统交叉编译 vs buildx

传统方式是用 Go 的交叉编译生成不同架构的二进制,然后分别构建镜像。但这只对 Go 程序有效,如果 Dockerfile 里用了 apk add 安装依赖,这些依赖不会自动跨架构。

docker buildx 利用 QEMU 模拟不同架构,可以让 Dockerfile 里的所有指令(包括 RUN apk add)都在目标架构下执行,实现真正的多平台构建。

2. 启用 buildx

Docker Desktop 默认启用 buildx。Linux 需要安装 docker-buildx-plugin(新版 Docker 已内置)。

创建一个支持多平台的 builder:

bash
# 创建 builder
docker buildx create --name multiarch --use

# 启动
docker buildx inspect --bootstrap

# 查看
docker buildx ls

3. 构建多平台镜像

bash
# 同时构建 amd64 和 arm64
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t myapp:multi \
  --push \
  .

注意:

  • --platform 指定多个平台,逗号分隔。
  • 多平台构建必须配合 --push 推送到仓库,不能 --load 到本地(本地 docker 不支持多平台镜像)。
  • 构建时间会比单平台慢(需要在 QEMU 中模拟)。

4. Go 应用的多平台 Dockerfile

Go 的交叉编译让多平台构建特别简单。可以在 Dockerfile 中根据目标架构设置 GOARCH

dockerfile
FROM --platform=$BUILDPLATFORM golang:1.22 AS builder

ARG TARGETOS
ARG TARGETARCH

ENV CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH}

WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -ldflags="-s -w" -o server .

FROM --platform=$TARGETPLATFORM alpine:3.19
RUN apk --no-cache add ca-certificates tzdata
WORKDIR /app
COPY --from=builder /build/server /app/server
ENTRYPOINT ["/app/server"]

变量说明:

  • $BUILDPLATFORM:执行构建的机器架构(如 linux/amd64)。
  • $TARGETPLATFORM:目标镜像架构(如 linux/arm64)。
  • $TARGETOS / $TARGETARCH:拆分后的 OS 和架构。

使用 --platform=$BUILDPLATFORM 让构建阶段在原生架构运行(更快),通过 GOARCH=$TARGETARCH 让 Go 交叉编译出目标架构的二进制。运行阶段用 --platform=$TARGETPLATFORM 拉取对应架构的 alpine。

构建:

bash
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t myregistry/myapp:v1.0 \
  --push \
  .

5. 验证多平台镜像

推送后可以查看 manifest:

bash
docker manifest inspect myregistry/myapp:v1.0
# {
#   "schemaVersion": 2,
#   "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
#   "manifests": [
#     {
#       "digest": "sha256:...",
#       "platform": { "architecture": "amd64", "os": "linux" }
#     },
#     {
#       "digest": "sha256:...",
#       "platform": { "architecture": "arm64", "os": "linux" }
#     }
#   ]
# }

在不同架构的机器上 docker pull myregistry/myapp:v1.0,会自动拉取对应架构的镜像。

六、镜像安全扫描:trivy

镜像构建出来后,应该扫描其中的已知漏洞(CVE)。trivy 是最常用的开源扫描工具。

1. 安装 trivy

macOS

bash
brew install trivy

Linux

bash
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh

Windows

powershell
choco install trivy

2. 扫描镜像

bash
trivy image myapp:1.0

输出示例:

text
myapp:1.0 (alpine 3.19.1)

Total: 2 (HIGH: 1, CRITICAL: 1)

┌─────────┬────────────────┬──────────┬───────────────────┬───────────────┬──────────────────────────────────┐
│ Library │ Vulnerability  │ Severity │ Installed Version │ Fixed Version │              Title               │
├─────────┼────────────────┼──────────┼───────────────────┼───────────────┼──────────────────────────────────┤
│ openssl │ CVE-2024-xxxx  │ CRITICAL │ 3.1.4-r0          │ 3.1.5-r0      │ openssl: ...                     │
├─────────┼────────────────┼──────────┼───────────────────┼───────────────┼──────────────────────────────────┤
│ libssl  │ CVE-2024-yyyy  │ HIGH     │ 3.1.4-r0          │ 3.1.5-r0      │ libssl: ...                      │
└─────────┴────────────────┴──────────┴───────────────────┴───────────────┴──────────────────────────────────┘

3. 扫描策略

  • 只扫 CRITICAL
bash
trivy image --severity CRITICAL myapp:1.0
  • 扫到漏洞则退出码非零(用于 CI)
bash
trivy image --exit-code 1 --severity CRITICAL,HIGH myapp:1.0
  • 忽略特定漏洞
bash
trivy image --ignore-unfixed myapp:1.0
# 只显示有修复版本的漏洞

4. 减少漏洞的策略

  • 使用 alpine 这类小镜像:组件少,漏洞少。
  • 使用 distroless:去掉包管理器和 shell,攻击面更小。
  • 定期重建镜像:拉取最新的基础镜像和依赖。
  • 在 CI 中加入 trivy 扫描门禁。
  • 升级 Go 版本:新版本通常修复了标准库的安全问题。

七、完整示例:生产级 Go 应用 Dockerfile

下面给出一个生产级 Go 应用的 Dockerfile,综合运用本篇讲过的所有优化技术。

1. 项目结构

text
prod-app/
├── main.go
├── go.mod
├── go.sum
├── Dockerfile
├── .dockerignore
└── Makefile

2. main.go

go
package main

import (
	"fmt"
	"net/http"
	"os"
	"runtime"
	"time"

	"github.com/gin-gonic/gin"
)

var (
	version   = "dev"
	commit    = "none"
	buildTime = "unknown"
)

type HealthResponse struct {
	Status    string    `json:"status"`
	Version   string    `json:"version"`
	Commit    string    `json:"commit"`
	BuildTime string    `json:"build_time"`
	GoVersion string    `json:"go_version"`
	Now       time.Time `json:"now"`
}

func main() {
	gin.SetMode(gin.ReleaseMode)
	r := gin.New()
	r.Use(gin.Logger(), gin.Recovery())

	r.GET("/", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"message": "Hello from production Go app",
			"version": version,
		})
	})

	r.GET("/health", func(c *gin.Context) {
		c.JSON(http.StatusOK, HealthResponse{
			Status:    "ok",
			Version:   version,
			Commit:    commit,
			BuildTime: buildTime,
			GoVersion: runtime.Version(),
			Now:       time.Now(),
		})
	})

	r.GET("/version", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"version":    version,
			"commit":     commit,
			"build_time": buildTime,
			"go_version": runtime.Version(),
		})
	})

	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}

	fmt.Printf("Starting app version=%s commit=%s on port %s\n", version, commit, port)
	if err := r.Run(":" + port); err != nil {
		fmt.Printf("Server failed: %v\n", err)
		os.Exit(1)
	}
}

3. Dockerfile

dockerfile
# syntax=docker/dockerfile:1

# ============================================
# 阶段 1: 构建阶段
# ============================================
FROM --platform=$BUILDPLATFORM golang:1.22-alpine AS builder

ARG TARGETOS=linux
ARG TARGETARCH
ARG VERSION=dev
ARG COMMIT=none
ARG BUILD_TIME=unknown

ENV CGO_ENABLED=0 \
    GOOS=${TARGETOS} \
    GOARCH=${TARGETARCH} \
    GOPROXY=https://goproxy.cn,direct

WORKDIR /build

# 利用缓存
COPY go.mod go.sum ./
RUN go mod download && go mod verify

COPY . .

# 静态编译并注入版本信息
RUN go build \
    -ldflags="-s -w \
      -X main.version=${VERSION} \
      -X main.commit=${COMMIT} \
      -X main.buildTime=${BUILD_TIME}" \
    -o server \
    .

# ============================================
# 阶段 2: 运行阶段
# ============================================
FROM alpine:3.19

# 安装最小化依赖
RUN apk --no-cache add ca-certificates tzdata

# 创建非 root 用户
RUN addgroup -S app && adduser -S app -G app

WORKDIR /app

# 拷贝二进制
COPY --from=builder /build/server /app/server

# 设置时区与运行用户
ENV TZ=Asia/Shanghai
USER app

EXPOSE 8080

# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD wget -qO- http://localhost:8080/health || exit 1

ENTRYPOINT ["/app/server"]

4. Makefile(自动化构建)

makefile
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
COMMIT  ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo none)
TIME    ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
IMAGE   ?= myregistry/myapp

.PHONY: build run scan multi

build:
	docker build \
	  --build-arg VERSION=$(VERSION) \
	  --build-arg COMMIT=$(COMMIT) \
	  --build-arg BUILD_TIME=$(TIME) \
	  -t $(IMAGE):$(VERSION) \
	  -t $(IMAGE):latest \
	  .

run:
	docker run --rm -p 8080:8080 $(IMAGE):$(VERSION)

scan:
	trivy image --severity HIGH,CRITICAL $(IMAGE):$(VERSION)

multi:
	docker buildx build \
	  --platform linux/amd64,linux/arm64 \
	  --build-arg VERSION=$(VERSION) \
	  --build-arg COMMIT=$(COMMIT) \
	  --build-arg BUILD_TIME=$(TIME) \
	  -t $(IMAGE):$(VERSION) \
	  --push \
	  .

5. 构建与验证

bash
# 构建
make build

# 查看大小
docker images myregistry/myapp
# REPOSITORY            TAG        SIZE
# myregistry/myapp      v1.2.3     18MB
# myregistry/myapp      latest     18MB

# 运行
make run

# 测试
curl http://localhost:8080/version
# {"version":"v1.2.3","commit":"abc1234","build_time":"2026-08-01T10:00:00Z","go_version":"go1.22.0"}

# 安全扫描
make scan

# 多平台构建并推送
make multi

八、常见优化误区

1. 把所有 RUN 合并成一条

网上常见一种「优化」说法:把多条 RUN 合并成一条减少层数。但在多阶段构建时代,这种优化意义不大——运行阶段镜像只有几条指令,且最终镜像里层是合并存储的。为了减层而牺牲可读性得不偿失。

不过运行阶段的 RUN 确实应该尽量合并,特别是 apk add 后立即清理缓存:

dockerfile
# 推荐:一条命令完成安装和清理
RUN apk --no-cache add ca-certificates tzdata

# 不推荐:分开写
RUN apk add ca-certificates tzdata
RUN rm -rf /var/cache/apk/*

--no-cache 已经自动清理缓存,分两条反而多一层。

2. 过度追求 scratch

scratch 镜像最小,但没有 shell、没有证书、没有时区数据,调试和运维成本高。除非有严格的体积要求,否则 alpine 是更实用的选择。建议默认用 alpine,有特殊需求再考虑 scratch 或 distroless。

3. 忽略 .dockerignore

.dockerignore 不仅影响构建上下文大小,更影响缓存命中。如果 .git 目录、测试文件被发送给 Docker,每次它们变化都会让 COPY . . 缓存失效。务必配置好 .dockerignore

4. 用 latest 标签

生产镜像绝对不要用 latest 标签,它不可追溯。应该用语义化版本(v1.2.3)、Git SHA(abc1234)或两者结合(v1.2.3-abc1234)作为标签。

九、小结

本篇重点讲解了多阶段构建和镜像优化的各种技术。

关键要点:

  1. 多阶段构建是核心:构建阶段用完整环境编译,运行阶段用最小化镜像,最终镜像只含运行必需内容。
  2. 基础镜像选择:scratch(极致小,无调试)、alpine(5MB,最常用)、distroless(高安全,无 shell)。
  3. 编译优化CGO_ENABLED=0 实现静态编译,-ldflags "-s -w" 去除调试信息,-ldflags -X 注入版本信息。
  4. 多平台构建:用 docker buildx --platform 构建 amd64+arm64 镜像,Go 的交叉编译让这一切很自然。
  5. 安全扫描:用 trivy 在 CI 中扫描镜像漏洞,设置 HIGH/CRITICAL 门禁。
  6. 生产级 Dockerfile:综合多阶段、非 root 用户、健康检查、时区配置、版本注入。

下一篇我们将进入 Docker Compose 多服务编排,把 Go 应用与 MySQL、Redis 等依赖服务组合起来,搭建完整的本地开发与测试环境。