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

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
简介: 想亲手给你的应用程序打造一个专属的“集装箱”吗?Dockerfile就是你的说明书!它其实就是一个简单的文本文件,你可以在里面像搭积木一样,用FROM、COPY、RUN这些指令,一步步告诉Docker如何打包你的应用。最后,通过多阶段构建的小技巧,还能给镜像“减肥”,让它变得轻巧又高效。快来学习用Dockerfile变身打包达人吧!

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

思维导图

image.png

image.png

一、什么是 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"
目录
相关文章
|
20天前
|
应用服务中间件 Shell nginx
七、Docker核心技术:深入理解网络模式 (Bridge, Host, None, Container)
容器不仅仅是孤立的运行环境,它们需要相互通信,也需要与外部世界进行交互。理解 Docker 的不同网络模式,是构建和部署复杂多容器应用的关键。本节将深入探讨 Docker 原生提供的四种网络模式以及强烈推荐使用的自定义网络。要让它们通信,需要将其中一个容器也连接到另一个网络上。默认 bridge 网络不支持容器名DNS解析,只能通过IP地址通信。容器没有自己的独立IP地址,它共享宿主机的IP。网络模式启动一个容器后,如何查看该容器的IP地址?时,该容器默认会连接到哪个网络?模式运行,并且其内部的应用监听。
390 4
|
20天前
|
关系型数据库 MySQL Shell
三、Docker常用命令
把 Docker 玩转,就像一个建筑师,需要掌握两套核心工具:一套用来管理你的“图纸”(镜像),另一套用来管理你用图纸盖好的“房子”(容器)。
193 2
|
10天前
|
存储 Kubernetes 数据库
K3S ——轻量化K8S 入门指南
本文介绍轻量级Kubernetes发行版K3s,适用于边缘计算、IoT等场景。涵盖其架构、安装部署(单节点/高可用/离线)、核心组件、网络存储配置及生产建议,助力快速构建轻量化容器平台。
148 4
|
20天前
|
存储 关系型数据库 MySQL
五、Docker 核心技术:容器数据持久化之数据卷
别把重要数据直接放进Docker容器里,因为容器就像一辆“临租车”,车一还(容器被删除),落在里面的东西就全没了。正确的做法是使用数据卷 (Volume),它好比一个属于你自己的、可插拔的“移动硬盘”。你可以把这个“硬盘”(具名数据卷)挂载到任何一辆“临租车”(容器)上使用。这样一来,就算车换了,你的数据也安然无恙,完美解决了数据库等应用的数据持久化问题。
167 32
五、Docker 核心技术:容器数据持久化之数据卷
|
30天前
|
Java
Java基础知识总结(超详细整理)
本文系统总结Java基础知识,涵盖语法、面向对象(类与对象、封装、继承、多态)、常用类(String、包装类、集合框架)及异常处理等核心内容,结合代码示例深入浅出,助你扎实掌握Java编程基础。
144 1
|
1月前
|
人工智能 Java Nacos
基于 Spring AI Alibaba + Nacos 的分布式 Multi-Agent 构建指南
本文将针对 Spring AI Alibaba + Nacos 的分布式多智能体构建方案展开介绍,同时结合 Demo 说明快速开发方法与实际效果。
1608 58
|
20天前
|
存储 监控 Shell
四、Portainer图形化管理实战与Docker镜像原理
如果觉得命令行繁琐,可以试试Portainer这个图形化管理工具,让你在网页上点点鼠标就能轻松管理容器和镜像。安装它只需要一条docker run命令,非常方便。 同时,要理解Docker为何如此高效,关键在于它的镜像原理:镜像像洋-葱一样分层,启动容器时只在外面加一层可写的“外皮”。所有改动都发生在这层“外皮”上,这就是容器启动快、占用空间小的秘诀。
187 4
|
1月前
|
运维 开发者 Docker
一、Docker:一场颠覆应用部署与运维的容器革命
Docker的出现,就是为了解决“在我电脑上能跑”这个老大难问题。它像个魔法集装箱,把你的程序和它需要的所有东西(比如库、配置)都打包好,这样无论在哪运行,环境都一模一样。理解它很简单,就三个核心玩意儿:镜像是程序的“安装包”,容器是跑起来的程序,而仓库就是存放和分享这些“安装包”的地方。
336 6
|
24天前
|
存储 算法 Java
深入理解JVM:内存管理与垃圾回收机制探索
JVM是Java程序的运行核心,实现跨平台、自动内存管理与高效执行。其架构包括类加载、运行时数据区、执行引擎等模块。内存模型历经演变,JDK 8起以元空间替代永久代,优化GC性能。JVM通过分代回收机制,结合标记清除、复制、整理等算法,管理对象生命周期,提升系统稳定性与性能。
kde
|
1月前
|
应用服务中间件 网络安全 nginx
手把手教你使用 Docker 部署 Nginx 教程
本文详解Nginx核心功能与Docker部署优势,涵盖镜像拉取、容器化部署(快速、挂载、Compose)、HTTPS配置及常见问题处理,助力高效搭建稳定Web服务。
kde
748 4