在前面的章节中,我们学会了如何拉取和运行他人构建好的镜像。但要真正掌握 Docker,我们必须学会创建属于自己的镜像。Dockerfile 就是实现这一目标的核心工具。它就像一张自动化的"安装说明书"或"构建蓝图",让镜像的创建过程变得透明、可重复且易于版本控制
思维导图
一、什么是 Dockerfile?
Dockerfile 是一个包含一系列指令的文本文件。每一条指令都对应 Docker 镜像中的一个层。当我们执行 docker build
命令时,Docker 会逐一执行 Dockerfile 中的指令,最终生成一个完整的、可运行的自定义镜像。
核心构建命令:
bash
docker build -t <image_name>:<tag> .
-t
: 指定新镜像的名称和标签。
.
: 指定构建上下文的路径,通常是包含 Dockerfile 的当前目录。构建上下文中的所有文件都会被发送到 Docker 守护进程,以便在构建过程中使用。
二、Dockerfile 核心指令详解
以下是最常用且最重要的 Dockerfile 指令,我们将逐一解析。
FROM
作用 :指定新镜像所基于的基础镜像。
语法 :FROM <image>[:<tag>] [AS <name>]
说明 :FROM
指令必须是 Dockerfile 中第一条非注释的指令。AS <name>
用于在多阶段构建中为当前构建阶段命名。
代码案例:
bash
FROM ubuntu:22.04
解析 :这个镜像将基于 Ubuntu 22.04 官方镜像进行构建
WORKDIR
作用 :设置后续 RUN
, CMD
, ENTRYPOINT
, COPY
, ADD
指令的工作目录。
语法 :WORKDIR /path/to/workdir
说明 :如果目录不存在,WORKDIR
会自动创建它。使用 WORKDIR
是一个非常好的实践,可以避免在多个指令中使用 cd
命令。
代码案例:
bash
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
。
代码案例:
bash
# 将当前目录下的 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
指令中。
代码案例:
bash
# 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>
参数。
代码案例:
bash
# 声明容器将监听 8080 端口
EXPOSE 8080
CMD 与 ENTRYPOINT
作用 :这两个指令都用于指定容器启动时要执行的命令。
指令 | 行为 | 语法 (推荐 exec 格式) |
---|---|---|
CMD |
提供容器启动时的默认命令。如果 docker run 命令后面跟了其他命令,CMD 会被覆盖。 |
CMD ["executable", "param1", "param2"] |
ENTRYPOINT |
配置容器使其像一个可执行文件。docker run 后面跟的所有内容都会被当作参数传递给 ENTRYPOINT 。 |
ENTRYPOINT ["executable", "param1", "param2"] |
最佳实践 (组合使用) :
使用 ENTRYPOINT
定义容器的主执行命令,使用 CMD
提供该命令的默认参数。
bash
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 构建过程中有效,容器运行后该变量不存在。
代码案例:
bash
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
文件
bash
# ---- 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
和项目代码的目录下,执行:
bash
docker build -t my-springboot-app:1.0 .
步骤三:运行容器
bash
docker run -d -p 8080:8080 --name spring-app my-springboot-app:1.0
步骤四:验证应用
bash
# 查看容器日志,确认 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 写法有什么潜在问题?应该如何优化以减小镜像体积?
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
命令的输出是什么?
dockerfile
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
指令中,以减少镜像层数,从而减小最终镜像的体积。
bash
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
之前。
bash
# ...
RUN chown -R myuser:mygroup /app
USER myuser
ENTRYPOINT ["./my-app"]
答案九:
主要目的是将前一个构建阶段的产物 (如编译好的二进制文件、打包好的JAR/WAR包) 复制到当前新的、更精简的构建阶段中,从而实现最终镜像的瘦身,去除所有不必要的构建工具和中间文件。
答案十:
bash
docker build -t my-web-app:v2 .
答案十一:
bash
docker run -d -p 8888:80 --name webapp-instance my-web-app:v2
答案十二:
因为 Dockerfile 的每一条 RUN
指令都会创建一个新的镜像层。如果在一条 RUN
指令中安装了软件包,然后在另一条 RUN
指令中清理缓存,那么包含缓存的那一层仍然存在于镜像中,无法减小镜像体积。将安装和清理放在同一条 RUN
指令中,可以确保在该层提交之前,缓存就被清除了,不会占用最终镜像的空间。
答案十三:
dockerfile
FROM alpine:latest
RUN apk add --no-cache curl
CMD ["curl", "ifconfig.me"]
解析:
alpine
使用apk
作为包管理器。--no-cache
选项可以在安装时避免留下缓存,是减小镜像体积的好习惯。CMD
使用 exec 格式定义启动命令。
答案十四:
bash
docker run <image> echo "Hello Docker"

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