六、Docker 核心技术:Dockerfile 指令详解

发布于:2025-09-09 ⋅ 阅读:(24) ⋅ 点赞:(0)

在前面的章节中,我们学会了如何拉取和运行他人构建好的镜像。但要真正掌握 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> 是一个URLADD尝试下载该文件。
最佳实践优先使用 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.xmlsrc 目录。
最终打包生成的 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

题目四:CMDENTRYPOINT
假设 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 指令之后,在设置 ENTRYPOINTCMD 之前

# ...
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"

在这里插入图片描述

日期:2025年9月8日
专栏:Docker教程


网站公告

今日签到

点亮在社区的每一天
去签到