一、Dockerfile 的作用

​ Docker 镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。

​ 镜像的定制实际上就是定制每一层所添加的配置、文件。如果我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像,那么无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决。这个脚本就是 Dockerfile。

​ Dockerfile 是一个文本文件,其内包含了一条条的指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。有了 Dockerfile,当我们需要定制自己额外的需求时,只需在 Dockerfile 上添加或者修改指令,重新生成 image 即可,省去了敲命令的麻烦。

二、多阶段构建

第一阶段:编译构建
↓
第二阶段:运行时环境
↓
最终产物:精简的生产镜像

三、源码解析

Dockerfile源码

# ==================== 多阶段构建(Multi-Stage Build) ====================
# 云原生最佳实践:使用多阶段构建减小镜像体积
# 第一阶段(builder):编译 Go 程序
# 第二阶段(runtime):只包含编译后的二进制文件
# 最终镜像大小通常只有 10-20MB(对比传统方式 几百MB)

# ---------- 阶段1:编译 ----------
FROM golang:1.22-alpine AS builder

# 替换 Alpine 软件源为阿里云镜像(国内环境必须,否则下载超时)
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories

# 安装编译需要的工具
RUN apk add --no-cache git ca-certificates

# 设置工作目录
WORKDIR /app

# 设置 Go 模块代理(国内环境加速依赖下载)
ENV GOPROXY=https://goproxy.cn,direct

# 先复制依赖文件,利用 Docker 层缓存
# 只有 go.mod/go.sum 变化时才重新下载依赖
COPY go.mod go.sum ./
RUN go mod download

# 复制源代码
COPY . .

# 编译
# CGO_ENABLED=0: 禁用 CGO,生成静态链接的二进制(不依赖 C 库)
# -ldflags="-s -w": 去除调试信息,减小二进制体积
RUN CGO_ENABLED=0 GOOS=linux go build \
-ldflags="-s -w" \
-o /app/server \
./cmd/server

# ---------- 阶段2:运行时 ----------
# 使用最小基础镜像 scratch 或 distroless
# scratch 是空镜像,distroless 包含基础运行时
FROM alpine:3.19

# 替换 Alpine 软件源为阿里云镜像
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories

# 安装 CA 证书(HTTPS 请求需要)和时区数据
RUN apk --no-cache add ca-certificates tzdata

# 创建非 root 用户(安全最佳实践)
RUN adduser -D -g '' appuser

# 设置工作目录
WORKDIR /app

# 从 builder 阶段复制编译好的二进制
COPY --from=builder /app/server .

# 使用非 root 用户运行(安全最佳实践)
USER appuser

# 暴露端口(仅作文档用途,实际端口由运行时环境变量控制)
EXPOSE 8080

# 健康检查
# Docker 原生健康检查,Kubernetes 中通常使用 Probe 替代
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/healthz || exit 1

# 启动命令
ENTRYPOINT ["./server"]

四、Docker 多阶段构建为什么能压缩体积

核心原理

Docker 镜像是分层叠加的——每一条 RUNCOPY 指令都会新增一层,所有层的内容都会保留在最终镜像中,即使后面删除了也不会真正减小体积

多阶段构建的本质:用一个大而全的环境编译,然后只把编译产物复制到一个干净的小镜像里,构建环境的几百 MB "垃圾"不会进入最终镜像。


单阶段 vs 多阶段对比

单阶段构建(传统方式)

FROM golang:1.22-alpine

WORKDIR /app
COPY . .
RUN go mod download
RUN CGO_ENABLED=0 go build -o server ./cmd/server

ENTRYPOINT ["./server"]

最终镜像里包含了什么?

内容 大小(约) 运行时需要?
Alpine Linux 基础系统 ~7 MB
Go 编译器 + 工具链 ~500 MB
Go 模块缓存(所有依赖源码) ~100 MB
项目源代码(.go 文件) ~5 MB
git 等构建工具 ~20 MB
编译好的二进制 server ~15 MB
总计 ~650 MB

实际运行只需要那个 15MB 的二进制,但其余 600 多 MB 的"构建垃圾"全部被打进了镜像。

多阶段构建

# 阶段1: builder —— 这个阶段的所有内容最终会被丢弃
FROM golang:1.22-alpine AS builder
RUN apk add --no-cache git ca-certificates
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app/server ./cmd/server

# 阶段2: runtime —— 只有这个阶段的内容进入最终镜像
FROM alpine:3.19
RUN apk --no-cache add ca-certificates tzdata
COPY --from=builder /app/server .    # 只拿走编译好的二进制
ENTRYPOINT ["./server"]

最终镜像里包含什么?

内容 大小(约) 运行时需要?
Alpine Linux 基础系统 ~7 MB
ca-certificates + tzdata ~2 MB
编译好的二进制 server ~15 MB
总计 ~24 MB

Go 编译器、源代码、依赖缓存、git 等全部留在了 builder 阶段,没有进入最终镜像。


关键机制图解

┌─────────────────────────────────────┐
│  阶段1: builder (golang:1.22-alpine)│
│                                     │
│  ├── Go 编译器        ~500 MB       │
│  ├── 依赖源码缓存     ~100 MB       │
│  ├── 项目源代码        ~5 MB        │
│  ├── git 等工具        ~20 MB       │
│  └── 编译产物 server   ~15 MB  ──────────┐
│                                     │    │
│(整个阶段被丢弃,不进入最终镜像)    │    │
└─────────────────────────────────────┘    │
                                           │ COPY --from=builder
┌─────────────────────────────────────┐    │
│  阶段2: runtime (alpine:3.19)       │    │
│                                     │    │
│  ├── Alpine 基础系统   ~7 MB        │    │
│  ├── ca-certs + tzdata ~2 MB        │    │
│  └── server 二进制     ~15 MB  ◄─────────┘
│                                     │
│  最终镜像 = ~24 MB                  │
└─────────────────────────────────────┘

为什么单阶段删除也没用?

你可能会想:在单阶段里编译完后删掉 Go 编译器不行吗?

FROM golang:1.22-alpine
WORKDIR /app
COPY . .
RUN go build -o server ./cmd/server
RUN rm -rf /usr/local/go    # 删掉 Go 编译器

不行。 Docker 每条指令是一层,rm 只是在新层标记"删除",但底层那 500MB 仍然存在于之前的层中。镜像体积 = 所有层之和,删除操作不会减小已有层的大小

即使合并成一条 RUN

RUN go build -o server ./cmd/server && rm -rf /usr/local/go

虽然同一层内删除有效,但基础镜像 golang:1.22-alpine 本身就带了 500MB 的 Go 工具链,这部分在更底层,无论如何删不掉。


当前 Dockerfile 中的其他优化技巧

除了多阶段构建,Dockerfile 还使用了以下优化:

1. 静态编译

RUN CGO_ENABLED=0 GOOS=linux go build ...
  • CGO_ENABLED=0:禁用 CGO,生成静态链接的二进制文件
  • 不依赖任何 C 库,运行时镜像不需要安装 glibc
  • 这也是为什么可以使用极简的 Alpine 甚至 scratch 镜像

2. 去除调试信息

-ldflags="-s -w"
  • -s:去除符号表
  • -w:去除 DWARF 调试信息
  • 二进制体积可减小约 30%

3. 依赖缓存分离

COPY go.mod go.sum ./
RUN go mod download
COPY . .
  • 先复制 go.modgo.sum,单独下载依赖
  • 只有依赖变化时才重新下载,源码变化不影响依赖缓存层
  • 这是构建速度优化,不直接影响体积,但大幅减少重复构建时间

4. 最小运行时基础镜像

FROM alpine:3.19
基础镜像 大小
ubuntu:22.04 ~77 MB
debian:12-slim ~74 MB
alpine:3.19 ~7 MB
scratch(空镜像) 0 MB

Alpine 是目前最常用的轻量级基础镜像,比 Ubuntu 小 10 倍。


总结

优化手段 效果
多阶段构建 650 MB → 24 MB,去除编译环境
静态编译 CGO_ENABLED=0 不依赖 C 库,可用极简镜像
去调试信息 -ldflags="-s -w" 二进制体积减小 ~30%
依赖缓存分离 加速重复构建
Alpine 基础镜像 比 Ubuntu 小 10 倍

一句话总结:多阶段构建 = 大环境编译 + 小环境运行,编译垃圾不进最终镜像。

最后修改:2026 年 02 月 25 日
如果觉得我的文章对你有用,请随意赞赏