我们在上一章曾介绍了如何使用DockerFile来为这个容器绑定一个数据卷,并知道了DockerFile的其中一个功能:Docker利用它来构建一个镜像。
DockerFile遵守着一定的格式。在本章,重点介绍有关DockerFile的具体内容。
DockerFile是什么
DockerFile是用来构建Docker镜像的构建文件,是由一系列命令和参数构成的脚本。
DockerFile使用方式
1、 创建新文件,内容遵守DockerFile的规范。
2、 通过Build
命令加载该DockerFile,并生成自定义的镜像。
3、 运行这个镜像。
重新剖析centos镜像
我们从浏览器端进入Docker hub官网。然后通过搜索centos进入到GitHub仓库中。(以centos7.6.1804为例)
FROM scratch
ADD centos-7-docker.tar.xz /
LABEL org.label-schema.schema-version="1.0" \
org.label-schema.name="CentOS Base Image" \
org.label-schema.vendor="CentOS" \
org.label-schema.license="GPLv2" \
org.label-schema.build-date="20181204"
CMD ["/bin/bash"]
特别留意一下文件中的第一行:其中FROM scratch
代表这个镜像源于基础镜像scratch
。(不要忘记UnionFS的分层特色)而这个srcatch
镜像在DockerFile中的地位,就相当于Object
在Java类中的地位:所有衍生的DockerFile都源自于scratch
。
DockerFile基本格式
1、 每条保留字指令必须全部都是大写,后面至少有一个参数。
2、 指令按照从上到下,顺序执行。
3、 使用#
符号表示注释。
4、 每条指令最终会创建出一个新的镜像层,并commit
到本地。
一个镜像的构建过程
1、 首先,Docker从FROM
指向的父镜像中创建一个容器。
2、 每执行一条指令,就对容器做出一条修改。
3、 执行类commit
操作生成一个新的镜像层。
4、 重复步骤2-3,直到所有的语句执行完,并保留最终生成的镜像。
DockerFile,Docker镜像和Docker容器
从应用软件的角度来看,DockerFile,Docker image,Docker container代表软件的三个不同阶段:
1、 DockerFile是软件的原材料。
2、 Docker镜像是软件的交付品。
3、 Docker容器可以是软件的运行状态。
好吧!从一个Java程序员的角度来看,我们还可以这么理解:
1、 DockerFile是由我们编写的*.java
文件。
2、 Docker image是javac
编译生成的.class
文件。
3、 Docker容器是在运行时JVM在内存根据这个.class
模板生成的一个个实例。
DockerFile面向开发,Docker镜像成为了交付标准,Docker容器则涉及了运维与开发,三者缺一不可。
DockerFile体系结构
我们在这里给出DockerFile详细的保留字指令:
关键字 | 作用 |
---|---|
FROM | 指明父镜像,表示当前镜像源自哪个镜像。 |
MAINTAINER | 镜像维护者的姓名和邮箱地址。格式为:name <mail> |
RUN | 基于此镜像构建容器时需要运行的基础命令。 |
EXPOSE | 容器对外暴露的端口,如tomcat容器的dockerfile会留下8080端口。 |
WORKDIR | 在创建容器之后,终端默认登录时的初始工作目录。 |
ENV | 设置此DockerFile内的环境变量。 |
ADD | 将宿主机目录下的文件,拷贝到镜像,并自动处理URL以及解压缩tar压缩包。 |
COPY | 类似ADD,拷贝文件和目录到镜像当中,但不执行解压缩。存在两种写法:copy s t 或者选择copy["s","t"] |
VOLUME | 容器数据卷,用于数据保存和持久化命令。 |
CMD | 指定一个容器启动时要运行的命令。DockerFile内可以有多个CMD,但是只会执行最后一个,在执行run命令时也可以主动覆盖掉它。 |
ENTRYPOINT | 指定一个容器启动时要运行的命令,与CMD不同,它会追加执行run命令时输入的CMD参数,然后拼接成一条命令。(详细请查阅后文的实战部分) |
ONBUILD | 当该DockerFile用于构建子镜像时才会执行。 |
体会CMD与ENTRYPOINT
我们之前已经练习过如何从官网下载tomcat镜像/通过exec修改文件夹/最后正确启动了。但大家而有没有这样一个疑问:为什么我启动tomcat镜像时,似乎完全省略了centos启动等过程,而直接就启动了tomcat服务呢?
原因就是这个tomcat镜像的dockerfile文件已经为我们提供了一站式服务。我们可以追踪到GitHub上的对应dockerfile源码(这里仅展现了一部分):
感兴趣的同学可以点击此链接查看GitHub上的源码。
#我选取的tomcat镜像基于jdk-8.官网目前可以提供到jdk-14的镜像.
FROM openjdk:8-jdk
ENV CATALINA_HOME /usr/local/tomcat
ENV PATH $CATALINA_HOME/bin:$PATH
RUN mkdir -p "$CATALINA_HOME"
WORKDIR $CATALINA_HOME
#....中间完成了大量的额外工作.....
EXPOSE 8080
CMD ["catalina.sh", "run"]
原因就是dockerfile
的最后一部分,它使用CMD
声明:执行tomcat
内部的catalina.sh
,并启动服务。
如果我们故意在创建该容器中覆盖该行CMD
,比如:
$ docker run -it -p 10000:8080 runnable_tomcat:1.0 ls -l
那么就会发现,它除了打印目录之外,什么都没有做。并且迅速地被Docker关闭了。 接下来咱们来举出一个实际的例子,来对比CMD
和ENTRYPOINT
的区别。
实现目标
- 基于CMD实现,使得centos在启动时打印公网ip地址。
- 基于ENTRYPOINT,使得centos在启动时打印公网ip地址。
注:本实例和centos本身的操作并没么关系,仅为了叙述CMD和ENTRYPOINT的区别。
由于CMD没有执行/bin/bash,这个centos容器在执行完curl之后就会被马上关闭。
curl命令
curl是一个利用URL语法,在命令行下的文件传输工具。cURL支持的通信协议有FTP、FTPS、HTTP、HTTPS、TFTP、SFTP、Gopher、SCP、Telnet、DICT、FILE、LDAP、LDAPS、IMAP、POP3、SMTP和RTSP。 ——百度百科
比如说通过http协议请求百度的主页:
$ curl http://www.baidu.com
另外还有一个查询ip地址的网址:ipinfo.io.。我们就利用这个网址来实现公网IP查询的功能。(速度稍慢,需要等待数秒)
$ curl ipinfo.io
我们再touch一个dockerfile文件,只需要在原先的centos
镜像中覆盖上薄薄的一层:
FROM centos
RUN yum install -y curl
CMD curl https://ipinfo.io
然后生成这个镜像。
$ docker build -f ipFile -t ip/centos:latest .
我们当使用这个ip/centos
创建容器的时候,在最开始,curl
就命令就会替我们查询到公网的IP地址。
现在,一个新问题又出现了:curl
是可以带参数的。比如说-i
可以显示这个https response的头信息。那我现在想要在创建这个容器的时候执行带参数的curl
,此时只能在命令行中覆盖[COMMAND]:
$ docker run ip/centos curl -i https://ipinfo.io
这时,我们使用ENTRYPOINT来对之前的dockerfile文件做些改写,来体验和CMD的区别:
FROM centos
RUN yum install -y curl
ENTRYPOINT curl https://ipinfo.io
此时,我们只需要这样执行(ip/centos/ep是基于修改的dockerfile生成的镜像):
$ docker run ip/centos/ep -i
此时Docker就相当于执行了curl https://ipinfo.io -i
。原因就是执行run
命令的时候输入的-i
拼接到了ENDTRYPOINT当中作为一个完整的CMD去执行。
体会ONBUILD的使用
我们还是拿centos
作为父模板。并在这个示例中简单体验ONBUILD的功能。首先touch第一个dockerfile,命名为parent_onbuild。
FROM centos
ONBUILD RUN echo "Parent image has been built."
CMD /bin/bash
注意:我在ONBUILD中留下了一条RUN语句。而当我们执行build
语句构建parent/centos
镜像时,构建过程并没有什么改变:
Sending build context to Docker daemon 3.072kB
Step 1/3 : FROM centos
---> 470671670cac
Step 2/3 : ONBUILD RUN echo "Parent image has been built."
---> Running in 3cb5dc8af44c
Removing intermediate container 3cb5dc8af44c
---> 1373d6bec880
Step 3/3 : CMD /bin/bash
---> Running in 10b4a59a154b
Removing intermediate container 10b4a59a154b
---> 377e1c81ab39
Successfully built 377e1c81ab39
Successfully tagged parent/centos:latest
此刻,再touch一个新的dockerfile,命名为son_centos,注意,这一次我们将基础镜像设置为刚才生成的parent/centos
。
FROM parent/centos
CMD /bin/bash
我们以这个dockerfile为模板生成son/centos
。然后观察创建过程:
Sending build context to Docker daemon 4.096kB
Step 1/2 : FROM parent/centos
# Executing 1 build trigger
---> Running in dd7f02353963
Parent image has been built.
Removing intermediate container dd7f02353963
---> 0be39ca76dc1
Step 2/2 : CMD /bin/bash
---> Running in 6354f02c4f66
Removing intermediate container 6354f02c4f66
---> 695ee43d5c5f
Successfully built 695ee43d5c5f
Successfully tagged son/centos:latest
注意,此时命令行会显示# Executing 1 build trigger
,显示有一个ONBUILD触发器被触发。然后我们就可以观察到,RUN echo "Parent container has been built."
语句被触发了。
简单案例:创建一个预安装vim的centOS镜像
实现目标
我希望通过自定义的centOS镜像有这些功能:
- 切换初始登录时的默认工作目录(原来的centOS镜像默认是
/
目录) - 安装
vim
。(我们发现压缩版的centOS并没有安装它)
我们touch
出一个文件,然后按照dockerfile的格式编写脚本:
#我们以centos为基础镜像,来制作一个新的镜像。
FROM centos
#标注镜像的作者,可选择项。
MAINTAINER Li Junhu <376781642@qq.com>
#设置本dockerfile内生效的环境变量
ENV MYPATH /usr/local
#指定默认的工作目录
WORKDIR $MYPATH
#通过RUN指定->使用yum命令安装vim
RUN yum -y install vim
#开放端口80.
EXPOSE 80
#通过观察可以发现,只有最后一个CMD生效。如果在run镜像时主动输入CMD,则以下三行CMD会被全部覆盖。
CMD echo $MYPATH
CMD echo "success build ok."
CMD /bin/bash
随后,我们再使用build
命令,基于这个dockerfile生成新镜像。
$ docker build -f dockerfile -t ljh/centos:latest .
我们稍微介绍build
命令的参数:-f
,即file string,输入dockerfile的路径。-t
,即target string,指定输出的image文件的路径。.
表示将这个image文件保存到docker对应的image文件目录当中。
详细的想象可以通过--help
命令查阅。
*宿主机禁用IPv4转发导致的问题
笔者在build过程中报出了这个错误(如果没有报该错误,则可以忽略这段叙述):
[Warning] IPv4 forwarding is disabled. Networking will not work.
原因是宿主机默认禁用了IPv4转发,导致在执行CMD yum -y install vim
时无法联网而出错。因此需要在宿主机中更改以下配置文件:
$ vim /usr/lib/sysctl.d/00-system.conf
最末行添加如下设置,并通过$ systemctl restart network
重启网络服务。
net.ipv4.ip_forward=1
综合案例:DIY tomcat容器
准备文件
在实现这个案例之前,我们首先需要准备一些文件。创建一个空目录作为我们的工作区:
$ mkdir -p /root/dockerTest/tomcat9
创建一个c.txt
。(这个文件没有什么意义,只是等会测试COPY用)
$ touch c.txt
从从官网中下载tomcat9 (core) 和Linux-jdk8的tar.gz
包,并拷贝到tomcat9文件夹下:
现在,tomcat9文件夹下应该有三个文件:
apache-tomcat-9.0.35.tar.gz c.txt jdk-8u144-linux-x64.tar.gz
梳理流程
现在,我们来整理思路,如果使用Dockerfile来自己创建一个tomcat镜像,都需要哪些步骤:
1、 首先我们应该清楚,我们自定义的tomcat镜像是基于centos
执行的,因此我们选取centos作为我们的基础镜像。
2、 随后我们通过拷贝的形式,将tomcat和jdk的压缩包发送给Docker引擎,并让它解压到镜像内。
3、 当然,在运行tomcat和jdk之前,我们还要做一个额外的工作:配置环境变量。
4、 在这一切完成之后,我们最终使用CMD唤醒tomcat的启动脚本startup.sh
,然后顺便打印以下tomcat的日志信息。
你也可以选择性的为你的DIY镜像额外装配一些零件,比如说安装vim,或者拷贝一些别的文件到镜像内(这里我们选择c.txt
),或者通过VOLUME设定tomcat容器的数据卷。
注意,不同jdk解压包的输出文件夹,名字是不同的。笔者选择的jdk版本是8u144,因此解压的路径是./jdk1.8.0_144/
。tomcat同理。因此如果你选择的版本跟笔者不同,下面的dockerfile内部的ENV可能会有细微的差别。
另外,在编写ADD和COPY命令时,注意相对路径的问题。因为笔者将所有的文件全部放入了一个工作区间,因此可以不用考虑路径的问题。
FROM centos
MAINTAINER Li junhu <376781642@qq.com>
#test COPY
COPY c.txt /usr/local/cincontainer.txt
#add tomcat and jdk
ADD jdk-8u144-linux-x64.tar.gz /usr/local
ADD apache-tomcat-9.0.35.tar.gz /usr/local
#install yum
RUN yum -y install vim
ENV MYPATH /usr/local
WORKDIR $MYPATH
#set jdk
ENV JAVA_HOME $MYPATH/jdk1.8.0_144
ENV CLASSPATH $JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
#set tomcat_path
ENV CATALINA_HOME $MYPATH/apache-tomcat-9.0.35
ENV CATALINA_BASE $MYPATH/apache-tomcat-9.0.35
ENV PATH $PATH:$JAVA_HOME/bin:$CATALINA_HOME/lib:$CATALINA_BASE/bin
EXPOSE 8080
#startup tomcat
CMD $CATALINA_HOME/bin/startup.sh && tail -F $CATALINA_HOME/bin/logs/catalina.out
随后,我们再使用build
命令,基于这个dockerfile生成自定义的tomcat镜像,并且命名为diy_tomcat
。
$ docker build -f dockerfile -t diy_tomcat:latest .
利用容器卷部署APP
为了在tomcat部署一个现成的web服务,我们进入到码云仓库中,克隆一个样例JavaWeb项目的HTTPS地址。(感谢该作者的无偿分享)
我们在物理机(真·宿主机)下打开IntelliJ IDEA中导入该项目,点击右侧的Maven Project扫描,并使用install
将这个项目打成一个war包,发送到centOS虚拟机环境的相应目录。
一般情况下,我们只需要将这个war包拷贝到apache-tomcat-x.x.x/webapps/
目录下,然后执行apache-tomcat-x.x.x/bin/
目录下的startup.bat
(Windows环境)或者startup.sh
(Linux环境)来让tomcat来运行我们的web服务。
我们在外部的浏览器可以通过URL来访问对应的服务(这个war包的名字是helloworld。):
http://${host}:${port}/${warName}/
但是,我们希望能够通过容器卷的方式,让tomcat容器运行宿主机本地内的web包。
因此我们需要将宿主机的helloworld.war包所在的目录和虚拟机中tomcat/webapps
目录关联起来:
-v /root/dockerTest/tomcat9:/usr/local/apache-tomcat-9.0.35/webapps
我们启动DIY的tomcat容器,并将上述的数据卷添加进去。由于容器和宿主机通过数据卷共享数据,因此只要宿主机在此数据卷/root/dockerTest/tomcat9
中放置.jar
或.war
包,虚拟容器中的tomcat就可以将它们运行起来。
$ docker run --name tomcat_helloworld \
-v /root/dockerTest/tomcat9:/usr/local/apache-tomcat-9.0.35/webapps \
-d -p 10000:8080 diy_tomcat
如果一切配置正常,我们就可以在物理机的浏览器访问虚拟机的10000端口,并访问到这样的页面:
(笔者的URL是http://hadoop102:10000/helloworld
。hadoop102是笔者为虚拟机配置的主机名。)
至此,容器卷 + tomcat的简单应用也结束了。