7.2 写一个可运行的 Dockerfile
前置知识:镜像、容器与运行边界
预计时间:20 分钟
练习环境:Docker Engine;首次使用 BusyBox 时需要访问 registry
完成标志:能解释构建上下文、缓存、启动命令和非 root 用户,并构建一个最小镜像
你已经会运行别人提供的镜像。现在编写自己的 Dockerfile,把应用、依赖、用户和启动命令写成可审查的构建说明。
Dockerfile 描述怎样构建镜像。它不会复制你的整台电脑,也不会自动包含构建上下文之外的文件。
一个可运行的最小项目
先建一个隔离目录,所有构建上下文都留在里面:
image_workdir=$(mktemp -d)
image_tag="workshop-ch07-message:${RANDOM}-${RANDOM}"
mkdir -p "$image_workdir/workshop-image"
cd "$image_workdir/workshop-image"目录里放三个文件:
workshop-image/
├── Dockerfile
├── .dockerignore
└── message.txtmessage.txt:
portable workshopDockerfile:
FROM busybox:latest
WORKDIR /app
COPY --chown=65532:65532 message.txt .
USER 65532:65532
ENTRYPOINT ["cat"]
CMD ["/app/message.txt"].dockerignore:
.git
*.log
.envcd "$image_workdir/workshop-image"
docker build -t "$image_tag" .
docker run --rm "$image_tag"
docker run --rm --entrypoint id "$image_tag" -u预期先看到 portable workshop,再看到数字用户 65532。这里的 COPY --chown 和数字 UID 针对 Linux 容器;Windows 容器使用不同的用户与权限模型。数字 UID 不需要在镜像里有同名账号,应用若依赖 home 目录或用户数据库,再显式创建用户。
每条指令负责什么
| 指令 | 阶段 | 作用 |
|---|---|---|
FROM | 构建 | 选择基础镜像并开始构建阶段 |
WORKDIR | 构建与运行配置 | 设置后续指令和默认启动目录 |
COPY | 构建 | 从构建上下文复制文件;不能越过上下文读取任意主机路径 |
RUN | 构建 | 执行构建步骤并形成结果层 |
USER | 构建与运行配置 | 设置后续 RUN 的用户,以及默认容器进程的用户和组 |
EXPOSE | 元数据 | 说明应用监听端口,不发布端口 |
ENTRYPOINT | 运行配置 | 定义主要可执行程序 |
CMD | 运行配置 | 定义默认命令,或为 ENTRYPOINT 提供默认参数 |
优先使用 JSON exec form,例如 CMD ["java", "-jar", "app.jar"]。它不会额外套一层 shell,参数边界清楚,主进程也更容易收到 docker stop 发出的信号。若 shell form 确有需要,应理解 PID 1 和 exec 的影响。
构建上下文与 .dockerignore
docker build -t name . 末尾的 . 是构建上下文,不只是“找 Dockerfile 的目录”。COPY 的源路径从上下文解析,Docker 客户端也可能需要把上下文发送给远端 builder。
.dockerignore 可排除 .git、日志、测试产物、依赖目录和 .env。它能缩小上下文、减少缓存失效,也降低误把秘密送进 builder 的概率。不要把它当秘密管理系统;凭据应使用 BuildKit secret mount,而不是 ARG、ENV 或普通 COPY。
缓存的准确规则
latest 是可移动标签,重复构建可能取到不同基础镜像;需要复现确切环境时,记录并固定经过审核的 digest。Builder 按顺序检查能否复用缓存。COPY / ADD 的缓存校验考虑文件内容和相关元数据,但修改时间 mtime 本身不参与;一层失效后,后续层需要重建。把低频变化的依赖描述放在高频变化源码之前通常有利,但“看起来没更新”不等于缓存错误。
排查顺序:
docker build --progress=plain -t "$image_tag" .
docker image inspect "$image_tag"
docker history "$image_tag"先确认构建上下文、标签和实际运行的镜像。--no-cache 可用于诊断,却不是日常修复缓存的首选;它也不自动拉取更新后的基础镜像。
Java 项目的多阶段骨架
构建工具应出现在构建阶段,运行阶段只保留运行所需内容:
FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /src
COPY pom.xml .
RUN --mount=type=cache,target=/root/.m2 mvn -B dependency:go-offline
COPY src ./src
RUN --mount=type=cache,target=/root/.m2 mvn -B package
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=build /src/target/app.jar /app/app.jar
USER 10001:10001
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]这是一份 partial 骨架:需要支持 RUN --mount 的 BuildKit、完整 POM 和源码,以及实际产物名称。app.jar 还必须有正确的 Main-Class,并已打包或配置运行依赖;普通 mvn package 不自动保证 java -jar 可用。多阶段构建可排除 Maven 和源码,但镜像最终大小取决于基础镜像、依赖和产物,不能预先保证某个数字。发布时还应选择受支持版本,并用不可变 digest 或供应链策略控制基础镜像变化。
清理本讲资源
docker image rm "$image_tag"
cd
ls -ld -- "$image_workdir"
rm -r -- "$image_workdir"只删除本讲创建的标签和临时目录。不要用 docker system prune 作为教程清理命令,它可能影响其他项目的缓存和停止容器。指令细节以 Dockerfile reference 为准。
确认输出和 UID 符合预期后,单个镜像的构建与运行练习就完成了。接下来用 Compose 给一组服务划定共同的项目边界。