Dockerfile 编写指南
1. 最小示例
# syntax=docker/dockerfile:1
FROM alpine:3.23
RUN addgroup -S app && adduser -S -G app -u 10001 app
WORKDIR /app
COPY --chown=app:app ./bin/server /app/server
USER app
EXPOSE 8080
ENTRYPOINT ["/app/server"]
CMD ["--listen", "0.0.0.0:8080"]构建与运行:
docker build --pull -t team-server:dev .
docker run --rm -p 127.0.0.1:8080:8080 team-server:dev2. 构建上下文
命令末尾的 . 是构建上下文:
docker build -f docker/Dockerfile -t app:dev .COPY 只能读取上下文中的文件,不能通过 ../ 逃出上下文。上下文过大会增加传输、缓存计算和 secret 泄露风险。
.dockerignore 示例:
.git
.env
*.pem
*.key
node_modules
dist
coverage
tmp
docs
!package-lock.json用 docker build --progress=plain 观察上下文传输。不要把 .git、凭据或本地构建产物默认送入 builder。
3. FROM
FROM golang:1.26-alpine AS build
FROM gcr.io/distroless/static-debian13:nonroot选择基础镜像:
- 来自可信 Registry 和维护者;
- 版本在支持期;
- 大小适合,但不能只追求最小而牺牲可运维性;
- 明确 libc、证书、时区和调试需求;
- 生产可按 digest 锁定。
FROM alpine:3.23@sha256:REPLACE_WITH_APPROVED_DIGEST按 digest 可复现,但不会自动获取安全更新;需要依赖更新机器人和定期重建。
4. RUN
Debian/Ubuntu:
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl \
&& rm -rf /var/lib/apt/lists/*Alpine:
RUN apk add --no-cache ca-certificates tzdata将更新、安装和缓存清理放在同一层,避免复用过期 package index。安装包应最少且必要。
BuildKit cache mount:
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg/mod \
go build -o /out/server ./cmd/server5. COPY 与 ADD
COPY go.mod go.sum ./
COPY --chown=10001:10001 ./config /app/config优先 COPY。ADD 具有 URL、Git 和本地 tar 自动解包等额外语义,只在明确需要时使用。远程下载通常用受校验的 RUN curl ... && sha256sum -c,或使用 BuildKit 支持的 checksum 能力。
5.1 缓存友好的顺序
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o /out/server ./cmd/server先复制低频变化的依赖清单,再复制源码,可复用依赖层。
6. WORKDIR
WORKDIR /app优于:
RUN cd /app && ...WORKDIR 会影响后续 RUN、CMD、ENTRYPOINT、COPY 和 ADD,必要时会创建目录。
7. ARG 与 ENV
ARG VERSION=dev
ENV APP_VERSION=$VERSIONARG主要在构建时使用,但值可能出现在构建历史或证明材料中。ENV保留在镜像配置和运行容器中。- 二者都不是 Secret 机制。
Secret:
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
npm ci构建:
docker build --secret id=npmrc,src="$HOME/.npmrc" .8. CMD 与 ENTRYPOINT
推荐 exec form:
ENTRYPOINT ["/app/server"]
CMD ["--config", "/app/config.yaml"]运行时:
# 替换 CMD,保留 ENTRYPOINT
docker run app:latest --config /tmp/test.yaml
# 替换 ENTRYPOINT
docker run --entrypoint sh app:debugShell form:
CMD /app/server --config /app/config.yaml会经 /bin/sh -c,环境变量展开方便,但信号和参数行为不同。服务进程优先 exec form。
9. USER
RUN addgroup --system --gid 10001 app \
&& adduser --system --uid 10001 --ingroup app app
USER 10001:10001最后的运行阶段应以非 root 用户运行。需要绑定 80/443 时,通常让反向代理发布宿主端口到容器高端口,而不是给应用 root。
构建时 USER 会影响后续 RUN,必要时:
USER root
RUN ...
USER 10001:1000110. EXPOSE
EXPOSE 8080/tcp这是文档性元数据,不会开放宿主端口,也不能替代应用监听地址。
11. VOLUME
VOLUME ["/var/lib/app"]镜像作者声明 VOLUME 后,使用者会得到隐式数据生命周期,可能产生匿名卷。团队内部镜像通常更适合在 Compose/部署层明确挂载,让运维知道卷名称、备份和保留策略。
12. HEALTHCHECK
HEALTHCHECK --interval=30s --timeout=3s \
--start-period=20s --retries=3 \
CMD ["/app/healthcheck"]或禁用继承检查:
HEALTHCHECK NONE健康检查二进制必须存在。不要为了 curl 健康端点给极简镜像增加大量无关包,可内置小型检查命令。
13. LABEL
LABEL org.opencontainers.image.title="team-api" \
org.opencontainers.image.version="$VERSION" \
org.opencontainers.image.revision="$REVISION" \
org.opencontainers.image.source="https://git.example.internal/team/api"构建元数据应与 CI 中实际 Git commit 对应。敏感仓库 URL 或内部结构按组织策略处理。
14. 多阶段构建
Go 示例:
# syntax=docker/dockerfile:1
FROM golang:1.26-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod go mod download
COPY . .
ARG TARGETOS
ARG TARGETARCH
RUN --mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \
go build -trimpath -ldflags="-s -w" -o /out/server ./cmd/server
FROM gcr.io/distroless/static-debian13:nonroot
COPY --from=build --chown=nonroot:nonroot /out/server /server
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/server"]构建依赖不进入最终镜像,可显著减少攻击面和大小。
15. Node.js 多阶段示例
# syntax=docker/dockerfile:1
FROM node:24-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
FROM deps AS build
COPY . .
RUN npm run build
FROM node:24-alpine AS runtime
ENV NODE_ENV=production
WORKDIR /app
RUN addgroup -S app && adduser -S -G app -u 10001 app
COPY --from=deps --chown=app:app /app/node_modules ./node_modules
COPY --from=build --chown=app:app /app/dist ./dist
COPY --chown=app:app package.json ./
USER app
CMD ["node", "dist/server.js"]实际框架可能需要 production dependency pruning、standalone 输出或运行时静态资源,应依据项目构建方式调整。
16. Shell 安全
管道中前面的命令失败,默认 /bin/sh 可能仍返回最后一个命令的状态。支持的镜像可配置:
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN curl -fsSL https://example.invalid/archive.tar.gz \
| tar -xz -C /opt更稳妥的是下载、校验、再解包。Alpine 的 /bin/sh 不等于 bash,不要盲目设置不存在的 shell。
17. 缓存失效
Dockerfile 指令与依赖内容共同决定缓存。常用操作:
docker build --no-cache --pull -t app:fresh .
docker build --progress=plain -t app:debug .--no-cache 不等于重新拉取基础镜像,所以安全重建通常还要 --pull。
18. 可复现性
提高可复现性:
- 锁定基础镜像 digest;
- 锁定语言依赖;
- 固定构建器版本/兼容版本;
- 不在构建中依赖当前时间;
- 使用
SOURCE_DATE_EPOCH(工具支持时); - 记录 source revision、SBOM、provenance;
- 在干净环境重复构建并比较 digest。
完全可复现需要工具链和压缩/时间戳等所有输入一致,不是只锁一个 base image。
19. Lint 与测试
docker build --check .
docker build --target test --progress=plain .可定义测试阶段:
FROM build AS test
RUN go test ./...CI 先构建 test target,再构建最终 target。扫描最终镜像和构建证明材料。
20. 反模式
- 使用浮动
latest且不记录 digest。 - 把 token、SSH key 或
.envCOPY进镜像。 - 用
ARG传长期 secret。 - 最终阶段保留编译器、包管理缓存和测试数据。
- 以 root 运行服务。
- 用
chmod -R 777解决权限。 - 把数据库数据写进镜像或容器层。
- 每次
COPY . .都在依赖安装前。 - 在容器启动脚本里在线安装依赖。
- 用
docker commit替代 Dockerfile。
21. Review 清单
- [ ] 首行声明受支持的 Dockerfile syntax
- [ ] base image 来源可信、版本受支持、更新策略明确
- [ ]
.dockerignore排除敏感和无关文件 - [ ] 依赖锁定且缓存顺序合理
- [ ] 使用多阶段构建
- [ ] 最终镜像无编译工具和包缓存
- [ ] 不含凭据
- [ ] 使用非 root 用户
- [ ] exec-form 启动并正确处理信号
- [ ] 健康检查低开销、无副作用
- [ ] OCI labels、SBOM、provenance 可追溯
- [ ] 镜像扫描和真实启动测试通过
22. 官方参考
继续阅读
- 命令
- 命令
- 故障排查