跳过正文

Docker 镜像优化实践

·900 字·5 分钟·
目录

一、镜像大小为什么重要
#

很多团队把镜像大小当成无关紧要的小事,直到几个问题同时出现:

  • 拉取速度:冷启动场景(节点扩容、Pod 迁移)依赖镜像拉取速度。一个 1.5 GB 的镜像比 80 MB 的镜像慢 10–20 倍
  • 存储成本:ECR、Docker Hub 按存储量计费,多环境多版本叠加下,几百个镜像的存储费用不可忽视
  • 安全面:镜像越大,包含的软件包越多,CVE 漏洞面越宽。distroless 镜像扫描出的漏洞数量通常是 ubuntu base 的 1/10

一个真实的对比:某 Go 服务未优化镜像 1.2 GB,优化后 18 MB,在 EKS 弹性扩容场景下,Pod 就绪时间从 45 秒降至 8 秒。


二、多阶段构建
#

多阶段构建是镜像优化最核心的手段,核心思路是:构建环境和运行环境分离,只把最终产物复制到运行镜像。

Go 服务完整示例
#

# syntax=docker/dockerfile:1

# ── Stage 1: 构建 ──────────────────────────────────────────────
FROM golang:1.23-alpine AS builder

WORKDIR /build

# 先复制依赖文件,利用 layer 缓存(代码改变时不重新下载依赖)
COPY go.mod go.sum ./
RUN go mod download

# 再复制源码
COPY . .

# 静态编译,不依赖 libc
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
    -ldflags="-s -w -extldflags '-static'" \
    -trimpath \
    -o /app/server \
    ./cmd/server

# ── Stage 2: 运行 ──────────────────────────────────────────────
FROM gcr.io/distroless/static-debian12:nonroot

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

# 如果需要时区数据
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo

EXPOSE 8080

ENTRYPOINT ["/server"]

最终镜像大小:约 15–20 MB(视业务代码量),而 golang:1.23 基础镜像本身就有 800 MB+。

Python 服务示例
#

# syntax=docker/dockerfile:1

# ── Stage 1: 安装依赖 ──────────────────────────────────────────
FROM python:3.12-slim AS dependencies

WORKDIR /app

# 只复制依赖声明文件
COPY requirements.txt ./

RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

# ── Stage 2: 运行 ──────────────────────────────────────────────
FROM python:3.12-slim AS runtime

WORKDIR /app

# 复制已安装的依赖
COPY --from=dependencies /install /usr/local

# 复制应用代码
COPY src/ ./src/

# 非 root 用户
RUN useradd -u 1001 -r appuser
USER appuser

EXPOSE 8000

CMD ["python", "-m", "uvicorn", "src.main:app", "--host", "0.0.0.00", "--port", "8000"]

Node.js 服务示例
#

# syntax=docker/dockerfile:1

# ── Stage 1: 安装依赖 ──────────────────────────────────────────
FROM node:20-alpine AS deps

WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production

# ── Stage 2: 构建 ──────────────────────────────────────────────
FROM node:20-alpine AS builder

WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

COPY . .
RUN npm run build

# ── Stage 3: 运行 ──────────────────────────────────────────────
FROM node:20-alpine AS runtime

WORKDIR /app

# 只复制生产依赖和构建产物
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./

RUN addgroup -g 1001 appgroup && adduser -u 1001 -G appgroup -s /bin/sh -D appuser
USER appuser

EXPOSE 3000
CMD ["node", "dist/index.js"]

三、基础镜像选型
#

镜像典型大小Shell包管理器适用场景
ubuntu:24.04~80 MB✅ bash✅ apt调试/开发
debian:12-slim~75 MB✅ sh✅ apt生产,需 apt 安装运行时依赖
alpine:3.20~8 MB✅ sh✅ apk生产,轻量,musl libc(注意兼容性)
gcr.io/distroless/static~2 MB静态编译二进制(Go)
gcr.io/distroless/base~20 MB需要 glibc 的动态链接程序
gcr.io/distroless/python3~50 MBPython 应用
scratch0 MB纯静态二进制,极限瘦身

实际选型建议

  • Go 静态编译 → distroless/static:nonroot,安全性最好
  • Python/Node → slim 变体 + 非 root 用户
  • 需要调试时 → 单独维护一个 debug 镜像,生产不用

Alpine 的 musl libc 与 glibc 存在微小差异,某些 C 扩展(如部分 Python 包)在 Alpine 上会编译失败或行为异常,踩坑后谨慎使用。


四、Layer 缓存优化
#

Docker 从上到下执行 Dockerfile,某一层变化后,后续所有层都会重新构建。原则:变化频率低的指令放前面

错误示范
#

# 每次代码改动都会导致 npm install 重新执行
COPY . .
RUN npm install

正确做法
#

# 先复制 package.json(不常变)→ install → 再复制源码(频繁变)
COPY package.json package-lock.json ./
RUN npm ci
COPY src/ ./src/

依赖文件先于代码的原则
#

各语言对应规则:

# Go
COPY go.mod go.sum ./
RUN go mod download
COPY . .

# Python
COPY requirements.txt ./
RUN pip install -r requirements.txt
COPY . .

# Java (Maven)
COPY pom.xml ./
RUN mvn dependency:go-offline
COPY src/ ./src/

五、.dockerignore 规范
#

.dockerignore 决定哪些文件不会被发送到 Docker build context,影响构建速度和镜像内容。

# 版本控制
.git
.gitignore

# 依赖目录(通过容器内安装,不从宿主机复制)
node_modules
vendor
__pycache__
*.pyc
*.pyo
.venv
venv

# 构建产物
dist
build
target
*.o
*.a

# 测试文件
**/*_test.go
**/*.test.js
coverage/
.pytest_cache

# 文档
docs
*.md
README*

# 本地配置
.env
.env.local
*.local

# IDE 文件
.idea
.vscode
*.swp

# CI 配置(不需要进镜像)
.github
.gitlab-ci.yml
Jenkinsfile

# Docker 自身文件
Dockerfile*
docker-compose*.yml

一个没有 .dockerignore 的 Node 项目,node_modules 可能有几百 MB,全部发送给 daemon 会让构建上下文膨胀,即使最终镜像不包含这些文件。


六、减小镜像大小的其他技巧
#

合并 RUN 指令
#

# 错误:每个 RUN 产生一个 layer,缓存无法清理
RUN apt-get update
RUN apt-get install -y curl wget
RUN rm -rf /var/lib/apt/lists/*

# 正确:合并为一条,确保缓存清理在同一层
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
      curl \
      wget \
      ca-certificates && \
    rm -rf /var/lib/apt/lists/*

--no-install-recommends
#

apt 默认会安装推荐包,加上这个参数只安装必需依赖:

RUN apt-get install -y --no-install-recommends nginx

pip 无缓存安装
#

RUN pip install --no-cache-dir -r requirements.txt

清理构建工具
#

RUN apk add --no-cache --virtual .build-deps \
      gcc musl-dev python3-dev && \
    pip install --no-cache-dir -r requirements.txt && \
    apk del .build-deps

七、漏洞扫描:Trivy
#

镜像构建完成后,用 Trivy 扫描 CVE:

# 安装 trivy
brew install aquasecurity/trivy/trivy  # macOS
# 或直接拉 docker 镜像
docker run --rm aquasec/trivy image my-app:latest

# 扫描本地镜像,只显示 HIGH 和 CRITICAL
trivy image --severity HIGH,CRITICAL my-app:latest

# 扫描并输出 JSON,供 CI 解析
trivy image --format json --output trivy-report.json my-app:latest

# 在 CI 中设置失败阈值
trivy image --exit-code 1 --severity CRITICAL my-app:latest

在 GitHub Actions 中集成:

- name: 漏洞扫描
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: ${{ env.IMAGE_URI }}
    format: sarif
    output: trivy-results.sarif
    severity: HIGH,CRITICAL
    exit-code: 1

- name: 上传扫描结果到 GitHub Security
  uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: trivy-results.sarif

八、构建缓存加速
#

BuildKit cache mount(最有效)
#

BuildKit 的 --mount=type=cache 允许在构建间持久化缓存目录,不会写入镜像层:

# syntax=docker/dockerfile:1

# Go 模块缓存
RUN --mount=type=cache,target=/go/pkg/mod \
    --mount=type=cache,target=/root/.cache/go-build \
    go build -o /app/server ./cmd/server

# pip 缓存
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

# npm 缓存
RUN --mount=type=cache,target=/root/.npm \
    npm ci

启用 BuildKit:

DOCKER_BUILDKIT=1 docker build .
# 或
docker buildx build .

GitHub Actions 缓存
#

- name: 设置 Docker Buildx
  uses: docker/setup-buildx-action@v3

- name: 构建并推送
  uses: docker/build-push-action@v5
  with:
    context: .
    push: true
    tags: ${{ env.IMAGE_URI }}
    cache-from: type=gha
    cache-to: type=gha,mode=max

type=gha 使用 GitHub Actions Cache,mode=max 缓存所有中间层,首次构建后后续构建速度提升明显。


九、实测对比
#

以一个实际 Go 微服务为例:

优化阶段镜像大小构建时间(有缓存)漏洞数(HIGH+)
原始(golang:1.21 + 应用代码)1.24 GB3m 20s47
多阶段构建(alpine runtime)38 MB1m 05s12
多阶段构建(distroless/static)18 MB58s0
distroless + BuildKit cache18 MB12s0

关键结论:

  • 多阶段构建是必做项,大小从 GB 级降到 MB 级
  • distroless 相比 alpine 大小差不多,但漏洞清零,安全优先选 distroless
  • BuildKit cache mount 对 CI 环境价值最大,有依赖变化时也只需重新安装变化部分

十、完整的生产级 Dockerfile 检查清单
#

  • 使用多阶段构建,运行镜像不包含编译工具
  • 选择最小化基础镜像(distroless / slim / alpine)
  • 依赖文件先于源码 COPY,充分利用缓存
  • .dockerignore 排除不必要文件
  • RUN 指令合并,清理包管理器缓存
  • 以非 root 用户运行(USER nonroot 或自建用户)
  • 设置 HEALTHCHECK
  • 使用 BuildKit cache mount 加速依赖安装
  • CI 中集成 Trivy 漏洞扫描,CRITICAL 级别阻断构建
  • 镜像 tag 包含 commit SHA,可追溯
Wenzhuo Huang
作者
Wenzhuo Huang
搞运维的工程师,写代码的运维人。专注 Kubernetes、AWS、GitOps 与基础设施可靠性。这个博客既是我的技术笔记本,也是我踩过的坑的受害者档案。

相关文章

ArgoCD + Kustomize GitOps 体系实践

·2128 字·10 分钟
记录在多套 K8s 集群(AWS EKS + 阿里云 ACK)上落地 GitOps 的完整过程:目录结构设计、Kustomize overlay 环境差异管理、ArgoCD ApplicationSet 自动化、以及真实踩过的坑。