1. 概述

前两篇已经完成了 Docker 的概念铺垫和运行层操作:知道镜像、容器、仓库之间的关系,也能在 Ubuntu 主机上安装 Docker Engine,并用 docker run 启动、查看、停止和清理容器。从这一篇开始,重点从“运行别人发布的镜像”转向“把自己的应用做成可交付的镜像”。

Dockerfile 是容器化交付的核心文件。它不是简单的安装脚本,而是一份描述镜像文件系统、运行用户、启动命令、构建上下文和默认配置的声明式构建说明。写得好的 Dockerfile 可以带来稳定的构建缓存、更小的镜像体积、更少的安全暴露面和更清晰的发布流程;写得差的 Dockerfile 则会造成镜像臃肿、缓存频繁失效、密钥泄露、构建缓慢、生产环境难以复现。

本文的目标是建立一套企业内部可以直接复用的 Dockerfile 编写方法:

  1. 理解 Dockerfile 指令、构建上下文、镜像层和缓存失效的关系。
  2. 为一个最小 Python Web 应用编写可构建、可运行、可验证的 Dockerfile。
  3. 使用 .dockerignore 控制构建上下文,避免把无关文件打进镜像。
  4. 使用多阶段构建把构建依赖和运行时依赖分离。
  5. 使用 BuildKit 的缓存和 secret 能力优化构建速度与安全性。
  6. 建立镜像构建、标记、扫描、运行验证和清理的基本工作流。

本文命令以 Docker Engine + Buildx 插件环境为基线。具体版本不写死,以实际 docker versiondocker buildx version 输出为准。

2. 理论基础

2.1 Dockerfile 到镜像的构建链路

一次 docker build 可以简化为下面几个阶段:

这里最容易被忽略的是“构建上下文”。执行下面命令时,最后的 . 不是装饰,而是告诉 Docker 把当前目录作为 build context 发送给 builder:

bash
docker build -t myapp:dev .

Dockerfile 中的 COPYADD 默认只能访问构建上下文里的文件。如果上下文目录过大,或者里面包含 .git、日志、测试报告、缓存、私钥和生产配置,构建会变慢,也会增加泄露风险。因此 .dockerignore 是 Dockerfile 的配套文件,不是可有可无的优化项。

2.2 镜像层与缓存失效

Dockerfile 中多数指令都会形成镜像层或影响镜像元数据。BuildKit 会尽量复用已有缓存,但只要某一层的输入发生变化,这一层以及后续依赖它的层就需要重新执行。

一个常见的低效写法是:

dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["python", "app.py"]

只要业务代码任意文件变化,COPY . . 这一层就会失效,后面的 pip install 也会重新执行。更合理的写法是先复制依赖清单,再安装依赖,最后复制业务代码:

dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]

这样修改 app.py 时,依赖安装层仍然可以复用缓存。Dockerfile 的顺序不是审美问题,而是构建速度和可复现性的关键。

2.3 常用 Dockerfile 指令

指令 作用 实战建议
FROM 指定基础镜像,开始一个构建阶段 生产镜像尽量固定主版本或小版本,不要长期依赖 latest
WORKDIR 设置后续命令工作目录 优先使用,不要反复 cd
COPY 从上下文或构建阶段复制文件 复制本地文件优先用 COPY
ADD 复制文件,额外支持 URL 和自动解压本地 tar 只有明确需要这些语义时再用
RUN 构建期间执行命令 合并相关包安装和清理步骤,避免留下缓存
ENV 设置镜像或容器环境变量 适合非敏感默认值,不要放密码
ARG 设置构建参数 只在构建期间使用,仍不适合传密钥
EXPOSE 声明容器服务端口 只是元数据,不会自动发布端口
USER 设置运行用户 生产镜像尽量不用 root 运行主进程
ENTRYPOINT 定义固定入口程序 适合封装命令型镜像
CMD 定义默认参数或默认启动命令 可被 docker run 后面的命令覆盖
HEALTHCHECK 定义容器健康检查 单机 Docker 可用,编排平台也可能有自己的健康检查机制
LABEL 写入镜像元数据 可记录源码地址、维护者、构建时间、版本等

RUNCMDENTRYPOINT 都能写 shell form 或 exec form。生产镜像的启动命令优先使用 exec form,例如:

dockerfile
CMD ["python", "app.py"]

这样主进程能正确接收信号,容器停止时更容易优雅退出。

2.4 基础镜像选择

基础镜像决定了镜像大小、包管理器、系统库、默认用户、安全维护和排障体验。

类型 示例 优点 注意事项
完整发行版 ubuntu:24.04debian:bookworm 工具完整,排障方便 体积较大,攻击面更大
slim 镜像 python:3.12-slimnode:22-slim 体积和兼容性较平衡 可能缺少编译依赖,需要显式安装
Alpine nginx:alpinealpine:3 很小,适合简单服务 使用 musl libc,部分依赖兼容性要验证
distroless gcr.io/distroless/* 攻击面小,适合生产运行 缺少 shell,排障方式不同
scratch scratch 空基础镜像,极小 只适合静态二进制等场景

企业内部建议为每种技术栈维护标准基础镜像,例如 python-runtimenode-runtimejava-runtime,把 CA 证书、时区、非 root 用户、基础安全配置和内网源配置统一沉淀进去。业务团队不要各自从互联网随意复制基础镜像。

3. 环境规划

本文用一个最小 Python Flask 应用演示镜像构建流程。验证环境假设如下:

项目 说明
操作系统 Ubuntu 24.04 LTS 或 Ubuntu 22.04 LTS
Docker 组件 Docker Engine、Docker CLI、Buildx 插件
应用语言 Python 3.12
服务端口 容器内 8000,宿主机示例映射到 18080
构建方式 docker builddocker buildx build
网络要求 能访问 Docker Hub 或企业镜像代理

创建实验目录:

bash
mkdir -p ~/dockerfile-lab
cd ~/dockerfile-lab

准备三个文件:

bash
cat > requirements.txt <<'EOF'
flask==3.0.3
gunicorn==22.0.0
EOF

cat > app.py <<'EOF'
from flask import Flask, jsonify

app = Flask(__name__)

@app.get("/")
def index():
    return jsonify(status="ok", service="dockerfile-lab")

@app.get("/health")
def health():
    return jsonify(status="healthy")
EOF

cat > .dockerignore <<'EOF'
.git
.venv
__pycache__
*.pyc
*.log
.env
dist
build
node_modules
coverage
.pytest_cache
EOF

.dockerignore 中特意加入 .env,因为环境变量文件经常包含数据库密码、Token、API Key。构建镜像时不应该把这类敏感文件复制进镜像。

4. 实战操作

Dockerfile 构建优化模式

这个优化视图把后续实战中的优化动作归纳为四类目标:减少镜像体积、提高缓存命中、降低运行权限、避免密钥进入镜像层。写 Dockerfile 时不要只追求“能 build 成功”,还要考虑产物是否可复现、可审计、可维护。

4.1 编写第一个可运行 Dockerfile

创建 Dockerfile:

bash
cat > Dockerfile <<'EOF'
# syntax=docker/dockerfile:1
FROM python:3.12-slim

WORKDIR /app

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

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

COPY app.py .

EXPOSE 8000

CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]
EOF

构建镜像:

bash
docker build -t dockerfile-lab:dev .

运行验证:

bash
docker run --rm -d \
  --name dockerfile-lab \
  -p 18080:8000 \
  dockerfile-lab:dev

curl http://127.0.0.1:18080/
curl http://127.0.0.1:18080/health
docker logs dockerfile-lab
docker rm -f dockerfile-lab

如果 curl 返回 JSON,说明镜像构建、容器启动、端口监听和应用路由都正常。不要只看 docker ps,容器在运行并不代表应用逻辑可用。

4.2 查看镜像层和元数据

查看本地镜像:

bash
docker images dockerfile-lab

查看镜像构建历史:

bash
docker history dockerfile-lab:dev

查看镜像配置:

bash
docker inspect dockerfile-lab:dev

关注几个关键字段:

  • Config.Cmd:默认启动命令。
  • Config.Env:镜像内默认环境变量。
  • Config.WorkingDir:容器启动后的工作目录。
  • Config.ExposedPorts:Dockerfile 中声明的端口。
  • RootFS.Layers:镜像层摘要。

EXPOSE 8000 只是声明端口,不等于发布端口。真正把容器端口映射到宿主机的是 docker run -p 18080:8000

4.3 给镜像增加标签

镜像标签应该服务于发布和回滚,而不是只用 latest

bash
docker tag dockerfile-lab:dev dockerfile-lab:2026-05-20
docker tag dockerfile-lab:dev dockerfile-lab:git-$(git rev-parse --short HEAD 2>/dev/null || echo local)
docker images dockerfile-lab

企业 CI/CD 中常见标签策略:

标签 用途
dev 本地或测试构建
staging-<build_id> 测试环境发布
prod-<date>-<build_id> 生产发布
<git_sha> 精确追溯源码版本
latest 可以保留,但不要作为生产唯一依据

生产回滚依赖明确标签或 digest。只记录 latest 会让“当时到底发布了哪个镜像”变得不可追溯。

4.4 优化 apt 和系统依赖安装

如果应用需要系统包,apt-get updateapt-get install 应放在同一个 RUN 指令里,并清理 apt 列表:

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

不要写成:

dockerfile
RUN apt-get update
RUN apt-get install -y curl

分开写可能导致缓存复用旧索引,也会让镜像层留下更多无用内容。安装系统包时尽量加 --no-install-recommends,避免把不需要的推荐包一并装进镜像。

4.5 使用非 root 用户运行应用

默认情况下,很多基础镜像内的进程以 root 启动。生产镜像建议创建专用用户:

dockerfile
# syntax=docker/dockerfile:1
FROM python:3.12-slim

WORKDIR /app

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

RUN groupadd --system app && useradd --system --gid app --home /app app

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

COPY app.py .
RUN chown -R app:app /app

USER app
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]

重新构建并验证运行用户:

bash
docker build -t dockerfile-lab:nonroot .
docker run --rm dockerfile-lab:nonroot id

如果镜像启动命令被覆盖为 id 后显示的是普通用户 UID/GID,就说明 USER app 生效。注意:非 root 用户可能没有权限写入某些目录,因此日志、缓存、上传文件目录要提前规划权限,不能等生产容器启动失败后再临时修。

4.6 使用多阶段构建

多阶段构建适合把构建依赖留在 builder 阶段,只把运行所需产物复制到最终镜像。下面以 Python wheelhouse 为例:

dockerfile
# syntax=docker/dockerfile:1
FROM python:3.12-slim AS builder

WORKDIR /build
COPY requirements.txt .
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt

FROM python:3.12-slim AS runtime

WORKDIR /app

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

RUN groupadd --system app && useradd --system --gid app --home /app app

COPY --from=builder /wheels /wheels
RUN pip install --no-cache-dir /wheels/* \
    && rm -rf /wheels

COPY app.py .
RUN chown -R app:app /app

USER app
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]

构建指定阶段可以用于排障:

bash
docker build --target builder -t dockerfile-lab:builder .
docker build --target runtime -t dockerfile-lab:runtime .

多阶段构建的价值不只是减小镜像体积,更重要的是把“编译环境”和“运行环境”隔离开。生产镜像中没有编译器、源码生成缓存和测试工具,攻击面更小,也更符合最小化原则。

4.7 使用 BuildKit 缓存挂载

BuildKit 支持在 RUN 中使用缓存挂载,让依赖下载缓存不进入最终镜像层,但能在后续构建复用。例如 pip 缓存:

dockerfile
# syntax=docker/dockerfile:1
FROM python:3.12-slim

WORKDIR /app
COPY requirements.txt .

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

COPY app.py .
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]

构建:

bash
docker build -t dockerfile-lab:cache .

缓存挂载适合依赖下载频繁、CI 构建时间敏感的场景。它不同于把缓存目录保留在镜像里:缓存供 builder 使用,不应该成为运行镜像的一部分。

4.8 安全传递构建密钥

不要用 ARG TOKEN=...ENV TOKEN=... 把密钥传入镜像构建。构建参数和环境变量很容易进入构建历史、镜像元数据或日志。BuildKit 提供 secret mount:

bash
printf '%s' "$PIP_TOKEN" > /tmp/pip_token

docker build \
  --secret id=pip_token,src=/tmp/pip_token \
  -t dockerfile-lab:secret .

rm -f /tmp/pip_token

Dockerfile 中使用:

dockerfile
RUN --mount=type=secret,id=pip_token \
    PIP_TOKEN="$(cat /run/secrets/pip_token)" \
    && pip install --extra-index-url "https://token:${PIP_TOKEN}@pypi.example.com/simple" -r requirements.txt

这类写法的重点是:密钥只在该 RUN 步骤中以临时文件形式可见,不会作为镜像层内容保存。实际企业环境中,更推荐通过 CI/CD 的 secret store、私有包仓库和短期令牌配合使用。

5. 配置详解

5.1 COPYADD 的选择

优先使用 COPY,因为语义单一、可预测。ADD 只有在需要自动解压本地 tar 包,或明确使用 Docker 支持的额外能力时再考虑。

dockerfile
COPY app.py /app/app.py
COPY requirements.txt /app/requirements.txt

不建议用远程 URL ADD 直接拉文件作为生产依赖来源。更稳妥的做法是在构建前通过包管理器、制品仓库或 CI 步骤准备好依赖,并校验版本与摘要。

5.2 CMDENTRYPOINT

普通 Web 应用通常用 CMD

dockerfile
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]

命令型工具镜像通常用 ENTRYPOINT 固定入口,再用 CMD 提供默认参数:

dockerfile
ENTRYPOINT ["python", "-m", "mytool"]
CMD ["--help"]

运行时可以覆盖默认参数:

bash
docker run --rm mytool:latest scan /data

如果入口脚本需要处理信号、生成配置或等待依赖,脚本里必须正确使用 exec "$@",避免 shell 成为无法转发信号的中间进程。

5.3 ARGENV

ARG 用在构建期间:

dockerfile
ARG APP_VERSION=dev
LABEL org.opencontainers.image.version=$APP_VERSION

构建时传入:

bash
docker build --build-arg APP_VERSION=1.2.3 -t myapp:1.2.3 .

ENV 会进入镜像运行环境:

dockerfile
ENV APP_ENV=production

两者都不适合保存敏感信息。密码、Token、证书私钥应该通过运行时 secret、挂载文件、Kubernetes Secret、Vault 或 CI/CD secret 机制管理。

5.4 HEALTHCHECK 的取舍

可以在镜像内定义健康检查:

dockerfile
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=2)"

验证:

bash
docker run -d --name dockerfile-lab -p 18080:8000 dockerfile-lab:runtime
docker inspect --format '{{json .State.Health}}' dockerfile-lab
docker rm -f dockerfile-lab

是否把 HEALTHCHECK 写进 Dockerfile,要看平台策略。如果运行在 Kubernetes,通常会在 Deployment 中配置 livenessProbe/readinessProbe;如果大量使用单机 Docker 或 Compose,把基础健康检查写进镜像也有价值。不要让镜像健康检查依赖外部数据库、第三方 API 或复杂业务链路,否则会制造误报。

6. 验证与测试

6.1 构建验证清单

每次提交 Dockerfile 后,至少执行下面的验证:

bash
docker build -t dockerfile-lab:test .
docker run --rm -d --name dockerfile-lab-test -p 18080:8000 dockerfile-lab:test
curl -fsS http://127.0.0.1:18080/health
docker logs --tail=50 dockerfile-lab-test
docker rm -f dockerfile-lab-test

如果使用非 root 用户,再验证:

bash
docker run --rm --entrypoint id dockerfile-lab:test

如果镜像包含命令行工具,再验证帮助信息和退出码:

bash
docker run --rm mytool:latest --help
echo $?

6.2 镜像内容检查

检查镜像大小:

bash
docker images dockerfile-lab

检查是否把敏感文件打入镜像:

bash
docker run --rm --entrypoint sh dockerfile-lab:test -c 'find /app -maxdepth 3 -type f | sort'

如果镜像没有 shell,可以用临时调试镜像或在 CI 中对构建上下文进行检查。不要为了排障方便,在生产运行镜像里长期保留调试工具。

6.3 多平台构建验证

如果需要同时支持 x86_64 和 ARM64,可以使用 Buildx:

bash
docker buildx ls
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t registry.example.com/team/dockerfile-lab:1.0.0 \
  --push .

单机 Docker Engine 环境中,多平台构建可能需要额外 builder、QEMU 或原生多架构节点。跨架构构建不要只看构建是否成功,还要在目标架构上做运行验证,尤其是包含系统库、C 扩展、GPU、网络工具或专有二进制依赖的镜像。

6.4 镜像扫描和元数据

建议为生产镜像补充 OCI Label:

dockerfile
ARG APP_VERSION=dev
ARG VCS_REF=unknown

LABEL org.opencontainers.image.title="dockerfile-lab" \
      org.opencontainers.image.version=$APP_VERSION \
      org.opencontainers.image.revision=$VCS_REF \
      org.opencontainers.image.source="https://git.example.com/team/dockerfile-lab"

构建:

bash
docker build \
  --build-arg APP_VERSION=1.0.0 \
  --build-arg VCS_REF="$(git rev-parse --short HEAD 2>/dev/null || echo local)" \
  -t dockerfile-lab:1.0.0 .

镜像扫描可以使用企业已有工具,例如 Trivy、Grype、Docker Scout 或云厂商镜像仓库扫描能力。扫描结果要进入发布门禁,但不要机械地追求零漏洞。更合理的做法是结合基础镜像维护状态、漏洞可利用路径、运行用户、暴露端口和补丁可用性制定处理策略。

7. 故障排查

Dockerfile 构建排障清单

这张清单按失败类型给出第一证据入口:路径问题先看构建上下文,性能问题先看缓存顺序,安全问题先看镜像层和密钥处理,CI 问题则要对比本地和流水线环境差异。

7.1 COPY failed: file not found

常见原因:

  • 文件不在 build context 中。
  • .dockerignore 把文件排除了。
  • 执行 docker build 的目录不对。
  • Dockerfile 路径和上下文路径混淆。

排查:

bash
pwd
ls -lah
cat .dockerignore
docker build -f Dockerfile -t dockerfile-lab:test .

如果 Dockerfile 不在当前目录,可以显式指定:

bash
docker build -f docker/Dockerfile -t myapp:test .

注意最后的 . 仍然是上下文根目录。

7.2 构建每次都很慢

重点检查缓存顺序:

  • 是否先 COPY . . 再安装依赖。
  • 依赖锁文件是否频繁变化。
  • 是否把 .git、日志、构建产物放进上下文。
  • 是否每次都使用 --no-cache
  • CI builder 是否没有持久缓存。

可以用普通构建对比:

bash
docker build -t dockerfile-lab:test .
docker build -t dockerfile-lab:test .

第二次构建如果仍然大量重新执行,说明 Dockerfile 层顺序或上下文控制需要优化。

7.3 镜像体积过大

常见原因:

  • 使用了过大的基础镜像。
  • 编译工具留在最终镜像。
  • 包管理器缓存没有清理。
  • 把测试数据、源码历史、日志、依赖缓存复制进镜像。
  • 没有使用多阶段构建。

排查:

bash
docker images myapp
docker history myapp:latest

优化方向:

  • 使用 slim 或企业标准 runtime 镜像。
  • 用多阶段构建移除 builder 依赖。
  • 补充 .dockerignore
  • 合并包安装和清理命令。
  • 删除运行不需要的临时文件。

7.4 容器启动后立刻退出

先看退出状态和日志:

bash
docker ps -a
docker logs <container>
docker inspect --format 'ExitCode={{.State.ExitCode}} Error={{.State.Error}}' <container>

常见原因:

  • CMD 路径错误。
  • 工作目录不对。
  • 依赖没有安装进最终阶段。
  • 应用监听了 127.0.0.1 而不是 0.0.0.0
  • 非 root 用户没有文件写权限。
  • 入口脚本没有执行权限或 shebang 错误。

调试时可以覆盖入口:

bash
docker run --rm -it --entrypoint sh myapp:latest

如果镜像没有 shell,需要在 builder 阶段或调试版本中排查,不建议为了生产排障长期保留 shell。

7.5 构建时密钥泄露

危险写法:

dockerfile
ARG TOKEN
RUN git clone https://token:${TOKEN}@git.example.com/private/repo.git

风险:

  • 构建日志可能出现密钥。
  • 镜像历史可能留下命令片段。
  • 中间层或缓存可能暴露敏感信息。

修复方向:

  • 使用 BuildKit --secret
  • 在 CI/CD 平台使用短期令牌。
  • 将私有依赖发布到内网制品仓库。
  • 对已推送镜像执行撤回、轮换密钥和访问日志审计。

7.6 本地能跑,CI 构建失败

常见差异:

  • 本地存在未提交文件,CI 上没有。
  • 本地 Docker 缓存掩盖了依赖问题。
  • CI 没有访问 Docker Hub 或私有仓库权限。
  • CI 的 CPU 架构与本地不同。
  • CI 没有开启 BuildKit 或 builder 配置不同。

排查时先做干净构建:

bash
git status --short
docker build --no-cache -t dockerfile-lab:clean .

再把 CI 中的构建命令、上下文路径、环境变量、secret 注入方式和 registry 权限逐项对齐。

8. 运维建议

8.1 把 Dockerfile 纳入代码评审

Dockerfile 直接决定运行环境和安全边界,应该像应用代码一样评审。评审重点:

  • 基础镜像是否来自可信来源。
  • 是否使用非 root 用户。
  • 是否把密钥、.env、证书私钥、源码历史打进镜像。
  • 构建缓存顺序是否合理。
  • 是否有多阶段构建或其他瘦身措施。
  • 启动命令是否能正确接收信号。
  • 端口、环境变量、健康检查是否清晰。

8.2 建立企业标准模板

不要让每个项目从零写 Dockerfile。可以按语言维护模板:

  • Python API 服务模板。
  • Node.js 前端构建 + Nginx 运行模板。
  • Java Spring Boot 运行模板。
  • Go 静态二进制 + distroless/scratch 模板。
  • Nginx 静态站点模板。

模板不应该过度复杂,但要包含 .dockerignore、非 root 用户、依赖缓存、多阶段构建、标签元数据和验证命令。

8.3 固定构建产物,而不是固定构建机器

容器化的目标不是让某台机器变得特殊,而是让构建产物可复现、可分发、可回滚。建议:

  • 每次发布记录镜像 tag 和 digest。
  • CI 中使用一致的构建命令。
  • 生产部署优先引用 digest 或不可变 tag。
  • 镜像推送到企业 registry 后再部署。
  • 保留关键版本的 SBOM、扫描报告和构建日志。

8.4 控制基础镜像更新节奏

基础镜像既不能几年不动,也不能每天无验证自动漂移。推荐做法:

  1. 企业维护标准基础镜像。
  2. 定期重建业务镜像,吸收基础镜像安全更新。
  3. 对关键服务先在测试环境运行冒烟测试。
  4. 发布前保留旧镜像,确保可回滚。
  5. 对不再维护的基础镜像建立替换计划。

8.5 明确构建与运行边界

Dockerfile 里不要塞进所有事情。构建阶段负责把应用和运行依赖做成镜像;运行配置由 Compose、Kubernetes、Nomad、systemd 或发布平台负责。下面这些内容一般不应该固化进镜像:

  • 生产数据库密码。
  • 生产 API Token。
  • 环境专属域名和 IP。
  • 临时调试脚本。
  • 大量测试数据。
  • 只能在某台机器上使用的本地路径。

镜像应该尽量环境无关,运行时通过环境变量、配置挂载、secret 和编排平台注入差异。

9. Dockerfile 评审清单

Dockerfile 是应用交付物的一部分,应该进入代码评审。评审重点不是格式好不好看,而是镜像是否可复现、可维护、可运行、可扫描、可回滚。

第一,看基础镜像。基础镜像是否来自可信来源,是否有明确版本,是否仍被维护,是否适合生产运行。FROM ubuntu:latestFROM node:latest 对实验很方便,但生产不可追踪。更好的做法是固定到主版本或具体小版本,并建立基础镜像更新节奏。

第二,看依赖安装。包管理器缓存是否清理,依赖是否固定版本,是否把构建工具留在运行镜像里。对于 Go、Java、Node.js、Python 等项目,多阶段构建通常能把构建依赖和运行依赖分开,减少镜像体积和攻击面。

第三,看缓存顺序。频繁变化的业务代码不应该放在依赖安装之前,否则每次代码变更都会让依赖层缓存失效。常见做法是先复制依赖描述文件,例如 package.jsonrequirements.txtgo.modpom.xml,安装依赖后再复制业务代码。

第四,看运行用户。镜像默认 root 运行会扩大风险。能以非 root 用户运行的应用,应创建专用用户,并配合文件权限、只读文件系统、最小 capability 和运行时安全策略。

第五,看密钥和配置。不要在 Dockerfile 中写入 token、密码、证书私钥、私有仓库凭据或生产配置。即使后续删除文件,密钥也可能留在镜像历史层中。构建期需要访问私有依赖时,应使用 BuildKit secret、CI 凭据注入或私有包代理。

第六,看启动方式。CMDENTRYPOINT 要表达清楚默认启动行为。容器内进程应以前台方式运行,日志输出到 stdout/stderr,避免依赖 systemd 或后台守护进程模型。一个容器里跑多个强耦合进程时,要说明为什么不能拆分。

10. 镜像标签和版本策略

镜像标签决定发布、回滚和排障能否追踪。latest 不是版本,它只是一个可移动指针。生产发布至少应该保留不可变标签,例如应用版本、Git commit、构建号或日期。

一个实用策略是同时打三类标签:

标签 用途 示例
版本标签 对应产品版本 app:1.4.2
提交标签 精确追踪源码 app:git-a1b2c3d
环境或渠道标签 表达发布渠道 app:stagingapp:prod

真正部署时,建议使用不可变标签或 digest,而不是只使用 prod 这种可移动标签。可移动标签适合人类识别当前渠道,不适合作为唯一回滚依据。如果生产事故需要回滚,团队要能明确知道上一版镜像是什么、由哪个 commit 构建、什么时候推送、是否通过扫描和测试。

镜像仓库也要设置保留策略。构建系统长期推送镜像不清理,会让 registry 存储持续增长;清理太激进,又可能删除仍需回滚的版本。比较稳妥的方式是保留近期构建、保留正式发布版本、保留当前生产和上一生产版本,其他临时构建按时间清理。

11. 构建上下文和 .dockerignore

很多 Dockerfile 构建慢、镜像意外变大,不是指令本身问题,而是构建上下文太大。执行 docker build . 时,当前目录会作为 build context 发送给构建器。如果目录里包含 .git、日志、测试数据、node_modules、虚拟环境、临时文件或大二进制文件,构建会变慢,也可能把不该进入镜像的内容复制进去。

.dockerignore 应该像 .gitignore 一样认真维护。常见排除项包括:

text
.git
.env
*.log
node_modules
dist
build
.venv
__pycache__
coverage
tmp

不要盲目排除所有构建产物。有些项目需要先在 CI 中构建,再把产物复制进镜像;有些项目则应该在 Dockerfile 内部完成构建。关键是明确构建边界:哪些文件由宿主机准备,哪些文件由 Dockerfile 构建,哪些文件绝不能进入镜像。

12. 镜像安全扫描与 SBOM

镜像构建完成后,至少要做基础扫描。扫描能发现已知 CVE、过期系统包、危险配置、敏感文件和基础镜像风险。扫描结果不是绝对真理,但它能让团队知道当前镜像的风险面。

安全扫描要结合业务判断。比如某个系统包存在漏洞,但应用运行时并不使用该组件,风险可能可接受;另一个漏洞分数不高,但暴露在互联网入口,可能需要优先修复。不要只看漏洞数量,也不要把扫描当成形式。关键是有分级、例外、修复和复验流程。

SBOM 可以记录镜像中包含哪些包、库和版本。对于企业交付、供应链安全和合规审计,SBOM 越来越重要。即使当前团队不做完整 SBOM,也至少要保留镜像构建记录、基础镜像版本、依赖锁文件和扫描结果。

13. CI 中构建镜像的建议

生产镜像不应依赖个人电脑构建。CI 构建能保证构建环境稳定、过程可追踪、产物可复现。一个基础流水线可以包含以下步骤:拉取代码、安装依赖或准备缓存、运行测试、构建镜像、扫描镜像、打标签、推送仓库、生成构建记录。

CI 构建时要注意缓存和密钥。缓存能加快构建,但不能把私有依赖凭据写进镜像层。密钥应通过 CI secret 或 BuildKit secret 注入,构建结束后不能留在镜像历史中。多平台构建时,要确认目标架构,例如 amd64、arm64,避免在 ARM 开发机上构建出生产服务器无法运行的镜像。

CI 构建还要输出元数据。至少包括 Git commit、构建时间、镜像标签、基础镜像、构建流水线链接和扫描结果。后续出现事故时,团队可以从运行容器反查镜像,再从镜像反查源码和构建过程。

14. Dockerfile 与运行时配置的边界

一个常见错误是把环境差异写进 Dockerfile。比如在 Dockerfile 里写死生产数据库地址、生产 API 地址、日志路径、证书路径或业务开关。这会导致镜像只能用于一个环境,也让测试和生产镜像不一致。

更好的方式是:Dockerfile 只定义应用如何被打包和默认如何启动;环境差异通过 Compose、Kubernetes、systemd、CI/CD 或运维平台传入。配置文件可以只读挂载,敏感信息通过 secret 管理,数据通过卷或外部服务保存。

这个边界会影响后续所有环节。Dockerfile 越干净,镜像越容易在开发、测试、预生产和生产之间复用;运行时配置越明确,排障时越容易判断问题来自镜像、配置、网络、数据还是宿主机。

15. 上线前镜像验收表

类别 验收项
构建 Dockerfile 可在 CI 中稳定构建
标签 镜像有不可变版本标签和源码追踪信息
体积 镜像体积符合预期,未包含无关构建缓存
权限 默认非 root 或有明确例外说明
密钥 镜像层和历史中不包含密钥
启动 容器前台启动,日志输出到 stdout/stderr
健康 有健康检查或外部探测策略
扫描 已完成漏洞扫描并处理高风险项
回滚 上一版本镜像可用,数据兼容已确认
文档 运行参数、端口、卷、环境变量说明完整

这张表可以作为 Dockerfile 代码评审和发布门禁的基础。它不要求团队一次性做到平台级治理,但能避免最常见的镜像构建问题进入生产。

16. 三个典型 Dockerfile 反模式

第一个反模式是“能运行就行”的巨型镜像。Dockerfile 从完整发行版开始,安装编译器、调试工具、包管理缓存、测试文件和源码历史,最后直接运行应用。这样的镜像构建慢、传输慢、漏洞多,排查时也很难判断哪些文件真正被运行时需要。改进方式是使用多阶段构建,把构建依赖留在 builder 阶段,只把运行产物复制到运行镜像。

第二个反模式是把配置和密钥固化进镜像。比如在 Dockerfile 中复制 .env、写入数据库密码、复制生产证书、设置生产 API 地址。这样镜像一旦推送到仓库,泄露范围就变大,而且同一个镜像无法复用于测试和生产。改进方式是用环境变量、secret、只读配置挂载或编排平台注入运行时差异。

第三个反模式是没有版本和验证。构建出来的镜像只有 latest,没有源码 commit,没有构建记录,没有扫描结果,也没有运行验证。发布后如果出现问题,只能靠猜测回滚。改进方式是每次构建输出不可变标签、记录 digest、运行最小冒烟测试,并把构建日志和扫描结果关联到发布记录。

17. 镜像问题的排查路径

镜像问题通常表现为三类:构建失败、启动失败、运行后行为不符合预期。构建失败先看 build context、Dockerfile 指令、网络、包源和缓存;启动失败先看入口命令、文件权限、运行用户、环境变量和依赖服务;运行异常再看配置、端口、卷、网络和应用日志。

排查时不要马上修改 Dockerfile。先用 docker build --progress=plain 看完整构建输出,用 docker history 看镜像层,用 docker inspect 看默认命令、环境变量和工作目录,用 docker run --rm -it --entrypoint sh 进入镜像检查文件。确认问题发生在构建期还是运行期,再决定改 Dockerfile、改运行参数或改应用代码。

如果本地构建成功、CI 构建失败,要重点比较构建上下文、平台架构、网络代理、私有依赖权限、BuildKit 设置和基础镜像访问。不要把 CI 失败简单归因于“流水线不稳定”。镜像构建的目标就是让这些差异可见、可记录、可修复。

18. 从 Dockerfile 到发布标准

当团队有多个服务都开始容器化时,就需要把 Dockerfile 经验沉淀成发布标准。标准可以很简单:每个服务必须有 Dockerfile、.dockerignore、镜像标签规则、构建命令、运行示例、端口说明、卷说明、环境变量说明、健康检查和回滚说明。

标准不应该变成僵硬模板。不同语言栈有不同最佳实践,Python、Node.js、Java、Go、Nginx 静态站点的 Dockerfile 不应完全一样。但它们应该共享相同底线:不泄露密钥、不使用不可追踪标签、不以 root 作为默认习惯、不把数据写进镜像、不跳过运行验证。

这套标准会直接影响后续 Compose 和平台化。Compose 文件引用的是镜像,Kubernetes 部署的也是镜像。如果镜像本身质量差,后面的编排系统只会把问题放大。镜像构建是容器化交付链路的质量入口。

19. 一个镜像验收案例

假设团队要交付一个 Python API 服务。验收时不能只看 docker build 成功。首先要确认基础镜像版本,例如 python:3.12-slim 是否符合团队标准。其次检查 .dockerignore 是否排除了 .git.env、测试缓存和本地虚拟环境。然后看 Dockerfile 是否先复制依赖文件、安装依赖,再复制业务代码,以保证缓存命中。

构建完成后,要运行容器并验证健康接口、日志输出、非 root 用户、端口暴露和环境变量注入。还要用 docker history 检查是否有密钥痕迹,用扫描工具检查高危漏洞,用镜像标签关联 Git commit。最后,把运行参数写入 Compose 或部署平台,而不是只保留一条手工 docker run

这个案例说明,镜像验收是构建、运行、安全和发布四件事的组合。任何一项缺失,都可能在后续 Compose、Kubernetes 或生产部署阶段变成故障。

20. 学习 Dockerfile 的练习顺序

建议按三个练习推进。第一,用 Nginx 或 Python 写一个最小镜像,理解 FROMCOPYCMD 和构建上下文。第二,把一个真实应用改成多阶段构建,比较优化前后的镜像体积、构建时间和运行用户。第三,把构建放进 CI,输出镜像标签、扫描结果和运行验证记录。

这三个练习分别对应“能构建”“构建得好”“构建可交付”。不要跳过第一步直接追求复杂模板,也不要停留在第一步长期使用粗糙镜像。Dockerfile 的质量会传递到所有运行环境,越早建立评审和验收习惯,后续容器平台越稳定。

如果团队只能先做一件事,就先把 Dockerfile 评审纳入合并流程。每次合并前检查基础镜像、构建上下文、密钥、运行用户、标签和启动命令。这个动作成本不高,但能挡住大量后续发布和安全问题。

镜像一旦进入仓库,就会被测试、发布和回滚流程反复使用;越早拦住镜像质量问题,后续环境越稳定。

这也是为什么 Dockerfile 应该和应用代码一起维护、一起评审、一起发布,而不是由运维在上线前临时补写。

镜像质量越稳定,后续发布链路越可控。

这也是镜像治理和发布审计的起点与长期基线。

21. 总结

本文完成了 Docker 容器化实战系列的第三步:从 Dockerfile 生成自己的应用镜像。核心结论如下:

  1. Dockerfile 是镜像交付规范,不是随手写的安装脚本。
  2. 构建上下文必须用 .dockerignore 控制,避免泄露和低效构建。
  3. Dockerfile 指令顺序会影响缓存复用,依赖清单应先于业务代码复制。
  4. 生产镜像应优先使用非 root 用户、多阶段构建和明确标签。
  5. BuildKit 的 cache mount 和 secret mount 能同时改善构建速度与安全性。
  6. 镜像验证不能只看构建成功,还要运行容器、访问健康检查、查看日志和检查镜像内容。

下一篇会进入容器网络配置。到那一步,重点会从“构建单个应用镜像”转向“让容器接入宿主机、其他容器、局域网和外部服务”,包括自定义 bridge、端口发布、DNS、host 网络和网络排障。

参考资料