在前面的章节中,我们学会了如何拉取和运行他人构建好的镜像。但要真正掌握 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教程