Dockerfile 构建镜像(十)
这篇文章介绍了Dockerfile的构建镜像过程,包括构建步骤、镜像结构图及Dockerfile指令。该文还详细解析了Dockerfile的基本结构和各个指令的用法,并提供了实战案例。通过DockerFile可以直接构建镜像,其中FROM指令是必须的,其他指令包括MAINTAINER、RUN、ADD、COPY、LABEL、ARG、ONBUILD、CMD、EXPOSE、ENTRYPOINT、ENV、WORKDIR、USER、VOLUME等。最后提供了参考文献。
Dockerfile 是一个用来构建镜像的文本文件,文本内容包含了一条条构建镜像所需的指令和说明。
通过如下架构图可以看出通过 DockerFile 可以直接构建镜像:
命令 | 运行阶段 | 描述 |
---|---|---|
FROM | BUILD | 指定基础镜像,必须为第一个命令。 |
MAINTAINER | BUILD | 构建指令,用于将镜像制作者相关的信息写入到镜像中。 |
RUN | BUILD | 用于在镜像容器中执行命令。 |
COPY | BUILD | 拷贝文件,不会自动解压文件,也不能访问网络资源。 |
ADD | BUILD | 将本地文件添加到容器中,tar类型文件会自动解压(网络压缩资源不会被解压),可以访问网络资源,类似wget。 |
LABEL | BUILD | 用于为镜像添加元数据 |
ARG | BUILD | 用于指定传递给构建运行时的变量(给dockerfile传参),相当于构建镜像时可以在外部为里面传参 |
ONBUILD | BUILD | 用于设置镜像触发器 |
CMD | RUN | 指定启动容器时执行的命令,每个 Dockerfile 只能有一条 CMD 命令。 |
EXPOSE | RUN | 将容器中的端口映射成宿主机器中的某个端口。 |
ENTRYPOINT | RUN | 类似于CMD指令,配置容器启动后执行的命令(容器的初始化命令)。 |
ENV | RUN | 设置环境变量。 |
WORKDIR | BOTH | bulid以及run阶段大家工作的场所(CMD以及RUN执行的工作目录)。 |
USER | BOTH | 指定运行容器时的用户名或UID,默认是root,后续的RUN也会使用指定用户。 |
VOLUME | RUN | 运行阶段容器为了持久化存储以正常的文件或者目录的形式挂载于宿主机上(持久化存储,容器挂了数据还在,用户存储有状态的数据)。 |
#
表示注释FROM:指定基础镜像,并且必须是第一条指令。如果不以任何镜像为基础,那么写法为:FROM scratch
。同时意味着接下来所写的指令将作为镜像的第一层开始。
语法:
bash# 格式:
FROM <image>
FROM <image>:<tag>
FROM <image>@<digest>
# 示例:
FROM mysql:5.6
# 注:tag或digest是可选的,如果不使用这两个值时,会使用 latest 版本的基础镜像
MAINTAINER:维护者信息
语法:
bash格式: MAINTAINER <name> 示例: MAINTAINER xuechao MAINTAINER xxx@163.com MAINTAINER xuechao <xxx@163.com>
RUN:构建镜像时执行的命令。
RUN命令有两种格式,一种是 shell 格式,还有一种为 exec 格式:
shell 语法如下:
bashRUN <command>
# 注:<command> 相当于终端操作的 shell 命令。
# 示例:
RUN yum -y install vim
exec 语法如下:
bashRUN ["executable", "param1", "param2"]
# 示例:
RUN ["/etc/execfile", "arg1", "arg1"] 等价于 RUN /etc/execfile arg1 arg2
注:RUN指令创建的中间镜像会被缓存,并会在下次构建中使用。如果不想使用这些缓存镜像,可以在构建时指定–no-cache参数,如:docker build --no-cache。
ADD:将本地文件添加到容器中。ADD
指令和 COPY
的格式和性质基本一致。但是在 COPY
基础上增加了一些功能。
bash# 格式:
ADD [--chown=<user>:<group>] <源路径>... <目标路径>
ADD [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]
# 示例:
ADD hom* /mydir/ # 添加所有以"hom"开头的文件
ADD hom?.txt /mydir/ # ? 替代一个单字符,例如:"home.txt"
#ADD是相对路径jar,把jdk-21_linux-x64_bin.tar.gz 添加到容器中,安装包必须要和Dockerfile文件在同一位置
ADD jdk-21_linux-x64_bin.tar.gz /usr/local/java/
如果 <源路径>
为一个 tar
压缩文件的话,压缩格式为 gzip
, bzip2
以及 xz
的情况下,ADD
指令将会自动解压缩这个压缩文件到 <目标路径>
去。
在某些情况下,这个自动解压缩的功能非常有用,比如官方镜像 ubuntu
中:
bash# 示例
FROM scratch
ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz /
...
但在某些情况下,如果我们真的是希望复制个压缩文件进去,而不解压缩,这时就不可以使用 ADD
命令了。
因此在 COPY
和 ADD
指令中选择的时候,可以遵循这样的原则,所有的文件复制均使用 COPY
指令,仅在需要自动解压缩的场合使用 ADD
。
在使用该指令的时候还可以加上 --chown=<user>:<group>
选项来改变文件的所属用户及所属组。
bash# 示例:
ADD --chown=55:mygroup files* /mydir/
ADD --chown=bin files* /mydir/
ADD --chown=1 files* /mydir/
ADD --chown=10:11 files* /mydir/
COPY:指令将从构建上下文目录中 <源路径>
的文件/目录复制到新的一层的镜像内的 <目标路径>
位置。功能类似 ADD,也有两种格式,一种类似于命令行,一种类似于函数调用。不会自动解压文件,也不能访问网络资源。
bash# 格式:
COPY [--chown=<user>:<group>] <源路径>... <目标路径>
COPY [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]
<源路径>
可以是多个,甚至可以是通配符,其通配符规则要满足 Go 的 [filepath.Match
(opens new window)](https://golang.org/pkg/path/filepath/#Match)规则,如:
bash# 示例:
COPY hom* /mydir/
COPY hom?.txt /mydir/
<目标路径>
可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 WORKDIR
指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。
此外,还需要注意一点,使用 COPY
指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用。特别是构建相关文件都在使用 Git 进行管理的时候。
在使用该指令的时候还可以加上 --chown=<user>:<group>
选项来改变文件的所属用户及所属组。
bash# 示例
COPY --chown=55:mygroup files* /mydir/
COPY --chown=bin files* /mydir/
COPY --chown=1 files* /mydir/
COPY --chown=10:11 files* /mydir/
注意:
LABEL:用于为镜像添加元数据,LABEL
是键值对。要在 LABEL
值中包含空格,请像在命令行中一样使用引号和反斜杠。一些用法示例:
bash# 格式:
LABEL <key>=<value> <key>=<value> <key>=<value> ...
# 示例:
LABEL version="1.0" description="这是一个Web服务器" by="IT笔录"
# 注:
使用LABEL指定元数据时,一条LABEL指定可以指定一或多条元数据,指定多条元数据时不同元数据
之间通过空格分隔。推荐将所有的元数据通过一条LABEL指令指定,以免生成过多的中间镜像。
ARG:用于指定传递给构建运行时的变量(给dockerfile传参),相当于构建镜像时可以在外部为里面传参。
bash#格式:
ARG <name>[=<default value>]
# 示例:
ARG site
ARG build_user=www
OUTBUILD:用于设置镜像触发器,是一个特殊的指令,它后面跟的是其它指令,比如 RUN
, COPY
等,而这些指令,在当前镜像构建时并不会被执行。只有当以当前镜像为基础镜像,去构建下一级镜像的时候才会被执行。
bash# 格式:
ONBUILD [INSTRUCTION]
# 示例:
ONBUILD ADD . /app/src
ONBUILD RUN /usr/local/bin/python-build --dir /app/src
CMD:为启动的容器指定默认要运行的程序,程序运行结束,容器也就结束。CMD 指令指定的程序可被 docker run 命令行参数中指定要运行的程序所覆盖。
类似于 RUN 指令,用于运行程序,但二者运行的时间点不同:
注意:如果 Dockerfile 中如果存在多个 CMD 指令,仅最后一个生效。
bash# 格式:
CMD ["executable","param1","param2"] (执行可执行文件,优先)
CMD ["param1","param2"] (设置了ENTRYPOINT,则直接调用ENTRYPOINT添加参数)
CMD command param1 param2 (执行shell内部命令)
# 示例:
CMD echo "This is a test." | wc -l
CMD echo "install success .....ok"
CMD ["/usr/bin/wc","--help"]
运行时可以指定新的命令来替代镜像设置中的这个默认命令,比如,ubuntu
镜像默认的 CMD
是 /bin/bash
,如果我们直接 docker run -it ubuntu
的话,会直接进入 bash
。我们也可以在运行时指定运行别的命令,如 docker run -it ubuntu cat /etc/os-release
。这就是用 cat /etc/os-release
命令替换了默认的 /bin/bash
命令了,输出了系统版本信息。
提到 CMD
就不得不提容器中应用在前台执行和后台执行的问题。这是初学者常出现的一个混淆。
Docker 不是虚拟机,容器中的应用都应该以前台执行,而不是像虚拟机、物理机里面那样,用 systemd
去启动后台服务,容器内没有后台服务的概念。
一些初学者将 CMD
写为:
bashCMD service nginx start
然后发现容器执行后就立即退出了。甚至在容器内去使用 systemctl
命令结果却发现根本执行不了。这就是因为没有搞明白前台、后台的概念,没有区分容器和虚拟机的差异,依旧在以传统虚拟机的角度去理解容器。
对于容器而言,其启动程序就是容器应用进程,容器就是为了主进程而存在的,主进程退出,容器就失去了存在的意义,从而退出,其它辅助进程不是它需要关心的东西。
而使用 service nginx start
命令,则是希望 upstart 来以后台守护进程形式启动 nginx
服务。而刚才说了 CMD service nginx start
会被理解为 CMD [ "sh", "-c", "service nginx start"]
,因此主进程实际上是 sh
。那么当 service nginx start
命令结束后,sh
也就结束了,sh
作为主进程退出了,自然就会令容器退出。
正确的做法是直接执行 nginx
可执行文件,并且要求以前台形式运行。比如:
bashCMD ["nginx", "-g", "daemon off;"]
EXPOSE:用于为容器暴露端口到外部,用于实现通讯,类似于docker run
的 -p 选项。语法如下:
bash# 格式:
EXPOSE <port> [<port>...]
# 示例:
EXPOSE 80 443
EXPOSE 8080
EXPOSE 11211/tcp 11211/udp
不管 EXPOSE 设置是什么,都可以通过使用 -p 标志在运行时覆盖它们。例如:
bash示例: docker run -p 80:80 ...
ENTRYPOINT 指定镜像的默认入口命令,该入口命令会在启动容器时作为根命令执行,所有其他传入值作为该命令的参数。
ENTRYPOINT 的值可以通过 docker run --entrypoint 来覆盖掉。
ENTRYPOINT 有两种语法格式,如下所示:
bash# exec 格式
ENTRYPOINT ["executable", "param1", "param2"]
# shell 格式
ENTRYPOINT command param1 param2
示例:
bash# exec 格式
ENTRYPOINT ["echo", "hello docker"]
# shell 格式
ENTRYPOINT echo "hello docker"
注意:
ENV:用来在镜像构建过程中设置环境变量,后续的 RUN 可以使用它所创建的环境变量。当创建基于该镜像的 container 的时候,会自动拥有设置的环境变量。
bash# 格式:
ENV <key> <value> #<key>之后的所有内容均会被视为其<value>的组成部分,因此,一次只能设置一个变量
ENV <key>=<value> ... #可以设置多个变量,每个变量为一个"<key>=<value>"的键值对,如果<key>中包含空格,可以使用\来进行转义,也可以通过""来进行标示;另外,反斜线也可以用于续行
示例:
ENV myName John Doe
ENV myDog Rex The Dog
ENV myCat=fluffy
WORKDIR:用于指定容器的一个目录, 相当于设置容器的工作目录。以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR
会帮你建立目录。
bash# 格式:
WORKDIR /path/to/workdir
# 示例:
ENV MYPATH /usr/local
WORKDIR $MYPATH
注:通过 WORKDIR 设置工作目录后,Dockerfile 中其后的命令 RUN、CMD、ENTRYPOINT、ADD、COPY
等命令都会在该目录下执行。在使用 docker run
运行容器时,可以通过 -w 参数覆盖构建时所设置的工作目录。
USER:指定运行容器时的用户名或 UID,后续的 RUN 也会使用指定用户。这个用户必须是事先建立好的,否则无法切换。使用 USER 指定用户时,可以使用用户名、UID 或 GID,或是两者的组合。
bash# 格式:
# USER <user>[:<group>]
USER user
USER user:group
USER uid
USER uid:gid
USER user:gid
USER uid:group
# 示例:
USER www
注:使用 USER 指定用户后,Dockerfile 中其后的命令 RUN、CMD、ENTRYPOINT
都将使用该用户。镜像构建完成后,通过 docker run
运行容器时,可以通过 -u
参数来覆盖所指定的用户。
VOLUME:用于指定持久化目录(指定此目录可以被挂载出去)。
bash# 格式
# VOLUME 挂载点
# 挂载点可以是一个路径,也可以是数组(数组中的每一项必须用双引号)
VOLUME ["<路径1>", "<路径2>"...]
VOLUME <路径>
# 示例:
VOLUME /data
容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中。为了防止运行时用户忘记将动态文件所保存目录挂载为卷,在 Dockerfile
中,我们可以事先指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据。
上面示例的 /data
目录就会在容器运行时自动挂载为匿名卷,任何向 /data
中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。当然,运行容器时可以覆盖这个挂载设置。比如:
bashdocker run -d -v mydata:/data xxxx
在这行命令中,就使用了 mydata
这个命名卷挂载到了 /data
这个位置,替代了 Dockerfile
中定义的匿名卷的挂载配置。
基础镜像为 CentOS 7
,本次实战模拟在 CentOS 7
的基础上安装 vim
、ifconfig
和 java
环境。
java环境下载地址:Index of /jdk/ (yangxingzhen.com)
在指定路径下 vim DockerFile
,填写内容如下:
bash# 指定基础镜像
FROM centos:7
# 定义变量
ENV MYPATH /usr/local
# 用于指定容器的一个目录
WORKDIR $MYPATH
# 维护者信息
MAINTAINER xuechao<whbblog.cn>
# 安装 VIM 编辑器
RUN yum -y install vim
# 安装 ifconfig 命令
RUN yum -y install net-tools
# 安装 java8 及 lib 库
RUN yum -y install glibc.i686
RUN mkdir /usr/local/java
# ADD 是把 jdk-8u192-linux-x64.tar.gz 添加到容器中,安装包必须要和 Dockerfile 文件在同一位置
ADD jdk-8u192-linux-x64.tar.gz /usr/local/java/
# 配置java环境变量
ENV JAVA_HOME /usr/local/java/jdk1.8.0_192
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
执行以下命令进行镜像构建。
bash
# 注意:定义的TAG后面有个空格,空格后面有个**点**
# docker build -t 新镜像名字:TAG .
docker build -t centosjava .
# 镜像构建成功输入如下:
Successfully built 1305abc9b75e
启动容器,验证构建的镜像,是否满足要求。
bash# 查看镜像
[root@VM-8-17-centos mydocker]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
centosjava latest 1305abc9b75e About a minute ago 1.39 GB
# 启动容器
docker run -it centosjava /bin/bash
# 查看 java 版本
[root@5fe7e4724010 /]# java -version
java version "1.8.0_192"
Java(TM) SE Runtime Environment (build 1.8.0_192-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.192-b12, mixed mode)
# 验证 vim 命令
[root@5fe7e4724010 /]# vim a.txt
# 验证 ifconfig 命令
[root@5fe7e4724010 /]# ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.17.0.2 netmask 255.255.0.0 broadcast 172.17.255.255
ether 02:42:ac:11:00:02 txqueuelen 0 (Ethernet)
RX packets 8 bytes 656 (656.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
loop txqueuelen 1000 (Local Loopback)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
本文作者:LiuXueChao
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!