Docker 多阶段构建深度实战:Nginx Ingress Controller 的三阶段源码构建

1. 引言

在云原生生态中,Nginx Ingress Controller 是 Kubernetes 集群流量入口的核心组件。它通常由两部分组成:Nginx 核心(负责流量转发)和Go 语言编写的 Controller(负责监听 Kubernetes API 并动态更新 Nginx 配置)。

为了构建一个生产可用的 Nginx Ingress Controller 镜像,我们需要经历三个关键阶段:

  1. GCC 构建阶段:从 Nginx 源码编译出 Nginx 二进制文件。
  2. Golang 构建阶段:编译 Go 语言编写的 Controller 二进制文件。
  3. Runtime 运行阶段:将前两个阶段产出的二进制文件合并到一个极简的运行镜像中。

本文将结合具体的 Dockerfile,深入讲解如何通过 Docker 多阶段构建实现这三个阶段的隔离与协作,最终得到一个体积小、安全性高的生产级镜像。

2. 为什么需要三阶段构建?

2.1 传统单阶段构建的困境

如果在一个 Dockerfile 中同时安装 GCC、Go 工具链、Nginx 源码和所有依赖,最终镜像将包含:

  • GCC、Make、Perl 等编译工具(数百 MB)
  • Go 编译器及标准库(数百 MB)
  • Nginx 源码及编译中间产物
  • 运行时不需要的调试工具和文档

这样的镜像体积通常在1.5GB 以上,且包含了大量不必要的攻击面。

2.2 多阶段构建的核心思想

Docker 多阶段构建允许我们在一个 Dockerfile 中使用多个FROM指令,每个阶段可以基于不同的基础镜像。我们可以:

  • 阶段 1(GCC Build):使用包含 GCC、Make 等编译工具的镜像,从 Nginx 源码编译出 Nginx 二进制。
  • 阶段 2(Golang Build):使用 Go 官方镜像,编译 Controller 二进制。
  • 阶段 3(Runtime):使用极简的 Alpine 或 Distroless 镜像,只从前面两个阶段复制编译好的二进制文件和必要的运行时依赖。

这样,最终镜像只包含运行所需的最小文件集,体积可缩小到50MB 以下

3. 三阶段 Dockerfile 实战

下面是一个完整的 Dockerfile 示例,展示了如何构建一个 Nginx Ingress Controller 镜像。

3.1 第一阶段:GCC 构建 Nginx

# ========== 第一阶段:GCC 构建 Nginx ========== # 使用包含完整编译工具链的镜像 FROM ubuntu:22.04 AS nginx-builder # 安装编译 Nginx 所需的工具和库 RUN apt-get update && apt-get install -y \ gcc \ make \ libpcre3-dev \ libssl-dev \ zlib1g-dev \ wget \ && rm -rf /var/lib/apt/lists/* # 下载并解压 Nginx 源码 WORKDIR /tmp RUN wget https://nginx.org/download/nginx-1.25.3.tar.gz && \ tar -xzf nginx-1.25.3.tar.gz # 编译 Nginx(配置常用模块,生成静态链接的二进制) WORKDIR /tmp/nginx-1.25.3 RUN ./configure \ --prefix=/usr/local/nginx \ --with-http_ssl_module \ --with-http_v2_module \ --with-http_realip_module \ --with-http_stub_status_module \ --with-stream \ --with-stream_ssl_module \ --without-http_autoindex_module \ --without-http_ssi_module \ && make -j$(nproc) \ && make install # 将编译好的 Nginx 二进制和配置文件复制到一个临时目录,方便后续阶段复制 RUN mkdir -p /nginx-output/usr/local/nginx && \ cp -r /usr/local/nginx/sbin /nginx-output/usr/local/nginx/ && \ cp -r /usr/local/nginx/conf /nginx-output/usr/local/nginx/ && \ cp -r /usr/local/nginx/html /nginx-output/usr/local/nginx/ && \ cp -r /usr/local/nginx/logs /nginx-output/usr/local/nginx/

关键点

  • 使用ubuntu:22.04作为基础镜像,它包含了 GCC、Make 等编译工具。
  • 通过apt-get install安装 Nginx 编译所需的依赖库(PCRE、OpenSSL、Zlib)。
  • 从源码编译 Nginx,并只保留编译产物(二进制、配置文件、静态文件)。
  • --without-http_autoindex_module等选项用于禁用不需要的模块,进一步精简。

3.2 第二阶段:Golang 构建 Controller

# ========== 第二阶段:Golang 构建 Controller ========== FROM golang:1.21 AS go-builder # 设置工作目录 WORKDIR /workspace # 复制 go.mod 和 go.sum,利用 Docker 缓存层 COPY go.mod go.sum ./ RUN go mod download # 复制所有源码 COPY . . # 编译 Go 项目,生成静态链接的二进制文件 # CGO_ENABLED=0 确保生成静态二进制,不依赖 libc RUN CGO_ENABLED=0 GOOS=linux go build \ -a -installsuffix cgo \ -ldflags="-s -w" \ -o nginx-ingress-controller \ ./cmd/nginx-ingress-controller # 将编译好的二进制文件复制到临时目录 RUN mkdir -p /go-output && \ cp /workspace/nginx-ingress-controller /go-output/

关键点

  • 使用golang:1.21作为 Go 编译环境。
  • CGO_ENABLED=0生成静态链接的 Go 二进制,不依赖任何动态库。
  • -ldflags="-s -w"用于去除调试信息,进一步减小二进制体积。
  • 只复制编译好的二进制文件到临时目录,Go 工具链和源码都被丢弃。

3.3 第三阶段:Runtime 运行环境

# ========== 第三阶段:Runtime 运行环境 ========== # 使用极简的 Alpine 镜像作为运行环境 FROM alpine:3.19 # 安装运行时依赖 # - ca-certificates:用于 HTTPS 证书验证 # - tzdata:时区数据 # - libcrypto3、libssl3:Nginx 运行时需要的 SSL 库 RUN apk add --no-cache \ ca-certificates \ tzdata \ libcrypto3 \ libssl3 \ pcre # 从第一阶段(GCC 构建)复制 Nginx 二进制和配置文件 COPY --from=nginx-builder /nginx-output/usr/local/nginx /usr/local/nginx # 从第二阶段(Golang 构建)复制 Controller 二进制 COPY --from=go-builder /go-output/nginx-ingress-controller /usr/local/bin/ # 创建必要的目录和用户 RUN addgroup -S nginx && \ adduser -S -G nginx nginx && \ mkdir -p /var/log/nginx && \ mkdir -p /var/cache/nginx && \ chown -R nginx:nginx /var/log/nginx /var/cache/nginx /usr/local/nginx # 暴露端口 EXPOSE 80 443 # 设置容器启动命令(实际启动由 Controller 管理 Nginx) CMD ["nginx-ingress-controller"]

关键点

  • 使用alpine:3.19作为运行基础镜像,体积仅约 5MB。
  • 只安装 Nginx 运行时需要的动态库(libcrypto3libssl3pcre),不安装任何编译工具。
  • 通过COPY --from=nginx-builderCOPY --from=go-builder分别从两个构建阶段复制产物。
  • 创建非 root 用户nginx,提升容器安全性。

4. 三阶段构建的优势分析

4.1 构建环境与运行环境完全隔离

方面传统单阶段构建三阶段多阶段构建
基础镜像ubuntu:22.04+golang:1.21(约 1.5GB)alpine:3.19(约 5MB)
包含工具GCC、Make、Go 编译器、Perl、调试工具等仅运行时动态库
最终镜像体积1.2GB - 1.8GB30MB - 60MB
攻击面大量不必要的工具和库最小化依赖,攻击面大幅减少

4.2 镜像体积对比

通过三阶段构建,最终镜像体积从1.5GB 降至约 50MB,提升了约30 倍。这意味着:

  • 更快的拉取和推送:在 CI/CD 流程中,镜像传输时间大幅缩短。
  • 更快的容器启动:镜像解压和加载时间减少。
  • 更低的存储成本:镜像仓库和节点本地存储占用减少。

4.3 安全性提升

  • 最小化攻击面:运行镜像中不包含 GCC、Make、Go 编译器、Perl 等工具,攻击者无法利用这些工具进行提权或横向移动。
  • 静态链接的 Go 二进制CGO_ENABLED=0生成的 Go 二进制不依赖libc,避免了libc漏洞(如 Shellshock、Ghost)的影响。
  • 非 root 用户运行:创建专用用户运行 Nginx 和 Controller,遵循最小权限原则。

4.4 构建缓存优化

# 利用 Docker 缓存层,避免重复下载依赖 COPY go.mod go.sum ./ RUN go mod download COPY . .

go.modgo.sum的复制放在源码复制之前,只要依赖文件没有变化,Docker 就会复用go mod download的缓存层,大幅加速构建过程。

5. 完整 Dockerfile 汇总

为了方便你直接使用,下面是完整的 Dockerfile 内容:

# ========== 第一阶段:GCC 构建 Nginx ========== FROM ubuntu:22.04 AS nginx-builder RUN apt-get update && apt-get install -y \ gcc make libpcre3-dev libssl-dev zlib1g-dev wget \ && rm -rf /var/lib/apt/lists/* WORKDIR /tmp RUN wget https://nginx.org/download/nginx-1.25.3.tar.gz && \ tar -xzf nginx-1.25.3.tar.gz WORKDIR /tmp/nginx-1.25.3 RUN ./configure \ --prefix=/usr/local/nginx \ --with-http_ssl_module \ --with-http_v2_module \ --with-http_realip_module \ --with-http_stub_status_module \ --with-stream \ --with-stream_ssl_module \ --without-http_autoindex_module \ --without-http_ssi_module \ && make -j$(nproc) && make install RUN mkdir -p /nginx-output/usr/local/nginx && \ cp -r /usr/local/nginx/sbin /nginx-output/usr/local/nginx/ && \ cp -r /usr/local/nginx/conf /nginx-output/usr/local/nginx/ && \ cp -r /usr/local/nginx/html /nginx-output/usr/local/nginx/ && \ cp -r /usr/local/nginx/logs /nginx-output/usr/local/nginx/ # ========== 第二阶段:Golang 构建 Controller ========== FROM golang:1.21 AS go-builder WORKDIR /workspace COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build \ -a -installsuffix cgo \ -ldflags="-s -w" \ -o nginx-ingress-controller \ ./cmd/nginx-ingress-controller RUN mkdir -p /go-output && \ cp /workspace/nginx-ingress-controller /go-output/ # ========== 第三阶段:Runtime 运行环境 ========== FROM alpine:3.19 RUN apk add --no-cache \ ca-certificates tzdata libcrypto3 libssl3 pcre COPY --from=nginx-builder /nginx-output/usr/local/nginx /usr/local/nginx COPY --from=go-builder /go-output/nginx-ingress-controller /usr/local/bin/ RUN addgroup -S nginx && \ adduser -S -G nginx nginx && \ mkdir -p /var/log/nginx /var/cache/nginx && \ chown -R nginx:nginx /var/log/nginx /var/cache/nginx /usr/local/nginx EXPOSE 80 443 CMD ["nginx-ingress-controller"]

6. 总结

通过本文的实战,我们深入了解了 Docker 多阶段构建在 Nginx Ingress Controller 场景下的强大能力:

  1. GCC 构建阶段:从 Nginx 源码编译出 Nginx 二进制,使用完整的编译工具链。
  2. Golang 构建阶段:编译 Go 语言编写的 Controller,生成静态链接的二进制。
  3. Runtime 运行阶段:将前两个阶段的产物合并到极简的 Alpine 镜像中,只保留运行时依赖。

这种三阶段构建模式实现了构建环境与运行环境的完全隔离,最终镜像体积从 1.5GB 降至约 50MB,同时大幅提升了安全性。这是生产级容器化应用的最佳实践,值得在项目中推广使用。