前言
说起docker,大家都知道是容器。我们从仓库中docker pull拉取镜像(image)到本地,然后docker run指定镜像来启动一个容器(container)。那么,这个容器就开始守护内部的程序。
但是很多时候,我们需要在docker中部署自己的程序,或者因为pull的镜像太大,我们需要基于一个小的镜像来重构,这就需要我们要自己构建docker,而docker的构建步骤是基于Dockerfile来实现的。
Dockerfile
在学习Dockerfile之前,首先弄明白镜像。容器就类似于“虚拟机”,而虚拟机就需要操作系统,这个操作系统就是镜像(image)。
image
通过docker images就可以查看宿主机上已存在的所有镜像。
在上面的镜像中,使用比较多的就是centos,但是centos里面已经预装了很多软件,所以大小为231MB。像alpine、busybox的镜像就比较小,很多命令都需要自己安装,但是实际中也不会过多地去容器中执行很多的命令。
指令
首先文件名必须是Dockerfile,第一行必须使用FROM指令来引入镜像,最后一行是通过CMD或者ENTRYPOINT指令来指定容器中要运行的命令。
构建镜像是由一个基础镜像开始,通过经过各种指令来构造成成我们自己的镜像
- FROM:第一行必须是 FORM centos,表示从centos镜像构建容器
- ENV key value:设置环境变量
- RUN command:在之前的镜像基础上执行指令,并提交为新的镜像
- EXPOSE 80:开放80端口,run时需要用 -p来指定映射端口,不指定默认分配
- VOLUME /data:声明容器中/data为匿名卷。-v可以将这个匿名卷绑定到宿主机,不指定会自动绑定到/var/lib/docker/volumes
- ADD src dest:文件必须和Dockerfile同一目录,除了复制本地文件到容器中外,还有解压缩文件的功能
- COPY src dest:文件必须和Dockerfile同一目录,将src目录/文件复制到容器的dest目录
- CMD:容器启动时执行的命令
- ENTRYPOINT:和CMD一样,指定容器启动执行的命令
上面就是在Dockerfile中常用的命令。看到上面可能会有疑问,CMD 和 ENTRYPOINT不是一样的命令吗?
CMD和ENTRYPOINT
CMD 指令用于为镜像指定一个默认的命令和参数,但该命令可以在运行容器时被指定的命令覆盖。我们还记得最初学习docker时,运行容器时使用docker run,在最后面会加一个/bin/bash,这个/bin/bash就是一个指令,它会覆盖CMD。
代码语言:bash复制docker run my-image echo "Hello, World!"
同样,这里,echo "Hello, World!" 将覆盖 Dockerfile 中的 CMD 指令。
而ENTRYPOINT指令用于配置一个容器启动时将始终执行的主命令,就不会有这种被覆盖的问题,它会默认把docker run指定的指令当做自己的参数,同样
代码语言:bash复制docker run my-image "Hello, World!"
这里 "Hello, World!" 将作为参数传递给 ENTRYPOINT 中的命令,而不会替换掉它。有时CMD和ENTRYPOINT也会搭配使用,ENTRYPOINT 用于指定固定的命令,而 CMD 用于提供默认参数。这样,可以在运行容器时添加或替换 CMD 的默认参数。
例如我使用下面的Dockerfile构造镜像之后:
代码语言:bash复制FROM centos
ENTRYPOINT ["echo"]
CMD ["Hello, World!"]
运行容器时不指定命令,就会输出"Hello, World!",如果我指定命令:
代码语言:bash复制docker run my-image "Hello, Docker!"
那么, "Hello, Docker!"就会覆盖CMD指定的 "Hello, World!" ,最后输出 "Hello, Docker!"
Tomcat的Dockerfile
这里我就基于centos镜像,编写Dockerfile,来构造一个Tomcat的镜像。在构造一个镜像之前,我们还需要考虑这个镜像依赖的环境变量、下载的软件包以及开放的命令等等。
如果我们在服务器上搭建一个Tomcat,我们需要下载tomcat,然后配置jdk、开放8080端口,然后在tomcat的bin目录下,执行startup.sh,这样一个tomcat服务就启动成功了。我们要做的就是,把这些操作转换到Dockerfile中。
Dockerfile
首先我下载了tomcat的安装包,在宿主机的/data目录下,同时宿主机的jdk放在了/usr/local下。如何把tomcat和jdk放到镜像中,这时候有的同学就要抢答了:“COPY!”
其实比较好的方案是使用VOLUMN指令,将宿主机的目录挂载到容器中,这样每个容器使用的就是宿主机上的文件,而减少了对磁盘存储的占用。
代码语言:bash复制FROM centos
VOLUME /usr/local/jdk
VOLUME /usr/local/tomcat
ENV JAVA_HOME /usr/local/jdk
ENV CATALINA_HOME /usr/local/tomcat
ENV PATH $PATH:$JAVA_HOME/bin:$CATALINA_HOME/bin
EXPOSE 8080
CMD catalina.sh run
在Dockerfile中,我在/usr/local下一共声明了两个卷:jdk和tomcat,然后在后面的ENV指令中,将这两个卷作为jdk和tomcat的环境变量目录,配置在了PATH中。
然后在CMD中执行catalina.sh run的指令,这里不能使用startup.sh,因为docker容器是为了守护一个进程/命令而存在的,所以必须必须使用前台启动方式。
构造镜像
然后构造开始构造镜像:
代码语言:bash复制docker build -t="tomcat-aqi" .
构造成功之后,通过docker images可以查看到我们的镜像。
运行容器
根据dockerfile中配置,在docker run命令中,我们要将宿主机中的jdk和tomcat目录,挂载到容器的卷中,然后容器8080端口映射为宿主机的9092。
代码语言:bash复制docker run -d --name tomcat-qi -p 9092:8080
-v /data/apache-tomcat-8.0.11:/usr/local/tomcat
-v /usr/local/jdk1.8.0_201:/usr/local/jdk
-it tomcat-aqi
在容器运行成功之后,通过docker ps可以查看到进程。
在浏览器中通过服务器的9092的端口,就访问到了容器中的tomcat。
在容器信息中,我们可以看到挂载信息,宿主机上的JDK和tomcat目录已经被挂载到了容器卷中。
也可以看到容器的ENV环境变量和CMD信息。
镜像层
镜像层(Image Layers)是 Docker 镜像的一个重要概念。每个 Docker 镜像实际上是由多层文件系统(Union File System)组成的,每一层都是只读的,称为镜像层。这些层次是镜像的基础,每个层次代表镜像构建过程中的一个步骤或一个命令。
每一层都是一个增量修改,相当于给上一层加上了新的变化。这样,Docker 镜像的每一层都是不可变的。
Docker 通过层来实现缓存机制。例如,如果你修改了 Dockerfile 中的某一行,Docker 只会从该行开始重新构建层,而之前的层都被缓存并且重用。这大大提高了构建速度
减少镜像层数和每层的大小可以优化镜像的体积和传输速度。过多的层数会导致冗余数据的增多和构建时间的增加。我们可以通过将多个 RUN 指令合并为一个,来减少镜像层。
我们在上面的Dockerfile中加入三行RUN指令:
代码语言:bash复制RUN ps
RUN echo 1
RUN date
再将三条RUN指令合并:
代码语言:bash复制RUN ps && echo 1 && date
分别使用两个Dockerfile构造镜像,从构造过程日志可以看出,指令合并前镜像层为8,合并后为6。
结语
最后讲讲为什么使用centos,而不是alpine镜像。原因就是alpine需要下载相应版本的OpenJDK,不支持我这个oracle的JDK,所以我还是走了捷径,直接使用了centos。