背景
上一篇聊了 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 镜像上线前,至少要经过两轮判断:
- 它是不是足够小、足够干净
- 它是不是足够安全、足够克制
很多时候,安全不是靠某一个“大招”实现的,而是靠这些基础约束:
- 低权限运行
- 精简基础镜像
- 敏感信息不入镜像
- 漏洞扫描进 CI
这些动作都不复杂,但长期收益非常高。
上线环境最怕“为了方便多装一点”,因为今天的方便,往往会变成明天的攻击面。