type
Post
status
Published
date
Mar 22, 2026
slug
/docker/images
summary
tags
docker
category
docker
icon
password
Docker镜像制作
镜像是容器运行的基础。每次执行
docker run 时,都需要指定一个镜像,容器就是基于该镜像启动的实例。如果直接使用公共镜像(如 Docker Hub 上的镜像)可以满足部分基础需求,但在实际生产环境中,往往存在以下情况:需要安装额外的软件、需要定制配置文件、需要预置运行环境(如 JDK、Python、依赖库等)。因此,需要基于基础镜像进行定制化构建。Docker 镜像的制作过程,可以类比为虚拟机中的“模板制作”:先准备好基础环境(软件、配置等),再将其保存为一个镜像(模板),最后基于该镜像批量创建容器。这样可以实现环境统一、快速部署、批量扩展。Docker 提供了两种制作镜像的方式:
基于容器手动制作:通过运行容器并手动修改环境,使用
docker commit 保存为镜像
基于 Dockerfile 自动化制作:通过编写 Dockerfile 描述构建过程,使用 docker build 构建镜像docker commit 构建镜像(不建议)
docker commit 用于将一个已有容器的当前状态保存为一个新的镜像。制作镜像和容器状态无关,容器是否运行,都不影响镜像制作,可以基于运行中或已停止的容器创建镜像。其本质上是把容器当前的文件系统状态保存为镜像。commit 不会改变镜像的默认启动命令(CMD/ENTRYPOINT),也就是说新镜像会继承原镜像的CMD、ENTRYPOINT。当你再次基于该镜像运行容器时,默认启动命令仍然是原来的配置,生产环境应优先使用 Dockerfile。
如果未指定
REPOSITORY[:TAG],镜像名称和标签都会是 <none>(悬空镜像),推荐在提交时指定标签,方便后续进行版本管理。docker commit 命令格式如下:基于容器手动制作镜像步骤具体如下:
1、下载一个系统的官方基础镜像,如: CentOS 或 Ubuntu
2、基于基础镜像启动一个容器,并进入到容器
3、在容器里面做配置操作:安装基础命令、配置运行环境、安装服务和配置服务、放业务程序代码
4、提交为一个新镜像 docker commit
5、基于自己的的镜像创建容器并测试访问
镜像是多层存储,每一层是在前一层的基础上进行的修改。而容器同样也是多层存储,是在以镜像为基础层,在其基础上加一层作为容器运行时的存储层。现在让我们以定制一个 Web 服务器为例子,来讲解镜像是如何构建的
这条命令会用 nginx 镜像启动一个容器,命名为 webserver,并且映射了 80 端口,这样我们可以用浏览器去访问这个 nginx 服务器。如果是在本机运行的 Docker,那么可以直接访问:http://localhost,如果是在虚拟机、云服务器上安装的 Docker,则需要将 localhost 换为虚拟机地址或者实际云服务器地址。直接用浏览器访问的话,我们会看到默认的 Nginx 欢迎页面

假设我们非常不喜欢这个欢迎页面,我们希望改成欢迎 Docker 的文字,我们可以使用 docker exec 命令进入容器,修改其内容

我们以交互式终端方式进入 webserver 容器,并执行了 bash 命令,也就是获得一个可操作的 Shell。然后,我们用
<h1>www.qianshuaiblog.cn</h1> 覆盖了 /usr/share/nginx/html/index.html 的内容。现在我们再刷新浏览器的话,会发现内容被改变了
我们修改了容器的文件,也就是改动了容器的存储层。我们可以通过 docker diff 命令看到具体的改动

那么,我们希望将定制好的容器保存下来,形成镜像。当我们运行一个容器的时候 (如果不使用卷的话),我们做的任何文件修改都会被记录于容器存储层里。而 Docker 提供了一个 docker commit 命令,可以将容器的存储层保存下来成为镜像。换句话说,就是在原有镜像的基础上,再叠加上容器的存储层,并构成新的镜像。以后我们运行这个新镜像的时候,就会拥有原有容器最后的文件变化。我们可以用下面的命令将容器保存为镜像:
我们可以在 docker image ls 中看到这个新定制的镜像:

我们还可以用 docker history 具体查看镜像内的历史记录,如果比较 nginx:latest 的历史记录,我们会发现新增了我们刚刚提交的这一层

新的镜像定制好后,我们可以来运行这个镜像
这里我们命名为新的服务为 webserver2,并且映射到 81 端口。访问 http://localhost:81 看到结果,其内容应该和之前修改后的 webserver 一样。至此,我们第一次完成了定制镜像,使用的是 docker commit 命令,手动操作给旧的镜像添加了新的一层,形成新的镜像,对镜像多层存储应该有了更直观的感觉。
下面我们再来运行一个 busybox 容器进行演示,创建对应的文件
将 busybox_http 容器制作成镜像
启动容器服务,并访问测试

在原有镜像的基础上,再叠加上容器的存储层,并构成新的镜像

为什么不建议使用 docker commit 手动构建镜像?当运行一个容器时(不使用 volume 的情况下),所有文件修改都会记录在容器的可写层(container layer)。Docker 镜像采用分层存储机制,每一层都是只读的,修改不会改变旧层,而是新增一层。执行 docker commit 时,会把这一层保存下来,叠加到原镜像之上,形成一个新镜像。如果不断执行 commit,镜像体积会不断变大,镜像会越来越臃肿。使用 docker commit 构建镜像,没有构建过程记录,无法知道执行过哪些命令,无法复现构建过程。这种镜像被称为
黑箱镜像,问题在于,别人无法维护,自己过一段时间也会忘记怎么做的,无法纳入 CI/CD 流程。docker commit 并不是完全没用,它适用于临时调试环境保存,快速备份当前容器状态,但不适合用于正式构建镜像。
制作 nginx 镜像
基于Ubuntu
基于Ubuntu的基础镜像利用 apt 安装手动制作 nginx 的镜像,启动Ubuntu基础镜像
安装nginx,并修改index.html文件
执行exit命令退出容器,并将容器提交为镜像
将新制作的镜像启动为容器
测试访问

基于CentOS
yum安装
基于CentOS的基础镜像利用 yum 安装,手动制作 nginx 镜像,本次使用官方提供的centos镜像为基础
修改时区
更改 yum 源,清除缓存,并重新构建 yum 源
安装nginx
修改服务的配置信息,关闭 nginx 后台运行
自定义web界面
新打开一个终端,在不关闭容器的情况下,将容器提交为镜像
从制作的镜像启动容器
访问测试

编译安装
本次使用官方提供的centos镜像为基础,编译安装nginx
修改时区
更改 yum 源,清除缓存,并重新构建 yum 源
安装相关软件工具
安装nginx
自定义web界面
提交为镜像,不要退出容器,在另一个终端窗口执行以下命令
从制作的镜像启动容器
访问测试镜像

Dockerfile
Dockerfile 是用来构建镜像的脚本文件,由一条条指令组成。Docker 在构建镜像时,会逐行读取 Dockerfile 中的指令,并按照顺序执行,最终生成一个镜像。相比于手动commit制作镜像的方式,Dockerfile更能直观的展示镜像是怎么产生的。
Dockerfile中每条指令都是独立运行的,每条指令都会创建一个新的镜像层,并对镜像进行提交,最多不超过128层,命令最多128行。比如
RUN cd /tmp 对下一条指令不会有任何影响。基于 Dockerfile 制作镜像步骤具体如下:
1、 编写一个dockerfile文件
2、 docker build 构建称为一个镜像
3、 docker push 发布镜像(DockerHub、阿里云仓库)
4、 docker run 运行镜像Dockerfile 是一个有特定语法格式的文本文件,dockerfile 官方说明:<a href="https://docs.docker.com/engine/reference/builder/">https://docs.docker.com/engine/reference/builder/</a>
1、每一行以Dockerfile的指令开头,指令不区分大小写,但是惯例使用大写。
#开头表示注释
2、指令按文件的顺序从上至下进行执行,每一行只支持一条指令,每条指令可以携带多个参数
3、每个指令的执行会生成一个新的镜像层,为了减少分层和镜像大小,尽可能将多条指令合并成一条指令
4、制作镜像一般可能需要反复多次,每次执行dockfile都按顺序执行,从头开始,已经执行过的指令已经缓存,不需要再执行,如果后续有一行新的指令没执行过,其往后的指令将会重新执行,所以为加速镜像制作,将最常变化的内容放下dockerfile的文件的后面FROM 指定基础镜像
FROM 用于指定基础镜像,是构建镜像的起点。一般来说,Dockerfile 通常以 FROM 作为第一条指令,后续所有指令都是基于这个基础镜像进行构建的。FROM 指定的镜像可以来自远程仓库或本地镜像仓库。基础镜像可以理解为,一个不依赖其他镜像、作为起点的镜像。常见基础镜像有alpine(极简)、ubuntu、debian、centos,这些镜像通常提供:基础操作系统环境,包管理工具,基础库文件。
FROM 语法:
多阶段构建
Docker 支持多阶段构建,可以写多个 FROM,但每个阶段必须以 FROM 开始。在 Docker 17.05 版本之前,我们构建 Docker 镜像时,通常会采用两种方式:全部放入一个 Dockerfile 、分散到多个 Dockerfile
一种方式是将所有的构建过程包含在一个 Dockerfile 中,包括项目及其依赖库的编译、测试、打包等流程,这里可能会带来的一些问题:
镜像层次多:每个 RUN 都会增加一层,构建链路较长;
镜像体积较大:包含编译环境(如 Go、gcc、git 等),实际运行时并不需要这些工具;
部署效率低:镜像大,拉取慢,启动慢;
源代码存在泄露的风险:源代码被 COPY 进镜像,最终镜像中仍然存在源码。
例如,编写 app.go 文件,该程序输出 www.qianshuaiblog.cn
编写 Dockerfile 文件
构建镜像
我们来运行一下刚刚构建好的镜像

我们再来看另一种方式:双 Dockerfile 构建方式,就是将构建过程和运行环境拆分为两个 Dockerfile。一个负责编译构建,一个负责运行程序,这种方式虽然可以很好地规避第一种方式存在的风险,但明显部署过程较复杂。例如,编写 Dockerfile_build 文件:
编写 Dockerfile_run 文件:
构建镜像
我们来运行一下刚刚构建好的镜像

这种方式的整体流程:使用 Dockerfile_build 构建编译环境镜像,运行容器并生成二进制文件 app,将 app 拷贝出来(例如用 docker cp),再使用 Dockerfile_run 构建运行镜像。本质是手动实现 构建阶段 + 运行阶段 的分离。对比两种方式生成的镜像大小:

为解决以上问题,Docker v17.05 开始支持多阶段构建。使用多阶段构建我们就可以很容易解决前面提到的问题,并且只需要编写个 Dockerfile,例如,编写 Dockerfile 文件:
构建镜像
我们来运行一下刚刚构建好的镜像

最后,对比三个镜像大小,很明显使用多阶段构建的镜像体积小,同时也完美解决了上边提到的问题

scratch
除了使用已有镜像作为基础镜像外,Docker 还提供了一个特殊的镜像scratch。什么是 scratch?scratch 是一个特殊的基础镜像标识,它并不是真实存在的镜像文件。可以理解为空镜像,scratch 主要用于以下场景:
构建极小镜像:只包含一个可执行文件,没有多余依赖
构建基础镜像:用于构建像 busybox、debian 这类基础镜像的起点
LABEL 指定镜像元数据
LABEL可以指定镜像元数据,如镜像作者等。一个镜像可以有多个label,还可以写在一行中,即多标签写法,可以减少镜像的的大小
示例:
docker inspect 命令可以查看LABEL信息:RUN 执行命令
RUN 用于在构建镜像阶段执行命令,通常用来安装软件、安装依赖、执行编译等。RUN 执行的是基础镜像支持的命令,如 shell 命令,默认使用
/bin/sh -c 执行。RUN 可以写多个,每一个RUN指令都会建立一个镜像层,所以尽可能合并成一条指令。每个 RUN 是一个独立的容器执行环境(临时容器),执行的结果会被保存为镜像层。为了减少镜像体积和层数,应使用 && 连接命令。RUN格式有两种:
exec格式:这类格式在解析时会被解析为 JSON 数组,因此一定要使用双引号,而不要使用单引号。不会有额外的sh进程。
shell格式:会通过 shell 默认
/bin/sh -c 执行,支持:环境变量、管道、重定向、shell表达式。比如 CMD echo $HOME 在实际执行中,会将其变更为 CMD [ "sh", "-c", "echo $HOME" ] ,这就是为什么我们可以使用环境变量的原因,因为这些环境变量会被 shell 进行解析处理。每一个 RUN 指令都会新建一层镜像。为了减少镜像体积和层数,应使用 && 连接命令。糟糕的 RUN 写法,创建 3 层:
推荐写法 ,创建 1 层:
常见问题:为什么
RUN cd 不生效?每个 RUN 都在一个新的 Shell/容器环境中执行,cd 只影响当前 RUN 的环境。详细内容请看WORKDIR章节。WORKDIR 指定工作目录
WORKDIR 用于指定容器构建和运行阶段的工作目录(等效于 Linux 的 cd 命令,但更安全)。若指定的目录不存在,WORKDIR 会自动递归创建,无需手动 RUN mkdir,为后续的 RUN、CMD、ENTRYPOINT、COPY、ADD 指令统一设置当前工作目录。容器启动后,默认进入 WORKDIR 指定的目录,无需手动 cd。
很多人把 Dockerfile 等同于 shell 脚本来书写,这样是错误的,下面编写一个Dockerfile
构建镜像
将镜像运行为容器

将这个 Dockerfile 进行构建镜像运行后,会发现找不到
/tmp/world.txt 文件,因为在 Shell 中,连续两行是同一个进程执行环境,因此前一个命令修改的内存状态,会直接影响后一个命令。而在 Dockerfile 中,这两行 RUN 命令的执行环境根本不同,是两个完全不同的容器。这就是对 Dockerfile 构建分层存储的概念不了解所导致的错误。每一个 RUN 都是启动一个容器、执行命令、然后提交存储层文件变更。第一层
RUN cd /tmp 的执行仅仅是当前进程的工作目录变更,一个内存上的变化而已,其结果不会造成任何文件变更。而到第二层的时候,启动的是一个全新的容器,跟第一层的容器更完全没关系,自然不可能继承前一层构建过程中的内存变化。因此如果需要改变以后各层的工作目录的位置,那么应该使用 WORKDIR 指令
构建镜像
将镜像运行为容器

执行 docker run 命令,可以使用 -w 参数覆盖工作目录:

CMD 和 ENTRYPOINT
exec 和 shell 模式
CMD 和 ENTRYPOINT 指令都支持 exec 和 shell 模式的写法,所以要理解 CMD 和 ENTRYPOINT 指令的用法,就得先区分 exec 和 shell 模式。这两种模式主要用来指定容器中的不同进程为 1 号进程。下面我们通过 CMD 指令来学习 exec 模式和 shell 模式的特点。
使用 exec 模式时,容器中的任务进程就是容器内的 1 号进程,看下面的例子:
构建镜像并启动一个容器
然后查看容器中的进程 ID,我们看到运行 top 命令的进程 ID 为 1

exec 模式是建议的使用模式,因为当运行任务的进程作为容器中的 1 号进程时,我们可以通过 docker 的 stop 命令优雅的结束容器。exec 模式的特点是不会通过 shell 执行相关的命令,所以像
$HOME 这样的环境变量是取不到的:构建镜像并启动一个容器

通过 exec 模式执行 shell 可以获得环境变量:
构建镜像并启动一个容器,这次正确取到了
$HOME 环境变量的值
使用 shell 模式时,docker 会以
/bin/sh -c "task command" 的方式执行任务命令。也就是说容器中的 1 号进程不是任务进程而是 bash 进程,看下面的例子:构建镜像并启动一个容器
然后查看容器中的进程 ID,我们看到1 号进程执行的命令居然是
/bin/sh -c top。而我们指定的 top 命令的进程 ID 为 7。这是由 docker 内部决定的,目的是让我们执行的命令或者脚本可以取到环境变量
这意味着此进程在容器中的PID不为1,不能接收Unix信号,因此,当使用docker stop <container>命令停止容器时,此进程接收不到SIGTERM信号。
首先我们先来了解下什么是信号,信号是一种进程间通信(IPC)机制,本质上是内核向进程发送的一种通知,用于告知某个事件发生。当一个信号发送给进程时,进程不会立即强制中断执行,而是会在合适的时机(安全点)处理中断。更准确的描述是:内核记录该信号,当进程从内核态返回用户态时,才会执行对应的信号处理逻辑。当进程收到信号时,有三种处理方式:
自定义处理函数(signal handler):程序可以注册回调函数处理信号;
使用默认行为:不同信号有默认处理方式(如终止、忽略等);
忽略信号(部分信号支持)。
在实际开发中,进程通常会对关键信号进行处理,例如:处理 SIGTERM 用于优雅退出,可以在退出前完成:释放资源(连接、文件句柄),保存状态,完成正在处理的请求。在 Docker 中,容器本质上是一个进程,因此容器的停止与控制,本质就是发送 Unix 信号。当我们使用 docker stop 命令来停止正在运行的容器时,有时可能会使用 docker kill 命令强行关闭容器或者把某个信号传递给容器中的进程。这些操作的本质都是通过从主机向容器发送信号实现主机与容器中程序的交互。比如我们可以向容器中的应用发送一个重新加载信号,容器中的应用程序在接到信号后执行相应的处理程序完成重新加载配置文件的任务。
Docker 的 stop 和 kill 命令都是用来向容器发送信号的,docker stop 与 docker kill 的区别:
stop 优雅停止:stop 命令会首先发送 SIGTERM 信号,并等待应用优雅的结束。如果发现应用没有结束(用户可以指定等待的时间),就再发送一个 SIGKILL 信号强行结束程序。
kill 强制停止:kill 命令默认发送的是 SIGKILL 信号,当然你可以通过 -s 选项指定任何信号。
在容器中,只有 PID = 1 的进程(主进程)才能直接接收到 Docker 发送的信号,这一点非常关键!如果你的应用不是 PID 1,或被一层 shell 包裹(如 sh -c),那么就可能出现收不到信号,无法优雅退出。例如:CMD service nginx start,实际执行是:sh -c "service nginx start",进程关系:
此时 sh 是 PID 1,不是nginx,信号发给了 sh,nginx 收不到,容器无法优雅停止。为了更直观的演示,下面我们通过一个 nodejs 应用演示信号在容器中的工作过程。创建 app.js 文件,内容如下:
这个应用是一个 http 服务器,监听端口 3000,为 SIGINT 和 SIGTERM 信号注册了处理程序。接下来我们将介绍以不同的方式在容器中运行程序时信号的处理情况。创建 Dockerfile 文件,把上面的应用打包到镜像中:
接下来创建镜像:
然后启动容器运行应用程序

打开一个新的终端,此时 node 应用在容器中的进程号为 1

现在我们让程序退出,执行命令:

下面我们再来演示下应用程序不是容器中的 1 号进程,创建一个启动应用程序的脚本文件 app.sh,内容如下:
然后创建 Dockerfile 文件,内容如下:
接下来创建镜像:
然后启动容器运行应用程序:
打开一个新的终端,此时 node 应用在容器中的进程号不再是1:

在给 signal 发送 SIGTERM 信号试试,已经无法退出程序了。在这个场景中,应用程序由 sh 脚本启动,sh 作为容器中的 1 号进程收到了 SIGTERM 信号,但是它没有做出任何的响应动作。退出应用,它们最终都是向容器中的 1 号进程发送了 SIGKILL 信号。很显然这不是我们期望的,我们希望程序能够收到 SIGTERM 信号优雅的退出。

下面我们再来创建另外一个启动应用程序的脚本文件 app2.sh,在脚本中捕获信号,内容如下:
这个脚本文件在启动应用程序的同时,可以捕获发送给它的 SIGTERM 和 SIGUSR1 信号,并为它们添加了处理程序。其中 SIGTERM 信号的处理程序就是向我们的 node 应用程序发送 SIGTERM 信号。然后创建 Dockerfile 文件,内容如下:
接下来创建镜像:
然后启动容器运行应用程序:
此时 node 应用在容器中的进程号也不是 1

打开一个新的终端,发送 SIGTERM 信号优雅退出

CMD 容器启动命令
Docker 不是虚拟机,本质上容器就是一个运行中的进程。既然是进程,那么容器在启动时,必须指定一个要执行的程序(即容器的主进程)及参数。CMD 指令的作用,就是用于指定容器启动时默认执行的命令。该命令可以是一个可执行程序,也可以是一个 shell 命令。一个 Dockerfile 中可以写多个 CMD,但只有最后一个 CMD 指令会生效。
容器的生命周期完全取决于这个进程。一个容器通常只关注一个主进程,这个主进程就是通过 CMD(或 ENTRYPOINT)启动的程序,主进程退出就相当于容器退出。因此,CMD 指定的命令应该是持续前台运行的命令。Docker 不是虚拟机,容器中没有后台服务这一概念。在传统虚拟机中,我们习惯用
systemd 去启动后台服务,一些初学者将 CMD 写为:CMD service nginx start 然后发现容器执行后就立即退出了。甚至在容器内去使用 systemctl 命令结果却发现根本执行不了。这就是因为没有搞明白前台、后台的概念,没有区分容器和虚拟机的差异,依旧在以传统虚拟机的角度去理解容器。对于容器而言,其启动程序就是容器应用进程,容器就是为了主进程而存在的,主进程退出,容器就失去了存在的意义,从而退出,其它辅助进程不是它需要关心的东西。CMD service nginx start 会被理解为 CMD [ "sh", "-c", "service nginx start"],因此主进程实际上是 sh。那么当 service nginx start 命令结束后,sh 也就结束了,sh 作为主进程退出了,自然就会令容器退出。正确的做法是直接执行 nginx 可执行文件,并且要求以前台形式运行。比如:CMD ["nginx", "-g", "daemon off;"]在 Dockerfile 中,CMD(以及 ENTRYPOINT)有两种常见写法:
exec格式:这类格式在解析时会被解析为 JSON 数组,因此一定要使用双引号,而不要使用单引号。不会有额外的sh进程。
shell格式:会通过 shell 默认
/bin/sh -c 执行,支持:环境变量、管道、重定向、shell表达式。比如 CMD echo $HOME 在实际执行中,会将其变更为 CMD [ "sh", "-c", "echo $HOME" ] ,这就是为什么我们可以使用环境变量的原因,因为这些环境变量会被 shell 进行解析处理。参数列表格式:提供给 ENTRYPOINT 命令的默认参数
当容器启动时,CMD 是否生效,取决于 docker run 是否指定了运行命令。如果用户启动容器时用
docker run xxx 指定运行的命令,则会覆盖 CMD 指定的命令。如果 docker run 没有指定执行命令,且 Dockerfile 中也没有 ENTRYPOINT, 容器启动时会执行 CMD 指定的默认命令。以 ubuntu 镜像为例:
未指定命令,执行默认 CMD ,通常是 /bin/bash ,进入 bash 终端如果指定运行命令,实际执行的是 cat /etc/os-release,用该命令替换了默认的 /bin/bash,容器执行完命令后立即退出,并输出系统版本信息
多个 CMD 只有最后一个生效
构建镜像并启动一个容器,发现多个CMD指令,只有最后一个CMD指令生效

ENTRYPOINT 入口点
ENTRYPOINT 指定容器启动时运行的入口程序。与 CMD 不同,ENTRYPOINT 定义的命令不会被 docker run 的参数覆盖,而是接收这些参数。ENTRYPOINT语法格式:
ENTRYPOINT 指令的使用分为两种情况,一种是独自使用,另一种和CMD指令配合使用。如果 Dockerfile 同时存在 ENTRYPOINT 和 CMD:
CMD 会作为 ENTRYPOINT 的默认参数;
如果 docker run 没有提供参数,则使用 CMD 作为参数;
如果 docker run 提供了参数,则会覆盖 CMD,作为 ENTRYPOINT 的新参数。
CMD指令作为ENTRYPOINT的默认参数,这时CMD指令不是一个完整的可执行命令,仅仅是参数部分。ENTRYPOINT指令只能使用JSON方式指定执行命令,而不能指定参数
构建镜像
使用默认参数

覆盖参数

如果 Dockerfile 只有 ENTRYPOINT,docker run 后面的参数会作为 ENTRYPOINT 的参数进行追加
构建镜像并启动一个容器,发现 docker run 后面的参数会作为 ENTRYPOINT 的参数进行追加

每个 Dockerfile 中只能有一个 ENTRYPOINT,当指定多个时,只有最后一个生效
构建镜像并启动一个容器,发现ENTRYPOINT指令只有最后一个生效

ENTRYPOINT 在运行时也可以替代,不过比 CMD 要略显繁琐,需要通过 docker run 的
--entrypoint 参数来指定
在学习 Dockerfile 时,一个非常常见的误区是:当 Dockerfile 中同时存在 CMD 和 ENTRYPOINT,并且 CMD 是一个完整的可执行命令时,很多人会误以为二者会互相覆盖,只有最后一个生效。这种说法是错误的。实际上,CMD 和 ENTRYPOINT 并不是覆盖关系,而是组合关系:
ENTRYPOINT 是 exec 格式,CMD 会作为 ENTRYPOINT 的参数
ENTRYPOINT 是 shell 格式,CMD 被忽略
我们先来看一下 shell 格式,创建Dockerfile
构建镜像并启动一个容器,发现CMD命令被完全忽略

ENTRYPOINT 是 shell 格式,CMD 被完全忽略了。Docker 会把它解析为
/bin/sh -c "ls -l",/bin/sh -c "ls -l"这里已经是一个完整命令了,Docker 无法再把 CMD 拼进去。下面我们再来演示一下 ENTRYPOINT 是 exec 格式,创建Dockerfile构建镜像并启动一个容器,发现CMD命令被作为 ENTRYPOINT 的参数

COPY 和 ADD
Build 上下文
在学习 Dockerfile 时,很多人会忽略一个非常关键的概念:
Build Context(构建上下文)。当我们执行 docker build 命令时,会指定一个路径,这个路径就是 Build Context。在镜像 build 过程中可以引用上下文中的任何文件,比如我们要介绍的 COPY 和 ADD 命令,就可以引用上下文中的文件。比如
docker build -t testx . 命令中的 . 表示 build 上下文为当前目录,当前目录下的所有文件都会作为构建上下文。当然我们可以指定一个目录作为上下文,比如下面的命令:COPY 和 ADD 命令不能拷贝上下文之外的本地文件,如果要把本地的文件拷贝到镜像中,那么本地的文件必须是在上下文目录中的文件。其实这一点很好解释,因为在执行 build 命令时,docker 客户端会把上下文中的所有文件发送给 docker daemon。考虑 docker 客户端和 docker daemon 不在同一台机器上的情况,build 命令只能从上下文中获取文件。如果我们在 Dockerfile 的 COPY 和 ADD 命令中引用了上下文中没有的文件,就会收到类似下面的错误:

我们可以通过指定 /home/qianshuai/ 目录为 build 上下文,默认情况下 docker 会使用在上下文的根目录下找到的 Dockerfile 文件。当然,也可以手动指定,修改Dockerfile
构建镜像
现在运行并进入容器,如下所示,根目录下多了一个 app 目录,并且 app 目录内只有源文件夹下的 blog.txt 文件

COPY
当你只是想把宿主机上的文件或目录拷贝到镜像中,COPY 指令是最合适的选择。语法格式如下:
源路径:必须位于构建上下文(build context)中,通常是 Dockerfile 所在目录或其子目录,不能访问上下文目录之外的文件。如果源路径是目录,则会递归复制目录内部的文件和子目录,但不会复制目录本身。
目标路径:可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 WORKDIR 指令来指定)。如果目标路径中不存在某些目录,会递归自动创建父目录,无需手动创建。目标路径应以 / 结尾,表示复制到目录中。COPY 会尽量保留源文件的元数据,包括读、写、执行权限、修改时间等,一些常见的用法示例,如下:
下面我们来演示几个示例:
源路径和目标路径都是目录
在 Dockerfile 的规则中,如果目标路径最后跟
/ 符号,那么就代表目录,否则就是文件。如果目标目录不存在,那么会新建这个目录。下面例子使用 alpine 镜像作为示例,运行此镜像并查看其根目录,如下所示并没有一个 app 的目录,那么接下的例子中目标路径名称都用 app 来表示修改 Dockerfile,使其复制 blog 目录到 app 中
构建镜像
现在运行并进入容器,如下所示,根目录下多了一个 app 目录,并且 app 目录内只有源文件夹下的 blog.txt 文件

如果源路径是目录,则会递归复制目录内部的文件和子目录,但不会复制目录本身。另外文件自身的文件系统元数据也将复制过去,比如说文件权限等。
源路径是目录而目标路径是文件
修改 Dockerfile 为下面内容,只是把目标路径后的
/ 去掉构建镜像
运行并进入容器,发现和目标路径是不是目录并没有区别。也就是说如果目标路径不包含
/,那么将会把目标路径视作普通文件,然后会将源路径的所有文件写入目标路径。这里目标路径虽然是文件但也被作为目录构建了
虽然上述可以使用,但是不推荐使用,因为这个语义并不明确,而且在多个源文件的情况下会报错,接下来,将 blog 目录下的 blog.txt 和 README.md 复制到镜像的 app 目录下
不通版本构建结果不同,老版本构建会报错,提示目录路径不是目录,如果构建成功了,可以运行查看,发现 app 是一个文件

修改 Dockerfile 为下面的内容,将上面的例子改一下,将目标路径后加上
/,使其变成语义上的目录运行并进入容器,发现文件复制到了 app 目录

源路径和目标路径都是文件
修改 Dockerfile 为下面的内容,复制 blog.txt 文件到 app ,不过再此之前我们修改下权限属性
运行并进入容器,发现 blog.txt 文件被重命名为了 app,按照文档的所说,只是将源路径的文件内容写入了目标路径文件,仍旧保留了其文件元数据

ADD
ADD 可以看作是 COPY 的增强版,它不仅可以完成 COPY 的所有功能,还额外提供了两类实用功能:
自动解压压缩包:当源路径是支持的压缩文件(如 .tar, .tar.gz, .tgz, .tar.bz2, .tar.xz 等)时,ADD 会自动解压到目标路径,行为类似 tar -x。注意:普通文件不会被解压,只有支持的压缩包才会解压。COPY 指令只是复制不解压。
远程 URL 支持:源路径可以是一个远程文件的 URL,Docker 会在构建镜像时自动下载该文件到目标路径。下载的文件默认权限为 600,如果需要其他权限,需要使用额外的
RUN chmod 指令进行修改。一些常见的用法示例,如下:
下面我们来演示几个示例:
ADD 指令源路径是网络地址,修改 Dockerfile 文件,这里选择文件大小比较小的 vuejs 最新源码打包文件作为源路径
然后进行镜像构建,可以看到 ADD 命令进行了文件下载,注意:这个是 COPY 命令不支持的

查看镜像构建的结果,如下所示,默认情况下,下载的文件的权限为 600,如果这不是想要的权限,那么可以再加一层 RUN 指令进行修改

ADD 源路径是打包压缩文件,使用 wget 命令下载 vue.tar.gz 文件
修改 Dockerfile 文件,并构建镜像
构建并查看文件内容,这里 Docker 已经解压了这个文件

根据 Docker 官方 <a href="https://docs.docker.com/build/building/best-practices/#add-or-copy
">Dockerfile 最佳实践文档</a>,在选择 COPY 和 ADD 时,推荐遵循以下原则:优先使用 COPY,COPY 的语义非常明确,仅用于复制文件或目录。由于功能单一,构建缓存更容易被利用,构建速度通常更快。仅在特定场景使用 ADD,ADD 除了复制文件,还会自动解压支持的压缩包或下载远程文件,因此行为更复杂,语义不如 COPY 清晰。最适合使用 ADD 的场景是:需要自动解压缩压缩包。
由于 ADD 的功能复杂,可能会导致构建缓存失效,从而使镜像构建速度变慢。因此在日常文件复制中,尽量使用 COPY,只有在必须解压或拉取远程文件时才使用 ADD。
ENV 设置环境变量
ENV 用于定义镜像和容器运行时的环境变量,定义后的变量可以在 Dockerfile 后续指令中使用,也会在容器运行时保留。并可在运行时被覆盖。基本用法:
下面我们来构建一个 Dockerfile 进行演示
构建镜像
容器运行时的表现


容器启动时也可以通过命令行覆盖定义的ENV变量

容器启动后可以通过
docker inspect查看这个环境变量
ARG 构建参数
ARG 和 ENV 都可以用于定义变量,但它们的作用范围和使用场景不同,ARG 是构建时变量,ENV 是运行时变量,二者作用范围完全不同:
ARG 构建参数:只在镜像构建阶段(docker build)生效,不会保留在最终运行的容器中。可以通过 docker build --build-arg 传入参数,常用于控制构建过程(如版本号、基础镜像选择等)。虽然 ARG 不会出现在容器运行时环境中,但其值仍可能通过 docker history 等方式被查看,因此不应用于存储密码、密钥等敏感信息。
ENV 环境变量:会保留在镜像中,并在容器运行时可用。可被应用程序直接读取,常用于运行时配置(如端口、环境配置等)。
基本语法:
ARG 指令如果在 FROM 指令之前指定,那么只能用于 FROM 指令中
构建镜像,发现ARG只能用于 FROM 指令中

使用上述 Dockerfile 会发现无法输出
${IMAGES} 变量的值,要想正常输出,你必须在 FROM 之后再次指定 ARG构建镜像

ARG 指令只在镜像构建阶段(docker build)生效,不会保留在最终运行的容器中。下面我们来运行容器,查看 ARG 指令定义的变量,发现输出结果为空

也可以在构建时指定ARG

对于多阶段构建,每个阶段都需要重新声明,尤其要注意这个问题
USER 指定当前用户
USER指令用来指定运行容器的用户名或 UID,后续的 RUN 也会使用指定用户执行命令。这个用户必须是事先建立好的,否则无法切换。如果没有指定 USER,默认是 root 身份执行,以非 root 用户运行容器是最重要的安全实践之一
- 链接:www.qianshuai.cn/article/docker/images
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。

镜像制作





