LabHub
学习 学习路径 课程

构建镜像

Dockerfile 不是配置文件,是构建脚本

在 LabHub 中继续学习

一句话总结

Dockerfile 指令分为两类:生成层的指令(RUN、COPY、ADD),以及把值写入镜像配置的指令(ENV、CMD、ENTRYPOINT、USER、LABEL、EXPOSE)。只要知道属于哪一类,就能提前避免大约一半的体积问题和安全问题。

概念图: 生成层的指令 · 把值写入镜像配置的指令 · 永久保留在镜像配置中 · 如果已经推送到镜像仓库,就必须将该令牌视为已经泄露并立即吊销。删除镜像无法撤回泄露。

为什么需要了解这一点

最常反复发生的事故是秘密泄露。使用 ENV NPM_TOKEN=... 写入令牌后,令牌会永久保留在镜像配置中

docker inspect -f '{{json .Config.Env}}' bad:v1
# "NPM_TOKEN=npm_9fA3xQ2LkD8vR1sT6yU0wZ4bN7mC5eJ"

即使通过 ARG 接收并使用,再执行 rm ~/.npmrc 删除也没有用。文件仍留在下层中,经典构建器还会把它保留在历史记录中。如果已经推送到镜像仓库,就必须将该令牌视为已经泄露并立即吊销。删除镜像无法撤回泄露。

工作原理

有些指令的实际行为与表面看起来不同。

ADD 不是 COPY 的超集。 ADD 会自动解压本地 tar,也能接收 URL。额外行为通常正是问题来源。若归档中包含 ../ 路径,文件可能写入意外位置;通过 URL 下载也没有校验和验证。默认应使用 COPY,只有确实需要自动解压本地 tar 时,ADD 才合理。

COPY --chownRUN chown -R 会造成两倍的体积差异。 如果 COPY . /app 写入 286MB,随后执行 RUN chown -R app:app /app 会在新层中重写全部文件,再增加 286MB。把所有者选项直接加到 COPY 上,只需一个层。

USER 必须使用数字 UID。 Kubernetes 的 runAsNonRoot 无法根据用户名判断它是否为 root,因此会拒绝 Pod。

Error: container has runAsNonRoot and image has non-numeric user (app),
cannot verify user is non-root

HEALTHCHECK 有两个常见误解:检查失败不会自动重启容器,只会把它标记为 unhealthy;而且 Kubernetes 完全忽略镜像中的 HEALTHCHECK,必须另外定义探针。

实际工作中的表现

评审清单只需五项。

  1. FROM 是否固定(latest 会让回滚目标消失)
  2. USER 是否为数字 UID
  3. CMD/ENTRYPOINT 是否为 JSON 数组(exec 形式)
  4. 秘密是否通过 ARG/ENV 进入镜像
  5. 最终阶段是否残留构建工具

常见反驳是“固定标签就收不到安全补丁”。更新可以由机器人提交 PR,再由 CI 验证并合并。区别在于补丁是自动流入,还是经过验证后流入

容易混淆的指令对

差异
COPY vs ADD ADD 可接收 URL 并自动解压 tar,因此更难预测。 默认使用 COPY
CMD vs ENTRYPOINT ENTRYPOINT 是要执行的程序,CMD 是默认参数;docker run 的参数会覆盖 CMD
ENV vs ARG ENV 在运行时仍存在,ARG 只在构建期间存在
RUN vs CMD RUN 在构建时执行,CMD 在运行时执行

ENTRYPOINTCMD 的组合最实用。

ENTRYPOINT ["python", "-m", "app"]
CMD ["--port", "8080"]
docker run myapp                    → python -m app --port 8080
docker run myapp --port 9000        → python -m app --port 9000

不要用 ARG 传递秘密。 它会原样留在 docker history 中。应使用构建秘密(RUN --mount=type=secret)。

Shell 形式与 exec 形式

CMD python app.py           # 셸 형식 — /bin/sh -c 로 감싸진다
CMD ["python", "app.py"]    # exec 형식 — 그대로 실행

应使用 exec 形式。Shell 形式会让 sh 成为 PID 1,无法把 SIGTERM 传递给应用。停止容器时,它会等待 10 秒后才强制终止。

但 exec 形式不会展开环境变量CMD ["echo", "$HOME"] 会原样输出 $HOME。需要变量时,应使用入口脚本,并在最后执行 exec "$@"

让构建可复现

FROM python:3.12-slim@sha256:abc123…    # 태그가 아니라 다이제스트로 고정

标签会移动。python:3.12-slim 昨天和今天可能不同,于是就会出现“昨天明明还能运行”的问题。生产镜像应固定到摘要。

依赖项也要固定。

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

--no-cache-dir 不把 pip 缓存留在层中,可减小镜像。使用 BuildKit 挂载缓存,既能加快构建,又能保持镜像精简。

经常遗漏的事项

下个练习将做什么

逐条添加指令、改变标签并进行构建,通过 inspect 确认每条指令写入了镜像的哪个字段,最后完成一个能通过检查清单的 Dockerfile。