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.8GB 30MB - 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,同时大幅提升了安全性。这是生产级容器化应用的最佳实践,值得在项目中推广使用。

更多推荐