当前位置 : 首页 » 文章分类 :  开发  »  Docker-Dockerfile

Docker-Dockerfile

Docker-Dockerfile

Dockerfile 是一个文本文件,其内包含了一条条的 指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。

Dockerfile reference
https://docs.docker.com/engine/reference/builder/


合理规划 Dockerfile 命令顺序来充分利用缓存层

Dockerfile 每一条 RUN、COPY、ADD、ENV 等指令都会生成一层(layer)。
只要前面的层没变,后面的层会直接复用缓存,不会重新执行。
只有发生变动的那一层及其后续层才会重新构建。


Python 项目利用 requirements.txt 缓存依赖包

例如 Python 项目的 Dockerfile 中:

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY 指令的缓存判断​​:
Docker 会检查源文件(requirements.txt)的​​内容校验和​​(checksum)。
如果文件内容或属性(如权限、时间戳)与上一次构建时​​完全相同​​,则使用缓存跳过执行。
如果文件有​​任何修改​​(哪怕一个字符变化),缓存失效,重新执行 COPY。
所以,仅当 requirements.txt 文件内容变化时才会重新执行 COPY 指令。

RUN 指令的缓存判断​​:
Docker 会检查该指令的​​字符串是否完全一致​​(包括空格和参数),如果指令的内容被修改(如命令变更),则 RUN 重新执行。

如果 COPY 指令因文件变动导致缓存失效,​​所有后续指令​​(包括 RUN)的缓存都会失效,需重新执行

COPY + RUN 指令顺序关系
如果 requirements.txt ​​未修改​​且 RUN 指令​​未改动​​ → 使用缓存,不执行。
如果 requirements.txt ​​文件发生变动​​ → COPY 重新执行 → RUN ​​必定重新执行​​(缓存链断裂)。
如果 RUN 指令​​被修改​​(如命令改成 pip install … → pip install … –new-arg)→ 即使 requirements.txt 未变,RUN 也会重新执行。


Java Maven 项目利用 pom.xml 缓存依赖包

Java Maven 项目的 Dockerfile 如果下面这样写,每次代码稍微有改动后打镜像,都需要重下载全部 maven 依赖,非常耗时:

# 复制所有源代码
COPY . .
# 执行 Maven 构建
RUN mvn clean package -Dmaven.test.skip=true

可以这样优化
先复制 pom.xml 文件,只要 pom.xml 不变,这些 COPY 指令就可以直接利用缓存,紧接着的 mvn dependency:go-offline 命令也可以利用缓存
就实现了利用 Docker 层缓存来缓存 Maven 依赖

# 先复制 pom.xml 文件,利用 Docker 层缓存来缓存 Maven 依赖
COPY pom.xml ./
COPY blog-api/pom.xml ./blog-api/
COPY blog-client/pom.xml ./blog-client/
COPY blog-persistence/pom.xml ./blog-persistence/
COPY blog-server/pom.xml ./blog-server/
COPY common/pom.xml ./common/

# 预下载 Maven 依赖,利用 go-offline 缓存依赖包
RUN mvn dependency:go-offline

# 复制所有源代码
COPY . .

# 执行 Maven 构建
RUN mvn clean package -Dmaven.test.skip=true

Node.js 利用 package.json 缓存依赖包


Dockerfile 最佳实践

https://docs.docker.com/develop/develop-images/dockerfile_best-practices/


FROM 指定基础镜像

定制镜像必须以一个镜像为基础,在其上进行定制。
FROM 指定 基础镜像,因此一个 Dockerfile 中 FROM 是必备的指令,并且必须是第一条指令。
Docker 还存在一个特殊的镜像,名为 scratch 。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。


VOLUME 创建挂载点

VOLUME 在镜像中创建挂载点
VOLUME 后的目录格式可以是纯文本的目录,空格分割的多个目录,或者json格式的字符串数组

VOLUME /var/log
VOLUME /var/log /var/db
VOLUME ["/data1","/data2"]

相比于 docker run 时 -v 指定目录映射, VOLUME 指令可实现通过此镜像创建的容器内都有一个预先创建好的目录,当然使用 docker run -v 也完全可以代替 VOLUME 指令。


COPY 复制文件

COPY [--chown=<user>:<group>] <源路径>... <目标路径>
COPY [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]
COPY 指令将从构建上下文目录中 源路径 的文件/目录复制到新的一层的镜像内的 目标路径 位置。
<源路径> 构建上下文中的文件或目录(可以是多个)。​​通配符(glob模式,如 *.js)是允许的。​
<目标路径> 可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 WORKDIR 指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。
--chown (可选) 设置复制到镜像中文件的所有者和所属组。格式如 --chown=user:group--chown=user(组使用用户组)或 --chown=:group(用户默认,通常是root)。如果源是远程URL(ADD支持),则无效;在Windows上需要设置的用户/组在容器中必须存在。

使用 COPY 指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。

如何判断目标是文件还是目录?

  • 目标路径以斜杠结尾 (/):​​ 明确指出目标是一个目录(即使不存在也会创建)。
  • 目标路径不以斜杠结尾:
    • 如果 <源路径> 是单个文件,Docker 会将其视为复制到 <目标路径> 指定的​​文件路径​​。
    • 如果 <源路径> 是目录或使用了通配符,Docker 会将其视为复制到 <目标路径> 指定的​​目录路径​​。

如果 COPY 的目标文件/目录已存在会怎样?
1、目标路径是文件:

  • 如果 <源路径> 是单个文件,且 <目标路径> 指向一个​​已存在​​的文件,那么 COPY 会 ​​覆盖​​ 该目标文件的内容。源文件的新内容替换掉旧内容。
  • 如果 <目标路径> 指定的文件不存在,Docker 会创建该文件并写入源文件的内容。

2、目标路径是目录:

  • 如果 <目标路径> 目录不存在,Docker 会​​自动创建它​​(以及任何必要的父目录),然后将 <源路径> 的内容复制到新创建的目录中。
  • 如果 <目标路径> 指向一个​​已存在​​的目录,且 <源路径> 是文件或目录:
    • <源路径> 下的 ​​文件​​ 会被复制到 <目标路径> 目录下。
    • 如果这些文件在 <目标路径> 目录下​​已存在同名文件​​,那么会被 ​​覆盖​​。
    • ​​镜像中 <目标路径> 目录下原有的其他文件或子目录(且不是被 <源路径> 中的文件覆盖的)会保留不变。

COPY 指令的缓存机制
对于 COPY 指令,Docker 会计算构建上下文中指定的源文件或目录内容的 ​​校验和(通常是基于文件内容的哈希值)​​。它也会检查目标路径。
缓存命中,如果 Docker 发现:
1、正在构建的 COPY 指令与之前构建中的一条 COPY 指令​​完全相同​​(包括相同的源路径模式、相同的目标路径、相同的 –chown 选项等)。
2、构建上下文中源文件或目录的内容的​​校验和没有发生变化​​。
3、之前构建中该 COPY 指令之后的​​所有层都存在于缓存中​​。
那么 Docker 会复用之前构建该指令时创建的镜像层(缓存),跳过执行这条 COPY 指令。
缓存失效:
1、​​源文件/目录内容改变:​​ 任何匹配 <源路径> 的文件内容被修改、文件被添加或删除(影响目录校验和)。
2、指令本身改变:​​ 修改了 COPY 指令中的任何部分(源路径模式、目标路径、–chown 选项)。
3、​之前的层无效:​​ 在 COPY 指令之前的任何指令(COPY、RUN 等)导致缓存失效(例如修改 RUN 命令或之前 COPY 的文件变化)。

COPY . . 的缓存机制
COPY . . 意味着将整个​​当前构建上下文​​复制到镜像中相对于 WORKDIR 的当前目录 .
这个指令的缓存取决于​​整个构建上下文​​(通常是 Dockerfile 所在目录及其子目录)的内容校验和是否与上次成功构建并使用缓存的那次相同。
任何​​在构建上下文中的文件或目录被修改、添加或删除,都会导致 COPY . . 指令的缓存失效,使其在下次构建时重新执行,将所有上下文文件复制到镜像中。
.dockerignore 文件可以排除不需要的文件,这样被排除文件的变化就不会导致 COPY . . 的缓存失效。这是控制缓存和提高构建速度的关键手段。
所以 COPY . . 会利用缓存,但要求整个构建上下文的内容自上次缓存创建以来完全没有改变。实践中,只要项目文件有变化(比如改一行代码),缓存通常就失效了,它会重新执行。

COPY 到容器中的文件所有权
Docker 构建的默认执行用户是 root:​​ 如果没有 USER 指令干预,COPY 操作(以及其他 RUN、CMD 等)由 root 执行,文件也归 root 所有。
Docker COPY 指令在将文件复制到镜像中时,会​​尽力保留源文件在构建主机上的权限位​​(包括 rwxrwxrwx 中的读、写、执行标志)。
默认情况下,复制进镜像的文件所有者和所属组都是 ​​root:root​​,除非通过 --chown 选项显式指定。
即使当前执行 docker build 命令的用户是 appuser,所有指令(包括 COPY)默认也是以 ​​root 用户​​的身份执行的,拷贝进容器的文件所有者也是 root
使用 --chown 选项(如 COPY --chown=node:node ...)时,Docker 会尝试将复制的文件/目录的所有权和组设置为指定的值。指定的用户和组​​必须​​在镜像中存在(例如,在 COPY 之前通过 RUN adduser ... 创建),否则构建会失败。

若果 COPY 前有 USER 指令,则执行 COPY 指令的进程身份变成了 USER 指令所设定的用户,那么拷贝进容器的文件的所有者变为 USER 指定的用户。
例如

FROM ubuntu
RUN useradd -ms /bin/bash newuser # 创建新用户 newuser
USER newuser                       # 将后续执行用户切换为 newuser
COPY file.txt /home/newuser/       # 这个 COPY 操作是由 newuser 用户执行的

COPY 指令由 newuser 用户执行。因此,复制进去的 file.txt 在镜像中的所有者就是 ​​newuser:newuser


ADD 更高级的复制文件(自动解压)

ADD 指令和 COPY 的格式和性质基本一致。但是在 COPY 基础上增加了一些功能。
比如 源路径 可以是一个 URL,这种情况下,Docker 引擎会试图去下载这个链接的文件放到 目标路径 去。
下载后的文件权限自动设置为 600,如果这并不是想要的权限,那么还需要增加额外的一层 RUN 进行权限调整,
另外,如果下载的是个压缩包,需要解压缩,也一样还需要额外的一层 RUN 指令进行解压缩。
所以不如直接使用 RUN 指令,然后使用 wget 或者 curl 工具下载,处理权限、解压缩、然后清理无用文件更合理。
因此,这个功能其实并不实用,而且不推荐使用。

相比 COPY, ADD 其实就增加了2个特性:
1、ADD 指令可以让你使用 URL 作为 src 参数。当遇到 URL 时候,可以通过 URL 下载文件并且复制到 dest
2、ADD 的另外一个特性是有能力自动解压文件。如果 src 参数是一个可识别的压缩格式(tar, gzip, bzip2, etc)的本地文件(所以实现不了同时下载并解压),就会被解压到指定容器文件系统的路径 dest


WORKDIR 指定工作目录

WORKDIR <工作目录路径>
使用 WORKDIR 指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录。
如该目录不存在,WORKDIR 会帮你建立目录。

WORKDIR /path/to/workdir
WORKDIR 指令用于​​设置后续 Dockerfile 指令(如 RUN, CMD, ENTRYPOINT, COPY, ADD)执行时的工作目录​​。这就像在命令行中执行 cd 命令一样,改变了当前所在的路径。

如果路径已存在?

  • 如果指定的路径 /path/to/workdir 在构建时所在的镜像层中已经存在(无论是之前 RUN mkdir 创建的基础镜像自带,还是由之前的 WORKDIR 创建的),WORKDIR 会简单地切换到该已存在的目录​​,将其设置为后续指令的工作目录。没有任何问题或冲突。
  • 如果指定的路径 /path/to/workdir 不存在,WORKDIR 指令会自动创建该目录及其所有必要的父目录。​​这就是它同时具备的“创建目录”的便利功能。

创建的目录的所有权是谁的?
这取决于​​创建该目录时的有效用户身份(UID/GID)

  • 默认情况:​​ 在 Dockerfile 构建过程中,如果没有使用 USER 指令切换用户,默认的有效用户是 ​​root (UID 0)​​。因此,自动创建的目录所有权为 root:root。
  • 如果使用了 USER 指令:​​ 如果在 WORKDIR 指令之前使用了 USER <username|UID>[:<group|GID>] 指令将用户切换为非 root 用户,那么 WORKDIR 自动创建的目录所有权将属于​​当前有效的 USER​​。
    例如:
    USER appuser:appgroup
    WORKDIR /app
    
    此时 /app 目录(如果之前不存在)会被创建,并且所有权为 appuser:appgroup。

相对路径
WORKDIR 指令可以接受相对路径。相对路径是相对于​​前一个 WORKDIR 指令设置的路径​​。如果没有前一个 WORKDIR,则相对于镜像的根目录 /
例如

WORKDIR /usr  # 绝对路径,切换到 /usr
WORKDIR local  # 相对路径,相对于上一个 WORKDIR (/usr),切换到 /usr/local
RUN pwd        # 输出: /usr/local

多次使用
Dockerfile 中可以包含多个 WORKDIR 指令。后续指令的工作目录由最后一条 WORKDIR 指定。

避免冗余 RUN cd
在 Dockerfile 中应该优先使用 WORKDIR 而不是 RUN cd ... && some_command
因为 RUN 会创建新层,而 WORKDIR 是元数据操作,更高效,且确保了状态延续(下一个 RUN 不会“忘记”之前的 cd)。


EXPOSE 声明端口

EXPOSE <端口1> [<端口2>...]
EXPOSE 指令是声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务
EXPOSE 仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。


RUN 执行命令(安装软件)

RUN 指令是用来执行命令行命令的,通常用于安装应用和软件包。
Dockerfile 中每一个指令都会建立一层,RUN 也不例外。每一个 RUN 的行为,就和刚才我们手工建立镜像的过程一样:新建立一层,在其上执行这些命令,执行结束后,commit 这一层的修改,构成新的镜像。

例如

RUN apk add --no-cache tzdata \
 && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
 && echo "Asia/shanghai" > /etc/timezone \
 && rm -rf /var/cache/apk/* /tmp/* /var/tmp/* $HOME/.cache ## 清除缓存
RUN chmod +x /root/apps/start_jar.sh

第一个安装时区的多条命令放在一个 RUN 中,保证构建在同一层镜像

CMD 默认容器启动命令

CMD 指令用于指定默认的容器主进程的启动命令的,此命令会在容器启动且 docker run 没有指定其他命令时运行,如果 docker run 后面指定其他命令则 CMD 会被忽略
在运行时可以指定新的命令来替代镜像设置中的这个默认命令
Dockerfile 中可以有多个 CMD 指令,但只有最后一个生效

例如
CMD echo "Hello world" 运行容器 docker run -it [image] 将输出 Hello world,但当后面加上一个命令,比如 docker run -it [image] /bin/bash,CMD 会被忽略掉,命令 bash 将被执行,会进入容器。

ENTRYPOINT 入口点

ENTRYPOINT 用于设置容器启动时要执行的命令及其参数,当指定了 ENTRYPOINT 后,CMD 的含义就发生了改变,不再是直接的运行其命令,而是将 CMD 的内容作为参数传给 ENTRYPOINT 指令,换句话说实际执行时,将变为: <ENTRYPOINT> "<CMD>"
与 CMD 不同的是,不管 docker run … 后是否运行有其他命令,ENTRYPOINT 指令后的命令一定会被执行。
Dockerfile 中可以有多个 ENTRYPOINT 指令,也是只有最后一个生效

RUN/CMD/ENTRYPOINT 区别

RUN 命令会创建新的镜像层,通常用于安装应用和软件包。Dockerfile 中常常包含多个 RUN 指令,每条 RUN 指令都会生成新的一层。
CMD 和 ENTRYPOINT 都可用于设置容器的启动命令,CMD 会被 docker run 命令覆盖,而 ENTRYPOINT 不会,docker run 命令中的参数都会成为 ENTRYPOINT 的参数。

例如

ENTRYPOINT ["/usr/bin/rethinkdb"]
CMD ["--help"]

这个 dockerfile 里 ENTRYPOINT 后面还有个 CMD 的考虑是,如果 docker run 没有参数,CMD(–help)将成为 ENTRYPOINT 的默认参数,输出帮助信息,例如 docker run rethinkdb 时就会输出帮助信息。
当 docker run 有参数时会代替 CMD 命令,例如 docker run -it rethinkdb bash 可以进入容器。

Shell 格式与 Exec 格式

RUN/CMD/ENTRYPOINT 命令的格式有两种:
1、shell 格式
RUN/CMD/ENTRYPOINT <命令> 就像直接在命令行中输入的命令一样。
例如

RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
RUN yum install -y vim
CMD echo "hello zhurs"
ENTRYPOINT echo "hello zhurs"

2、exec 格式
RUN/CMD/ENTRYPOINT ["可执行文件", "参数1", "参数2"]
这更像是函数调用中的格式
CMD 和 ENTRYPOINT 推荐使用 Exec 格式,因为指令可读性更强,更容易理解。RUN 则两种格式都可以
例如

RUN ["yum", "install", "-y", "vim"]
CMD ["bin/echo", "zhurs"]
ENTRYPOINT ["/bin/echo", "hello, $wd"]
ENTRYPOINT ["java", "-jar", "/blog-server.jar"]

USER 指定容器用户

USER <user>[:<group>]

USER <UID>[:<GID>]

指定运行容器时的用户名或 UID,后续的 RUN 等指令也会使用指定的用户身份
使用 USER 指定用户时,可以使用用户名、UID 或 GID,或是两者的组合
使用 USER 指定用户后,Dockerfile 中后续的命令 RUN、CMD、ENTRYPOINT 都将使用该用户

也可以使用 docker run -u 指定用户


镜像压缩技巧

一、dockerfile 优化
1、yum安装依赖之后执行yum -y clean all && rm -rf /var/cache 可以删除无用的缓存文件
2、RUN 合并,用 && 连接多个命令,减少镜像层浪费
3、wget下载等固定的命令执行放在前面,install等容易变动的命令放在后面,可以更好的利用分层缓存机制,可以减少镜像构建的等待时间
4、从基础镜像中拷贝编译环境中文件,减少镜像层层数

FROM iregistry.baidu-int.com/baidu-base/golang:1.19-alpine3.16 AS builder
COPY --from=builder /app/bin/ /app/bin/

二、docker export/import 镜像导出再导入可压缩镜像


上一篇 Docker-常用命令

下一篇 Docker-基础

阅读
评论
4.9k
阅读预计18分钟
创建日期 2025-08-05
修改日期 2025-08-05
类别
标签

页面信息

location:
protocol:
host:
hostname:
origin:
pathname:
href:
document:
referrer:
navigator:
platform:
userAgent:

评论