背景

上一篇聊了 Docker 多阶段构建,重点是镜像体积和构建效率。

但把镜像做小,只解决了一半问题。真正要上线时,还得继续问几个更现实的问题:

  • 镜像里有没有不该带进去的工具
  • 容器是不是还在用 root 运行
  • 依赖包有没有已知漏洞
  • 基础镜像是不是长期没人维护

很多项目的 Dockerfile 确实“能跑”,但离“适合上线”还差不少。

小镜像通常也更安全

这不是绝对规律,但大体成立。

原因很简单:

  • 装得越多,攻击面越大
  • 多余工具越多,漏洞概率越高
  • 调试方便的环境,往往也更容易被滥用

所以镜像瘦身和安全加固,在很多时候是同一个方向上的事情。

不要默认用 root 运行

很多 Dockerfile 最容易忽略的一点,就是进程默认是 root。

FROM alpine:3.20
WORKDIR /app
COPY myapp /app/myapp
ENTRYPOINT ["/app/myapp"]

这份文件能跑,但容器里的进程权限过大。

更稳妥的方式是显式创建低权限用户:

FROM alpine:3.20

RUN addgroup -S appgroup && adduser -S appuser -G appgroup

WORKDIR /app
COPY myapp /app/myapp

USER appuser
ENTRYPOINT ["/app/myapp"]

这样即使应用被利用,攻击者拿到的默认权限也会更低。

基础镜像要尽量克制

基础镜像选型很影响最终的安全边界。

常见选择大概是:

  • ubuntu / debian:通用,但内容更多
  • alpine:体积小,适合简单运行时
  • distroless:更克制,适合生产环境
  • scratch:最小,但调试和兼容性要求更高

如果你的程序是静态编译的 Go 服务,通常可以考虑 scratch 或 distroless。

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

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

distroless 的好处在于:

  • 没有 shell、包管理器等额外工具
  • scratch 更适合真实生产运行时
  • 安全边界通常更清晰

别把敏感信息打进镜像

这是很老的问题,但还是经常出现。

典型错误包括:

  • .env 直接 COPY 进去
  • 在 Dockerfile 里写死 token
  • 构建时把私钥、证书留在镜像层里
# 不推荐
ENV ACCESS_KEY=xxx
ENV SECRET_KEY=yyy

敏感配置应该放在运行时注入,而不是做进镜像。

如果你用 Kubernetes,就交给 Secret、配置中心或外部密钥管理系统。

包管理器缓存和临时文件要清理

很多镜像之所以臃肿,不是因为应用本身大,而是构建过程留下了一堆没用的东西。

RUN apt-get update && apt-get install -y curl ca-certificates \
    && rm -rf /var/lib/apt/lists/*

对于 Alpine 也是类似思路:

RUN apk add --no-cache ca-certificates

这些都是小事,但镜像安全很多时候就是靠这种“小事”一点点堆出来的。

扫描漏洞要放进流程里

镜像安全不能靠肉眼看 Dockerfile。

至少要让镜像扫描进入 CI:

  • Trivy
  • Grype
  • Docker Scout
  • 云厂商自带镜像扫描服务

它们不一定能解决所有问题,但至少能让你尽早发现:

  • 基础镜像已知 CVE
  • 系统库版本过旧
  • 依赖包存在公开漏洞

这类问题如果等到上线后再发现,通常修起来会比预防阶段麻烦得多。

.dockerignore 也属于安全边界

很多人把 .dockerignore 只当构建速度优化工具,但它其实也影响安全。

一个最基础的例子:

.git
.env
node_modules
dist
coverage
*.pem
*.key

如果不忽略这些文件,它们就可能被打进构建上下文,甚至被 COPY 到镜像里。

尤其是私钥、测试数据、调试脚本这类文件,完全没必要出现在生产镜像中。

一个相对稳妥的 Go 生产镜像示例

FROM golang:1.22-alpine AS builder
RUN apk add --no-cache ca-certificates git
WORKDIR /src

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

COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o app .

FROM gcr.io/distroless/static-debian12
WORKDIR /
COPY --from=builder /src/app /app

USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/app"]

这份镜像至少满足了几个基本要求:

  • 多阶段构建,不把工具链带进运行时
  • 运行时镜像足够精简
  • 非 root 用户运行
  • 没有多余 shell 和包管理器

总结

Docker 镜像上线前,至少要经过两轮判断:

  1. 它是不是足够小、足够干净
  2. 它是不是足够安全、足够克制

很多时候,安全不是靠某一个“大招”实现的,而是靠这些基础约束:

  • 低权限运行
  • 精简基础镜像
  • 敏感信息不入镜像
  • 漏洞扫描进 CI

这些动作都不复杂,但长期收益非常高。


上线环境最怕“为了方便多装一点”,因为今天的方便,往往会变成明天的攻击面。