Dockerfile 不是配置文件,是构建脚本
一句话总结
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 --chown 与 RUN 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,必须另外定义探针。
实际工作中的表现
评审清单只需五项。
FROM是否固定(latest会让回滚目标消失)USER是否为数字 UIDCMD/ENTRYPOINT是否为 JSON 数组(exec 形式)- 秘密是否通过
ARG/ENV进入镜像 - 最终阶段是否残留构建工具
常见反驳是“固定标签就收不到安全补丁”。更新可以由机器人提交 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 在运行时执行 |
ENTRYPOINT 与 CMD 的组合最实用。
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 挂载缓存,既能加快构建,又能保持镜像精简。
经常遗漏的事项
- 把
USER放在最后——之后的RUN都会以该用户执行。需要设置文件权限的操作应放在它之前。 WORKDIR不存在时会自动创建——无需执行RUN mkdir。- 先编写
.dockerignore——同时减少上下文传输量和缓存失效。 - 顺序比层数更重要——现代系统中,层数本身通常不是问题。把不常变化的内容放在前面,效果远胜于减少层数。
下个练习将做什么
逐条添加指令、改变标签并进行构建,通过 inspect 确认每条指令写入了镜像的哪个字段,最后完成一个能通过检查清单的 Dockerfile。