Docker核心技术之Dockerfile指令详解及综合案例
作者:IvanCodes
在前面的章节中,我们学会了如何拉取和运行他人构建好的镜像。但要真正掌握 Docker,我们必须学会创建属于自己的镜像。Dockerfile 就是实现这一目标的核心工具。它就像一张自动化的“安装说明书”或“构建蓝图”,让镜像的创建过程变得透明、可重复且易于版本控制
思维导图


一、什么是 Dockerfile?
Dockerfile 是一个包含一系列指令的文本文件。每一条指令都对应 Docker 镜像中的一个层。当我们执行 docker build 命令时,Docker 会逐一执行 Dockerfile 中的指令,最终生成一个完整的、可运行的自定义镜像。
核心构建命令:
docker build -t <image_name>:<tag> .
-t: 指定新镜像的名称和标签。
.: 指定构建上下文的路径,通常是包含 Dockerfile 的当前目录。构建上下文中的所有文件都会被发送到 Docker 守护进程,以便在构建过程中使用。
二、Dockerfile 核心指令详解
以下是最常用且最重要的 Dockerfile 指令,我们将逐一解析。
FROM
作用:指定新镜像所基于的基础镜像。
语法:FROM <image>[:<tag>] [AS <name>]
说明:FROM 指令必须是 Dockerfile 中第一条非注释的指令。AS <name> 用于在多阶段构建中为当前构建阶段命名。
代码案例:
FROM ubuntu:22.04
解析:这个镜像将基于 Ubuntu 22.04 官方镜像进行构建
WORKDIR
作用:设置后续 RUN, CMD, ENTRYPOINT, COPY, ADD 指令的工作目录。
语法:WORKDIR /path/to/workdir
说明:如果目录不存在,WORKDIR 会自动创建它。使用 WORKDIR 是一个非常好的实践,可以避免在多个指令中使用 cd 命令。
代码案例:
WORKDIR /app
解析:此后的所有指令,如 COPY . .,都会在容器内的 /app 目录下执行。
COPY 与 ADD
作用:将构建上下文中的文件或目录复制到镜像的文件系统中。
语法:COPY [--chown=<user>:<group>] <src>... <dest>
ADD [--chown=<user>:<group>] <src>... <dest>
核心区别:
COPY: 功能纯粹,就是复制文件/目录。
ADD: 功能更丰富,除了 COPY 的功能外,还支持:
自动解压:如果 <src> 是一个可识别的压缩文件 (如 tar, gzip, bzip2),ADD 会自动将其解压到 <dest>。
URL支持:如果 <src> 是一个URL,ADD 会尝试下载该文件。
最佳实践:优先使用 COPY。因为它的行为更明确、可预测。只在确实需要自动解压或远程下载时才考虑使用 ADD。
代码案例:
# 将当前目录下的 app.jar 复制到镜像的 /app/ 目录下 COPY app.jar /app/ # 将 src 目录下的所有内容复制到镜像的 /app/src/ 目录下 COPY src/ /app/src/
RUN
作用:在镜像构建过程中执行命令。
语法:
RUN <command> (shell 格式) RUN ["executable", "param1", "param2"] (exec 格式,推荐)
说明:每条 RUN 指令都会创建一个新的镜像层。为了减小镜像体积,通常建议将多个相关命令用 && 连接在同一条 RUN 指令中。
代码案例:
# shell 格式:安装依赖并清理缓存 RUN apt-get update && apt-get install -y wget && rm -rf /var/lib/apt/lists/* # exec 格式: RUN ["/bin/bash", "-c", "echo hello"]
EXPOSE
作用:声明容器在运行时会监听的网络端口。
语法:EXPOSE <port> [<port>/<protocol>...]
重要说明:EXPOSE 仅仅是一个文档性的指令,它并不会自动将端口发布到宿主机。实际发布端口需要在运行容器时使用 docker run -p <host_port>:<container_port> 参数。
代码案例:
# 声明容器将监听 8080 端口 EXPOSE 8080
CMD 与 ENTRYPOINT
作用:这两个指令都用于指定容器启动时要执行的命令。
| 指令 | 行为 | 语法 (推荐 exec 格式) |
|---|---|---|
| CMD | 提供容器启动时的默认命令。如果 docker run 命令后面跟了其他命令,CMD 会被覆盖。 | CMD ["executable", "param1", "param2"] |
| ENTRYPOINT | 配置容器使其像一个可执行文件。docker run 后面跟的所有内容都会被当作参数传递给 ENTRYPOINT。 | ENTRYPOINT ["executable", "param1", "param2"] |
最佳实践 (组合使用):
使用 ENTRYPOINT 定义容器的主执行命令,使用 CMD 提供该命令的默认参数。
ENTRYPOINT ["java", "-jar", "app.jar"] CMD ["--server.port=8080"]
docker run <image> -> 执行 java -jar app.jar --server.port=8080
docker run <image> --server.port=9090 -> CMD 被覆盖,执行 java -jar app.jar --server.port=9090
ENV 与 ARG
作用:用于定义变量。
核心区别:
ENV: 设置环境变量。它在构建过程中和容器运行时都有效。
ARG: 设置构建时变量。它只在 Dockerfile 构建过程中有效,容器运行后该变量不存在。
代码案例:
ARG APP_VERSION=1.0
ENV APP_HOME=/app
ENV APP_VERSION=${APP_VERSION} # 将ARG的值持久化到ENV中
WORKDIR ${APP_HOME}
RUN echo "Building version ${APP_VERSION} in ${APP_HOME}"
其他常用指令
- VOLUME: 创建一个可以挂载数据卷的挂载点。
- USER: 指定后续 RUN, CMD, ENTRYPOINT 指令所使用的用户名或UID。出于安全考虑,推荐创建一个非root用户来运行应用。
- LABEL: 为镜像添加元数据,如 LABEL maintainer="your.email@example.com"。
三、综合案例
这个案例将使用 多阶段构建,这是现代Dockerfile 的最佳实践,可以极大地减小最终镜像的体积
前提条件:
你有一个可以正常打包的 Spring Boot Maven 项目。
项目根目录下有 pom.xml 和 src 目录。
最终打包生成的 JAR 文件位于 target/ 目录下。
步骤一:在项目根目录下创建 Dockerfile 文件
# ---- Build Stage ---- # 使用一个包含 Maven 和 JDK 的镜像作为构建环境 FROM maven:3.8.3-openjdk-11 AS builder # 设置工作目录 WORKDIR /build # 复制 pom.xml 并下载依赖,利用 Docker 的层缓存机制 COPY pom.xml . RUN mvn dependency:go-offline # 复制源代码并进行打包 COPY src/ ./src/ RUN mvn package -DskipTests # ---- Runtime Stage ---- # 使用一个非常精简的、只包含Java运行时的镜像作为最终镜像 FROM openjdk:11-jre-slim # 设置工作目录 WORKDIR /app # 从构建阶段 (builder) 复制已打包好的 JAR 文件到当前阶段 COPY --from=builder /build/target/*.jar app.jar # 声明应用将监听的端口 EXPOSE 8080 # 定义容器启动时执行的命令 ENTRYPOINT ["java", "-jar", "app.jar"]
解析多阶段构建:
FROM ... AS builder: 定义了第一个阶段,并命名为 builder。这个阶段包含了所有构建工具 (Maven, JDK),它的唯一目的是生成 app.jar 文件。
FROM openjdk:11-jre-slim: 开始一个全新的、干净的构建阶段。这个基础镜像非常小,只包含运行Java应用所必需的 JRE。
COPY --from=builder ...: 核心步骤。它从之前命名为 builder 的构建阶段中,只把我们需要的构建产物 (app.jar) 复制到当前阶段。所有构建工具和中间文件都被丢弃了。
步骤二:构建镜像
在包含 Dockerfile 和项目代码的目录下,执行:
docker build -t my-springboot-app:1.0 .
步骤三:运行容器
docker run -d -p 8080:8080 --name spring-app my-springboot-app:1.0
步骤四:验证应用
# 查看容器日志,确认 Spring Boot 启动成功 docker logs spring-app # 使用 curl 测试应用的某个端点 (假设有一个 /hello 端点) curl http://localhost:8080/hello
通过这个流程,我们成功地将一个 Spring Boot 应用打包成一个轻量级、可移植、自包含的Docker镜像。
练习题
题目一:FROM 指令
一个 Dockerfile 的第一条有效指令 (非注释) 必须是什么?
题目二:COPY vs ADD
如果你只想简单地将本地的一个 config.json 文件复制到镜像中,应该优先选择 COPY 还是 ADD?为什么?
题目三:RUN 指令优化
以下 Dockerfile 写法有什么潜在问题?应该如何优化以减小镜像体积?
RUN apt-get update RUN apt-get install -y curl
题目四:CMD 与 ENTRYPOINT
假设 Dockerfile 中有 ENTRYPOINT ["/bin/echo", "Hello"]。执行 docker run <image> World 命令后,最终会执行什么命令?
题目五:EXPOSE 指令的作用
执行 EXPOSE 3000 指令后,在不使用 -p 参数的情况下运行容器,宿主机是否可以通过 localhost:3000 访问到容器?
题目六:WORKDIR 指令
以下 Dockerfile 执行后,pwd 命令的输出是什么?
WORKDIR /app WORKDIR client RUN pwd
题目七:ENV vs ARG
哪个指令设置的变量在容器运行后依然可以通过 env 命令查看到?
题目八:USER 指令
为了提高安全性,在 Dockerfile 中通常会在什么时间点之后使用 USER 指令切换到非root用户?
题目九:多阶段构建
在多阶段构建中,使用 COPY --from=<stage_name> 的主要目的是什么?
题目十:构建命令
如何构建一个名为 my-web-app,标签为 v2 的镜像,Dockerfile 位于当前目录?
题目十一:运行命令
如何以后台模式运行一个名为 webapp-instance 的容器,基于 my-web-app:v2 镜像,并将宿主机的 8888 端口映射到容器的 80 端口?
题目十二:Dockerfile 最佳实践
为什么在 RUN 指令中安装软件包后,通常会紧接着清理包管理器的缓存 (如 rm -rf /var/lib/apt/lists/*)?
题目十三:编写一个简单的 Dockerfile
编写一个 Dockerfile,基于 alpine 镜像,安装 curl 工具,并在容器启动时执行 curl ifconfig.me 命令。
题目十四:CMD 的覆盖
一个 Dockerfile 的最后一条指令是 CMD ["echo", "Default"]。如何运行这个镜像的容器,使其输出 “Hello Docker” 而不是 “Default”?
答案与解析
答案一:
FROM 指令。
答案二:
应该优先选择 COPY。
解析: COPY 的功能更单一、透明,就是复制文件。而 ADD 可能会有意想不到的自动解压行为,不够明确。遵循最小权限和最明确原则,选择 COPY。
答案三:
这会创建两个独立的镜像层。第一层缓存了 apt-get update 的结果,第二层安装了 curl。将它们合并到一条 RUN 指令中,以减少镜像层数,从而减小最终镜像的体积。
RUN apt-get update && apt-get install -y curl
答案四:
最终会执行 /bin/echo Hello World。
解析: 当 ENTRYPOINT 存在时,docker run 后面的所有内容 (World) 都被作为参数追加到 ENTRYPOINT 命令的末尾。
答案五:
不可以。
解析: EXPOSE 只起到声明和文档的作用,并方便容器间互联。要从宿主机访问,必须在 docker run 时使用 -p 或 -P 显式发布端口。
答案六:
输出是 /app/client。
解析: WORKDIR 指令可以使用相对路径。第二个 WORKDIR client 是相对于第一个 WORKDIR /app 的,所以最终工作目录是 /app/client。
答案七:
ENV 指令。
解析: ARG 是构建时变量,在镜像构建完成后就消失了。ENV 设置的环境变量会持久化在镜像中,并在容器运行时存在。
答案八:
通常在所有需要 root 权限的操作完成之后,例如安装软件包、创建目录、修改文件权限等 RUN 指令之后,在设置 ENTRYPOINT 或 CMD 之前。
# ... RUN chown -R myuser:mygroup /app USER myuser ENTRYPOINT ["./my-app"]
答案九:
主要目的是将前一个构建阶段的产物 (如编译好的二进制文件、打包好的JAR/WAR包) 复制到当前新的、更精简的构建阶段中,从而实现最终镜像的瘦身,去除所有不必要的构建工具和中间文件。
答案十:
docker build -t my-web-app:v2 .
答案十一:
docker run -d -p 8888:80 --name webapp-instance my-web-app:v2
答案十二:
因为 Dockerfile 的每一条 RUN 指令都会创建一个新的镜像层。如果在一条 RUN 指令中安装了软件包,然后在另一条 RUN 指令中清理缓存,那么包含缓存的那一层仍然存在于镜像中,无法减小镜像体积。将安装和清理放在同一条 RUN 指令中,可以确保在该层提交之前,缓存就被清除了,不会占用最终镜像的空间。
答案十三:
FROM alpine:latest RUN apk add --no-cache curl CMD ["curl", "ifconfig.me"]
解析: alpine 使用 apk 作为包管理器。--no-cache 选项可以在安装时避免留下缓存,是减小镜像体积的好习惯。CMD 使用 exec 格式定义启动命令。
答案十四:
docker run <image> echo "Hello Docker"
总结
到此这篇关于Docker核心技术之Dockerfile指令详解及综合案例的文章就介绍到这了,更多相关Docker Dockerfile指令详解内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
