首页  /  云原生  /  正文

Dockerfile 入门:指令、构建与 CMD/ENTRYPOINT

云原生 2026-10-02📖 22 分钟👁 —
🐳 云原生 · Docker第 7 / 9 页123456789📖 完整导航 →

一、Dockerfile 是什么

Dockerfile 就是构建 Docker 镜像的那个文件,说穿了是一段脚本,整个流程就四步:

  1. 编写文件
  2. build 构建镜像
  3. run 运行
  4. push 发布到仓库(私有/公有)

它跟 shell 脚本不是一回事,写法是声明式的:

DockerfileShell 脚本(Bash)
性质声明式(Declarative)命令式(Imperative)
作用描述镜像的构建步骤和最终状态描述执行一系列操作的指令序列
语法指令(FROM、RUN、COPY 等)+ 参数命令、变量、条件判断、循环等
执行环境由 Docker 引擎解析,逐层构建镜像由 Shell 解释器(如 Bash)执行

三个概念别搞混:

为什么非得写 Dockerfile?前面讲过镜像是一层层共用的(联合文件系统),改一个容器再用 docker commit 提交也能做出镜像,但要反复提交,越堆越臃肿,构建一次比一次慢。Dockerfile 就是把这些动作写成一个脚本,一次性构建出来:

Dockerfile 是脚本,build 出镜像,run 起容器,中间还能 commit、tag、push

所以它是这个体系里的固定套路:先写文件,再 docker build 出镜像,最后 docker run 成容器:

构建三步骤:编写 Dockerfile、docker build 构镜像、docker run 跑容器

在 Docker Hub 上的镜像有 tags 功能,点开之后能看到历史版本,还能跳转到 GitHub 的位置,顺便看它的 Dockerfile 代码 —— 想抄官方镜像是怎么写的,直接去翻就行。

写 Dockerfile 本身不难,它是一门面向开发的语言,会构建镜像就行;学会之后才算真正明白 Docker 是什么。

文件名最好写成 dockerfile01 这种,容易辨识。

二、四条基本规则

  1. 每个保留关键字(指令)必须都是大写的字母
  2. 从上到下顺序执行
  3. # 表示注释
  4. 每个指令都会创建提交一个新的镜像层
镜像是一层层叠起来的:最底下 bootfs,上面是 rootfs 基础镜像,再往上叠 jdk、tomcat 这些镜像层,最顶上是可写的容器层

第 4 条是关键:一条 RUN 就是一层,层数一多镜像就臃肿,后面讲构建优化的时候全是在这上面做文章。

四条规矩落到图上就是这么回事:

Dockerfile 指令的四条规矩:必须大写、按顺序执行、井号是注释、每条指令生成一层

构建的时候 docker 干了什么

docker build 读一遍 Dockerfile,其实是这五步在循环:跑一个容器、执行一条指令、把结果提交成一个镜像层、再基于这个新镜像跑下一个容器,直到所有指令走完:

docker 执行 Dockerfile 的五步流程:跑容器、执行指令、提交镜像层、再基于新镜像跑容器

这也解释了"每条指令一层"是怎么来的:每执行一条就要 commit 一次,层数自然就上去了。

三、常用指令

FROM				# 基础镜像 centos ubuntu
MAINTAINER 	# 作者,姓名和邮箱是国际标准
						# main-tainer 主要持有者
      
RUN					# 镜像构建需要运行的命令
ADD					# 步骤:tomcat 镜像,tomcat压缩包写在这
WORKDIR			# 工作目录
VOLUME			# 挂载目录
EXPOSE			# 指定开放端口(不写就通过 -p 暴露端口)

CMD					# 指定容器启动时运行的命令,比如echo
						# 只有最后一个生效,可替代


ENTRYPOINT	# 指定容器启动时运行的命令,可以追加命令			
						# entry point 入口点

ONBUILD			# 构建一个被继承的 dockerfile 会触发该指令(了解即可)
COPY				# 复制,类似ADD,将文件拷贝到镜像中
ENV					# 设置环境变量(environment)

保留字按生效阶段可以分成三类(构建时用、构建和运行都要、运行时用):

Dockerfile 保留字按 BUILD、Both、RUN 三个阶段分类

日常真正要盯的是 ADD、COPY、CMD、ENTRYPOINT 这四个,剩下的用到再回来查。想看别人是怎么写的,直接翻 tomcat 官方的 Dockerfile 最有参考价值(https://github.com/docker-library/tomcat):

tomcat 官方 Dockerfile 里各指令出现的次数,CMD 用了 11 次

这里面命令很多都很相似,比如 ADD 和 COPY、CMD 和 ENTRYPOINT,光看说明记不住,对比着做一遍测试最清楚。

ADD 和 COPY

两个都是把宿主机的文件拷进镜像,区别就在自动解压这一条上:

ADD 拷文件并自动处理 URL、解压 tar;COPY 只是把文件复制到镜像里的目标路径

ADD 的三个本事一张表就能说完 —— 复制本地文件、自动解压、还能直接给个 URL:

ADD 的说明:把宿主机目录下的文件拷进镜像,会自动处理 URL 和解压 tar 压缩包
ADD 的三个功能表格:复制本地文件、自动解压 tar 包、通过 URL 下载

要留意的是:只有本地的压缩包才自动解压,URL 下载回来的不会解压,而且权限是 600。我平时拷东西基本都用 ADD,因为它顺手;只想老老实实复制、不想被自动解压的时候才用 COPY。

四、docker build 的三个参数

docker build -f dockerfile01 -t zzz-usr/centos .

顺便提一句,用官方的构建方式会把文件写成 Dockerfile(首字母大写、无后缀),那时候 build 会自动寻找该文件,不需要手动 -f。

镜像名是有格式的:[仓库地址/][用户名/]镜像名[:标签]。以 / 开头会直接报错:

root@VM-0-9-ubuntu:/home/docker.v# docker build -f dockerfile01 -t /zzz-usr/centos .
[+] Building 0.0s (0/0)                                                                                         docker:default
ERROR: failed to build: invalid tag "/zzz-usr/centos": invalid reference format
# 这个报错是镜像名不合法
# 镜像名(tag)不能以 / 开头

把 / 去掉就正常了,下面是完整的一次构建输出:

root@VM-0-9-ubuntu:/home/docker.v# docker build -f dockerfile01 -t zzz-usr/centos .
[+] Building 0.2s (5/5) FINISHED                                                                                docker:default
 => [internal] load build definition from dockerfile01                                                                    0.0s
 => => transferring dockerfile: 119B                                                                                      0.0s
 => WARN: JSONArgsRecommended: JSON arguments recommended for CMD to prevent unintended behavior related to OS signals (  0.0s
 => WARN: MultipleInstructionsDisallowed: Multiple CMD instructions should not be used in the same stage because only th  0.0s
 => WARN: JSONArgsRecommended: JSON arguments recommended for CMD to prevent unintended behavior related to OS signals (  0.0s
 => [internal] load metadata for docker.io/library/centos:latest                                                          0.0s
 => [internal] load .dockerignore                                                                                         0.0s
 => => transferring context: 2B                                                                                           0.0s
 => [1/1] FROM docker.io/library/centos:latest@sha256:a27fd8080b517143cbbbab9dfb7c8571c40d67d534bbdee55bd6c473f432b177    0.0s
 => => resolve docker.io/library/centos:latest@sha256:a27fd8080b517143cbbbab9dfb7c8571c40d67d534bbdee55bd6c473f432b177    0.0s
 => exporting to image                                                                                                    0.1s
 => => exporting layers                                                                                                   0.0s
 => => exporting manifest sha256:ad8ac16c8808bcb38bd516b36d6442def438741a3699cb61a82baad70f21a1c0                         0.0s
 => => exporting config sha256:2771b5374df0a3d1fec5d343805c3291ee35f76cf68d2b731a421cb308eae93f                           0.0s
 => => exporting attestation manifest sha256:ec9d6a6990819ca284f54cc22866360aadfd23b5789b3891b2f50bcd8ce82083             0.0s
 => => exporting manifest list sha256:0203f7bd4f3db404f07dad01111bf4dc53bd7c59974e76e271c59758a45b50a3                    0.0s
 => => naming to docker.io/zzz-usr/centos:latest                                                                          0.0s
 => => unpacking to docker.io/zzz-usr/centos:latest                                                                       0.0s

 3 warnings found (use docker --debug to expand):
 - JSONArgsRecommended: JSON arguments recommended for CMD to prevent unintended behavior related to OS signals (line 5)
 - MultipleInstructionsDisallowed: Multiple CMD instructions should not be used in the same stage because only the last one will be used (line 5)
 - JSONArgsRecommended: JSON arguments recommended for CMD to prevent unintended behavior related to OS signals (line 6)

这次构建用的 Dockerfile 就三行指令,值得单独看一眼:

FROM centos

VOLUME ["volume01","volume02"]

CMD echo "---end---"
CMD /bin/bash

对上号了:写了两个 CMD,构建时就提示第二个会覆盖第一个;CMD echo 这种 shell 格式的写法也被建议改成 JSON 数组写法。这两条警告正好是后面两节要讲的东西。

五、动手:构建自己的 centos

Dockerfile 写在 /home/dockerfile 目录中。官方的 centos 是阉割过的,很多命令都没有,我想自己补一些进去。

先解决 yum 的两个坑。当时安装卡在 yum 上,怀疑是 OS 的 DNS 解析失败 —— centos 里的 /etc/resolv.conf 配了无效 DNS,而镜像只读又改不了,所以退一步配置全局的 docker DNS:

echo '{"dns": ["8.8.8.8", "114.114.114.114"]}' > /etc/docker/daemon.json
systemctl restart docker
# 当时安装卡在yum,可能是os的DNS解析失败
# 因为centos配置的 /etc/resolv.conf 配置了无效DNS而镜像只读无法修改
# 所以可以配置全局dockerDNS,但是这部分是网络的,这个问题先放着后续了解

另一个坑是 yum 源太慢,默认官方源换成阿里云镜像:

# yum卡住了,配置yum源,默认官方源改为指定镜像
# sed -i 's/旧内容/新内容/g' 文件名
RUN sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-*.repo && \
    sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://mirrors.aliyun.com|g' /etc/yum.repos.d/CentOS-*.repo && \
    yum makecache && yum update -y && yum install -y vim net-tools

完整的 Dockerfile:

FROM centos
MAINTAINER zzz<1212@qq.com>

ENV MYPATH /usr/local
WORKDIR $MYPATH

RUN yum update -y && yum install -y vim net-tools

# 安装了vim和net-tools两个包
EXPOSE 80
# CMD只能执行一个
CMD echo $MYPATH && echo "---end---" && /bin/bash

这里 RUN 是在镜像内部运行的(因为镜像里就是一套 centos 系统,用 yum 装包),ENV + WORKDIR 把默认工作目录设成 /usr/local,EXPOSE 80 只是声明,真正映射端口还得 run 的时候用 -p。

构建:

docker build -f dockerfile-os -t zzz/centos:0.1 .
# build 不默认在当前目录,需要指定当前目录,写	. 即可

跑起来验证一下装的命令和默认目录,都正常:

root@VM-0-9-ubuntu:/home/dockerfile# docker run -it zzz/centos:0.1 /bin/bash
[root@9c0b2b874ebf /]# pwd
/usr/local
# 镜像名字写镜像ID也可以,docker images 可以看到镜像ID
# 成功

想看这个镜像是怎么做出来的,用 docker history 看镜像 ID 或镜像名:

root@VM-0-9-ubuntu:/home/dockerfile# docker history b8a8cf9df2e3
IMAGE          CREATED          CREATED BY                                      SIZE      COMMENT
b8a8cf9df2e3   19 minutes ago   CMD ["/bin/sh" "-c" "/bin/bash"]                0B        buildkit.dockerfile.v0
<missing>      19 minutes ago   CMD ["/bin/sh" "-c" "echo \"---end---\""]       0B        buildkit.dockerfile.v0
<missing>      19 minutes ago   CMD ["/bin/sh" "-c" "echo $MYPATH"]             0B        buildkit.dockerfile.v0
<missing>      19 minutes ago   EXPOSE [80/tcp]                                 0B        buildkit.dockerfile.v0
<missing>      19 minutes ago   RUN /bin/sh -c sed -i 's/mirrorlist/#mirrorl…   349MB     buildkit.dockerfile.v0
<missing>      19 minutes ago   WORKDIR /                                       0B        buildkit.dockerfile.v0
<missing>      19 minutes ago   ENV MYPATH=/usr/local                           0B        buildkit.dockerfile.v0
<missing>      19 minutes ago   MAINTAINER zzz<1212@qq.com>                     0B        buildkit.dockerfile.v0
<missing>      4 years ago      /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B        
<missing>      4 years ago      /bin/sh -c #(nop)  LABEL org.label-schema.sc…   0B        
<missing>      4 years ago      /bin/sh -c #(nop)  ADD file:805cb5e15fb6e0bb0…   256MB  

从上往下读,每一行就是一层:一条 CMD 一层、一个 ENV 一层,连 EXPOSE 这种不占空间的也是一层(SIZE 是 0B)。只有 RUN 那层是 349MB,因为真的装东西了。

再做一个带 vim、ifconfig、jdk8 的 centos

同一个思路再走一遍。官方镜像太干净,实际用起来还想要 jdk8,需求就三条:

需求:Centos7 镜像要具备 vim、ifconfig、jdk8

jdk 可以去 Oracle 官网下,Linux 那一栏挑 x64 的压缩包:

Oracle 官网 JDK8 下载页,Linux 页签里的 x64 Compressed Archive

官网太慢就用镜像站,选好版本跟在目录后面直接 wget:

镜像站的目录列表,直接拿到 jdk-8u221-linux-x64.tar.gz 的下载地址

先把 jdk 压缩包和 Dockerfile 放进同一个目录(ADD 写的是相对路径,包必须和 Dockerfile 在一起),然后写文件:

FROM centos
MAINTAINER zzz<zzz@163.com>

ENV MYPATH /usr/local
WORKDIR $MYPATH

#安装vim编辑器
RUN yum -y install vim
#安装ifconfig命令查看网络IP
RUN yum -y install net-tools
#安装java8及lib库
RUN yum -y install glibc.i686
RUN mkdir /usr/local/java
#ADD 是相对路径jar:把jdk-8u202-linux-x64.tar.gz添加到容器中
ADD jdk-8u202-linux-x64.tar.gz /usr/local/java/
#配置java环境变量
ENV JAVA_HOME /usr/local/java/jdk1.8.0_202
ENV JRE_HOME $JAVA_HOME/jre
ENV CLASSPATH $JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JRE_HOME/lib:$CLASSPATH
ENV PATH $JAVA_HOME/bin:$PATH

EXPOSE 80

CMD echo $MYPATH
CMD echo "success---ok"
CMD /bin/bash

构建和验证:

# 在当前目录使用Dockerfile构建镜像,最后有个 . 表示在当前路径下
docker build -t centosjava8:1.5 .

# 跑起来试一下 vim、ifconfig、java -version
docker run -it centosjava8:1.5 bash

这次构建在我这儿是失败的,因为 centos 的 yum 源早就失效、官方不维护了,appstream 仓库直接 404:

docker build 失败:装 vim 时 appstream 仓库报 404

Dockerfile 本身没问题,跟前面那次 centos 构建踩的是同一个坑 —— 换台有源可用的机器、或者换成 ubuntu 底包就能过。上面三条 CMD 也正好印证了"多个 CMD 只有最后一个生效":真正生效的只有最后的 /bin/bash,前面两句等于白写。要是当初写的是 ENTRYPOINT + CMD,docker run 后面再跟参数就只是换个参数,而不是把入口整个换掉 —— 这正是下一节要做的实验。

六、CMD 和 ENTRYPOINT 的区别

这一节是重点,光看文档记不住,直接做实验。

先把 CMD 的几个注意点过一遍:一个 Dockerfile 里写多个 CMD 只有最后一个生效,而且 docker run 后面跟的参数会把它整个替换掉:

CMD 的注意点:多个 CMD 只有最后一个生效,而且会被 docker run 后面的参数替换掉

格式和 RUN 类似,也是三种写法:

CMD 的三种格式:shell 格式、exec 格式、以及配合 ENTRYPOINT 的参数列表格式

它和 RUN 最容易混,一句话说清:CMD 在 docker run 的时候运行,RUN 在 docker build 的时候运行:

CMD 在 docker run 时运行,RUN 在 docker build 时运行

拿 tomcat 官方镜像做例子,它 Dockerfile 的最后两行是:

tomcat 官方 Dockerfile 的最后两行:EXPOSE 8080,CMD 是 catalina.sh run

正常 docker run -it -p 8080:8080 tomcat 会带着这个 CMD 起来,服务是正常的。可要是我手贱在后面加个参数:

# 正常运行一个容器,会启用dockerfile的CMD命令
docker run -it -p 8080:8080 tomcat

# 如果后面加了参数,就会覆盖掉CMD,导致启动失败
docker run -it -p 8080:8080 tomcat /bin/bash

CMD 就被整个换掉了,相当于变成了 /bin/bash 在跑:

把 CMD 换成 /bin/bash 之后,CMD 被 run 的参数整个顶替

这时候容器看着是起来了,tomcat 页面却打不开 —— 因为容器的入口已经不是启动 tomcat 了。

1. 先测 CMD

FROM centos
CMD ["ls","-a"]

构建:

docker build -f centos-cmd -t zzz/centos-cmd .

直接 run 就会执行 CMD 里的命令:

root@VM-0-9-ubuntu:/home/dockerfile# docker run zzz/centos-cmd
.
..
.dockerenv
bin
dev
etc
home
lib
lib64
lost+found
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var

2. 在 run 后面追加命令 -l

root@VM-0-9-ubuntu:/home/dockerfile# docker run zzz/centos-cmd -l
docker: Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: exec: "-l": executable file not found in $PATH

Run 'docker run --help' for more information
# CMD 的情况下,-l 替换了CMD的["ls","-a"] 命令,-l不是命令报错了
# -l 作为参数和CMD是二选一的,-l 是run命令的参数
# 任何情况CMD都只会执行最后一个

# 下面就可以,正确的命令覆盖CMD就行
root@VM-0-9-ubuntu:/home/dockerfile# docker run zzz/centos-cmd ls -al
total 56
drwxr-xr-x   1 root root 4096 Aug  2 06:59 .
drwxr-xr-x   1 root root 4096 Aug  2 06:59 ..
-rwxr-xr-x   1 root root    0 Aug  2 06:59 .dockerenv
lrwxrwxrwx   1 root root    7 Nov  3  2020 bin -> usr/bin
drwxr-xr-x   5 root root  340 Aug  2 06:59 dev
drwxr-xr-x   1 root root 4096 Aug  2 06:59 etc
drwxr-xr-x   2 root root 4096 Nov  3  2020 home
lrwxrwxrwx   1 root root    7 Nov  3  2020 lib -> usr/lib
lrwxrwxrwx   1 root root    9 Nov  3  2020 lib64 -> usr/lib64
drwx------   2 root root 4096 Sep 15  2021 lost+found
drwxr-xr-x   2 root root 4096 Nov  3  2020 media
drwxr-xr-x   2 root root 4096 Nov  3  2020 mnt
drwxr-xr-x   2 root root 4096 Nov  3  2020 opt
dr-xr-xr-x 226 root root    0 Aug  2 06:59 proc
dr-xr-x---   2 root root 4096 Sep 15  2021 root
drwxr-xr-x  11 root root 4096 Sep 15  2021 run
lrwxrwxrwx   1 root root    8 Nov  3  2020 sbin -> usr/sbin
drwxr-xr-x   2 root root 4096 Nov  3  2020 srv
dr-xr-xr-x  13 root root    0 Jul 30 10:25 sys
drwxrwxrwt   7 root root 4096 Sep 15  2021 tmp
drwxr-xr-x  12 root root 4096 Sep 15  2021 usr
drwxr-xr-x  20 root root 4096 Sep 15  2021 var

一句话总结 CMD:run 后面给的东西是直接替换掉 CMD 的,所以 -l 会被当成命令去找,找不到就报错;写完整的 ls -al 才对。

3. 再测 ENTRYPOINT

ENTRYPOINT 就是为了解决这个"被覆盖"的问题而存在的。它和 CMD 搭配起来用,CMD 就成了参数:

ENTRYPOINT 的命令格式,指定后 CMD 的内容会作为参数传给 ENTRYPOINT

合起来就是 <ENTRYPOINT> "<CMD>" 这个意思。官方给的例子最清楚 —— 假设用 Dockerfile 构建了 nginx:test 镜像,ENTRYPOINT ["nginx", "-c"] 是定死的,CMD ["/etc/nginx/nginx.conf"] 是可变参数:

nginx:test 的例子:不传参走 Dockerfile 里写的配置文件,传参就替换掉配置文件路径

回到实验上:ENTRYPOINT 是直接在最后追加命令,不需要替换的过程,同样只执行最后一个:

# 构建
FROM centos
ENTRYPOINT ["ls","-l"]
# 创建
docker build -f centos-ent -t zzz/centos-ent .

# run追加测试
# 相当于把 -a 追加到 ENTRYPOINT ["ls","-l"]
# 最终是 ENTRYPOINT ["ls","-l","-a"]
root@VM-0-9-ubuntu:/home/dockerfile# docker run zzz/centos-ent -a
total 56
drwxr-xr-x   1 root root 4096 Aug  2 07:05 .
drwxr-xr-x   1 root root 4096 Aug  2 07:05 ..
-rwxr-xr-x   1 root root    0 Aug  2 07:05 .dockerenv
lrwxrwxrwx   1 root root    7 Nov  3  2020 bin -> usr/bin
drwxr-xr-x   5 root root  340 Aug  2 07:05 dev
...

(后面还是平时那一屏根目录,这里不重复贴了。)

-a 追加成功了,这一点跟 CMD 正好相反:

追加参数时的行为结果
CMD ["ls","-l"]run 镜像 -a 整段替换掉 CMD把 -a 当命令执行,报 executable file not found
ENTRYPOINT ["ls","-l"]run 镜像 -a 追加到 ENTRYPOINT 后面实际执行 ls -l -a,正常出结果

所以想做一个"固定入口、允许带参数"的镜像就用 ENTRYPOINT,想留一个能被整个覆盖的默认命令就用 CMD;两个一起写的时候,CMD 就成了 ENTRYPOINT 的默认参数。另外注意 docker run 后面追加的命令,会覆盖掉 CMD,但不会覆盖 ENTRYPOINT。


到这里 Dockerfile 的基础就差不多了:会写指令、会 build、知道每条指令是一层、能分开 CMD 和 ENTRYPOINT。下一页继续看实战镜像和多阶段构建、构建优化这些进阶内容。