文章目录
- Dockerfile应用的容器化
- 应用的容器化——简介
- 应用的容器化——详解
- 单体应用容器化
- 获取应用代码
- 分析Dockerfile
- 容器化当前应用/构建具体的镜像
- 推送镜像到仓库
- 运行应用程序
- 测试
- 总结
- 最佳实践
- 利用构建缓存
- 合并镜像
- 命令总结
Dockerfile应用的容器化
Docker 的核心思想是容器化,即将应用程序及其依赖项打包成一个可移植的容器,并通过容器来实现应用程序的快速部署、可靠性管理和跨平台运行。
应用的容器化——简介
完整的应用容器化过程主要分为以下几个步骤:
(1)编写应用代码
(2)创建一个Dockerfile,其中包括当前应用的描述、依赖以及该如何运行这个应用
(3)对该Dockerfile执行docker build 命令
(4)等待Docker 将应用程序(即应用被打包为一个Docker镜像)就能以镜像的形式交付并以容器的方式运行了。
应用的容器化——详解
单体应用容器化
获取应用代码
从源代码仓库或本地文件系统中获取应用程序代码。
[root@localhost ceshi]# ls
index.html start.sh
# index.html 当服务需要用到的html文件
# start.sh 里面放置着开启http服务器的脚本
该目录下包含了全部的应用源码。
分析Dockerfile
Dockerfile 是用于构建 Docker 镜像的脚本文件,其中包含一系列指令和参数。下面是 Dockerfile 中常用的参数:
FROM # 指定基础镜像,例如 FROM centos,表示使用 Centos Linux 作为基础镜像
MAINTAINER # 镜像是谁写的,姓名+邮箱
RUN # 镜像构建的时候需要运行的命令
ADD # COPY文件会自动解压
WORKDIR # 镜像的工作目录
VOLUME # 挂载的目录
EXPOSE # 声明容器将监听哪些端口,例如 EXPOSE 80,表示容器将监听 80 端口。
CMD # 指定这个容器启动的时候要运行的命令,只有最后一个会生效,可被替代
ENTRYPOINT # 指定这个容器启动的时候要运行的命令,可以追加命令
ONBUILD # 当构建一个被继承 Dockerfile 这个时候就会运行ONBUILD 的指令,触发指令
COPY # 类似ADD,将我们文件拷贝到镜像中
ENV # 构建的时候设置环境变量
在创建和编写Dockerfile需要注意些什么
Dockerfile 文件必须位于上下文目录中
,Docker 引擎会将该目录下的所有文件打包发送给 Docker daemon 进行构建。如果 Dockerfile 文件不在上下文目录中,则会出现构建失败的情况。- Dockerfile 文件的文件名可选择为 Dockerfile,Docker 引擎会默认寻找该文件名作为构建镜像的脚本文件,否则在构建时你就要指定你的Dockerfile文件
- 使用大写字母 在 Dockerfile 中使用大写字母来命名指令和参数。例如,使用
FROM
而不是from
。
了解了参数我们来创建一个自己的CentOS
[root@localhost ceshi]# vim Dockerfile
FROM centos:centos7.9.2009
MAINTAINER CSQ
RUN yum install -y httpd
COPY index.html /var/www/html/index.html
COPY . /srv
WORKDIR /srv
RUN chmod 775 start.sh
EXPOSE 80
CMD ["./start.sh"]
Dockerfile主要包括两个用途
- 对当前应用的描述
- 指导Docker完成应用的容器化(创建一个包含当前应用的镜像)
具体分析每一步的作用
每个Dockerfile文件第一行都是FROM指令。FROM指令指定的镜像,会作为当前镜像的一个基础镜像层,当前应用的剩余内容会作为新增镜像层添加到基础镜像层之上。
MAINTAINER CSQ
:Dockerfile通过MAINTAINER 方式指定当前镜像维护者为CSQ,自己也可以在维护者后面添加更详细的信息有助于为该镜像潜在的使用者提供沟通途径。该操作不会创建一个新的镜像层。
RUN chmod 775 start.sh
:给 /start.sh 文件添加可执行权限,会创建一个新的镜像层。
RUN yum install -y httpd
:在容器中安装 httpd,会创建一个新的镜像层。
COPY index.html /var/www/html/index.html
:将 Dockerfile 所在目录下的 index.html 文件复制到镜像中的 /var/www/html/ 目录下,会创建一个新的镜像层。
COPY . /srv
: 将当前目录下的所有文件复制到 容器内的 /srv目录下,会创建一个新的镜像层。
WORKDIR /srv
:Dockerfile 通过 WORKDIR 指令,为Dckerfile中尚未执行的指令设置工作目录。该目录与镜像有关,并且会作为元数据记录到镜像配置中,但不会创建新的镜像层。
EXPOSE 80
:因为当前应用需要通过TCP端口80对外提供一个Web服务,所以在Dockerfile中通过EXPOSE 80 指令来完成相应端口的设置。这个配置会作为元数据被保存下来,并不会产生新的镜像层。
CMD ["./start.sh"]
:容器启动后默认执行的命令,并不会产生新的镜像层。
目前镜像一共包含10层,如下图所示
容器化当前应用/构建具体的镜像
接下来就算构建自己的镜像了!,下面的目命令会构建一个名为 web:v1.0 的镜像,命令后的【.
】表示Docker镜像构建的时候,使用当前目录作为构建上下文
一定要在命令最后包含这个点,并且执行命令前,确认当前目录是你包含Dockerfile和应用代码的目录
# 通过这个文件构建镜像
# 命令 docker build -t 镜像名:[tag] -f dockerfile路径 .
[root@localhost ceshi]# docker build -t web:v1.0 .
Sending build context to Docker daemon 4.096kB
Step 1/9 : FROM centos:centos7.9.2009
---> eeb6ee3f44bd
Step 2/9 : MAINTAINER CSQ
---> Running in 0189981f25a3
Removing intermediate container 0189981f25a3
---> c6d8ef18c3de
Step 3/9 : RUN yum install -y httpd
---> Running in 90f24717dd91
Loaded plugins: fastestmirror, ovl
Determining fastest mirrors
* base: mirrors.huaweicloud.com
* extras: mirrors.huaweicloud.com
* updates: mirrors.bfsu.edu.cn
Complete!
Removing intermediate container 90f24717dd91
---> 909aa4b4a773
Step 4/9 : COPY index.html /var/www/html/index.html
---> 39539a6f3d77
Step 5/9 : COPY . /srv
---> aae2ee95e7ef
Step 6/9 : WORKDIR /srv
---> Running in 2d03bd7d7609
Removing intermediate container 2d03bd7d7609
---> ac81a36893bc
Step 7/9 : RUN chmod 775 start.sh
---> Running in 72b1cafd7673
Removing intermediate container 72b1cafd7673
---> 6a235fbc2631
Step 8/9 : EXPOSE 80
---> Running in 32d41d6a8470
Removing intermediate container 32d41d6a8470
---> 336e359c7dc7
Step 9/9 : CMD ["./start.sh"]
---> Running in 1c8c819dbb8f
Removing intermediate container 1c8c819dbb8f
---> 6107859f4850
Successfully built 6107859f4850
Successfully tagged web:v1.0
命令结束后,检查本地Docker镜像库是否包含了刚刚构建的镜像
[root@localhost ceshi]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
web v1.0 6107859f4850 4 minutes ago 445MB
centos centos7.9.2009 eeb6ee3f44bd 21 months ago 204MB
现在应用以及容器化成功了!
可以通过 docker inspect web:v1.0
来确认刚刚构建的镜像配置是否正确。这个命令会列出Dockerfile中设置的所有配置选项
推送镜像到仓库
在创建一个镜像之后,将其保存在一个镜像仓库服务是一个很不错的方式。这样存储镜像会比较安全,并且可以被其他人访问使用。Docker Hub就算这样一个开放的公共镜像仓库服务,并且这也是docker push
命令默认的推送地址
推送镜像之前,需要先使用Docker ID 登录Docker Hub。除此之外,还需要为待推送的镜像打上合适的标签。(没有Docker Hub 可以自己注册一个)
接下来使用命令登录Docker Hub
[root@localhost ceshi]# docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username: ayuqaq
Password:
Login Succeeded
推送Docker 镜像之前,还需要为镜像打上标签。者是因为Docker在镜像推送的过程中需要如下信息
- Registry(镜像仓库服务)
- Repository(镜像仓库)
- Tag(镜像标签)
当我们在使用Docker命令进行镜像推送时,如果没有明确指定Registry和Tag的值,Docker会默认使用docker.io作为Registry,使用latest作为Tag。
但是,Docker并没有默认值可供Repository使用,而是从被推送的镜像中的REPOSITORY属性值获取。因此,在进行镜像推送时,我们需要明确指定Repository的值,否则Docker会报错。
执行docker images
命令后发现当前的镜像仓库名称为web:v1.0
,意味着在执行docker push
命令时,会尝试将镜像推送到docker.io/web:v1.0
中。但是,当前用户ayuqaq没有访问web这个镜像仓库的权限,只能尝试推送到自己的ayuqaq命名空间下。因此,需要为当前镜像重新打一个标签,加上ayuqaq的ID作为命名空间,例如ayuqaq/web:v1.0
,以便成功地将镜像推送到自己的命名空间下。
为镜像打标签命令的格式
docker tag SOURCE_IMAGE[:TAG] TARGET_IMAGE[:TAG]
# SOURCE_IMAGE表示原始镜像名称,可以是镜像ID或者镜像名称加上标签名
# TAG表示原始镜像的标签,默认为latest
# TARGET_IMAGE表示打好标签后的目标镜像名称,可以包含命名空间,也可以不包含
[root@localhost ceshi]# docker tag web:v1.0 ayuqaq/web:latest
[root@localhost ceshi]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ayuqaq/web latest 6107859f4850 6 minutes ago 445MB
web v1.0 6107859f4850 6 minutes ago 445MB
centos centos7.9.2009 eeb6ee3f44bd 21 months ago 204MB
现在将该镜像推送到Docker Hub
[root@localhost ceshi]# docker push ayuqaq/web:latest
The push refers to repository [docker.io/ayuqaq/web]
d10a5034e7a0: Pushed
894e08c192f4: Pushed
2de26a085520: Pushed
0d4e9442311f: Pushed
174f56854903: Layer already exists
latest: digest: sha256:77f20a3da4e6d35ea56260ffac2438d551f9e62bc083f2ac328fff6d6575c872 size: 1363
下图展示了Docker如何确定镜像所要推送的目的仓库
运行应用程序
运行这个应用程序很简单
[root@localhost ceshi]# docker run -d -p 8888:80 --name webserver web:v1.0
# docker run:创建并启动一个新的容器。
# -d:在后台运行容器。
# -p 8888:80:将主机的8888端口映射到容器内部的80端口。
# --name webserver:指定容器的名称为webserver。
# web:v1.0:使用web:v1.0镜像作为容器的基础镜像。
接下来验证一下程序是否真的运行,并且对外提供服务的端口是否正常工作
从上面的输出可以看到,容器已经正常运行。需要注意的是,8888端口已经成功映射到了80之上,并且任意外部主机都可以通过8888端口访问容器。
测试
打开浏览器,输入 localhost:8888
就可以访问到了(localhost是自己的主机IP)
如果没有出现如上页面,尝试下面的检查来确认原因所在
- 使用
docker ps
指令来确认容器已经启动并且正常运行。容器名称是 webserver,并且从输出内容中可以看到 0.0.0.0:8888->80/tcp - 确认防火墙或者其他网络安全设置没有阻止访问Docker 主机的8888端口
总结
Dockerfile 中的注释行,都是以#开头的
除了注释之外,每一行都是一条命令。指令的格式和指令参数:RUN command
指令是不区分大小写的,但是通常采用大写的方式,这样Dockerfile的可读性会高一些
docker build 命令会按行来解析Dockerfile 中的指令并顺序执行
部分指令会在镜像中创建新的镜像层,其他指令只会增加或修改镜像的元数据信息。
上面例子中,新增镜像层指令包括 FROM RUN 以及 COPY
而新增元数据指令包括 EXPOSE、WORKDIR、ENV、CMD。关于如何区分命令是否会新建镜像层,一个基本原则是,如果指令的作用是向镜像中增添新的文件或者程序,那么这条指令就会新建镜像层;如果之上高速Docker如何完成构建或者如何运行应用程序,那么就只会增加镜像的元数据
可以通过docker history来查看构建镜像的过程中都执行了哪些指令
上面的输出内容中,有两点需要注意的。
首先,每行内容都对应了Dockerfile中的一条指令(顺序是自下而上的) CREATED BY 这一行还展示了当前行具体对应Dockerfile中的哪条指令
其次,从这个输出内容中,可以观察到只有5条指令会新建镜像层(就是SIZE列对应的数值不为0的指令),分别对应着 Dockerfile 中的 FROM 、RUN以及 COPY 指令。虽然其他指令看上去跟这些新建镜像层的指令并无区别,但是实际上它们只在镜像中新增了元数据信息。这些指令看上去之所以没有区别,是因为Docker对之前构建镜像层方式的兼容。
可以通过 docker inspect 指令来确认确实只有5个层被创建了
使用FROM 命令引用官方基础镜像是一个很好的习惯,因为官方镜像通常会遵循一些最佳实践,并且能够帮助使用者规避一些已知的问题。除此之外,使用FROM的时候选择一个相对较小的镜像文件通常也能规避一些潜在的问题。
最佳实践
利用构建缓存
Docker的构建过程是利用了缓存机制的,这个机制可以让构建过程更快。比如说,第一次构建一个Docker镜像需要花费一定的时间,因为要拉取基础镜像并构建镜像层。但是如果在一个全新的Docker主机上再次构建同样的镜像,基本上可以立即完成,因为第一次构建的内容已经被缓存下来了,可以被后续的构建过程复用。
在构建过程中,Docker会检查每一条指令对应的镜像层是否在缓存中存在,如果存在,就会使用缓存中的镜像层;如果不存在,就会基于这条指令构建新的镜像层。通过这个缓存机制,可以显著加快构建过程。
如下Dockerfile案例
FROM centos:centos7.9.2009
RUN yum install -y httpd
COPY index.html /var/www/html/index.html
COPY . /srv
WORKDIR /srv
RUN chmod 775 start.sh
EXPOSE 80
CMD ["./start.sh"]
第一条指令告诉Docker使用 centos:centos7.9.2009 作为基础镜像。如果主机中已经存在这个镜像,那么构建时会直接跳到下一条指令:如果镜像不存在,则会从Docker Hub(docker.io)拉取。
下一条指令(RUN yum install…) 对镜像执行了一条命令,此时Docker会检查构建缓存中是否存在基于同一基础镜像,并且执行了相同指令的镜像层。在此例中,Docker 会检查缓存中是否存在一个基于centos:centos7.9.2009镜像并且执行了RUN yum install -y httpd指令构建得到的镜像层
如果找到该镜像层,Docker 会跳过这条指令,并链接到这个已经存在的镜像层,然后继续构建;如果无法找到符合要求的镜像层,则设置缓存无效并构建该镜像层。
此处"设置缓存无效"作用于本次构建的后续部分。也就是说Dockerfile中接下来的指令将全部执行而不会尝试查找构建缓存。
假设Docker已经在缓存中找到了该指令对应的镜像层(缓存命中),并且假设这个镜像层的ID是AAA
下一条指令会复制一些代码到镜像中(COPY index.html …)。因为上一条指令命中了缓存,Docker会继续查找是否有一个缓存的镜像层也是基于AAA层并执行了COPY 。 /src 命令。如果有,Docker会链接到这个缓存的镜像层并继续执行后续的指令;如果没有,则构建镜像层,并对后续的构建操作设置缓存无效
假设Docker已经有一个对应该指令的缓存镜像层(缓存命中),并且假设这个镜像层的ID是BBB
那么Docker将继续执行Dockerfile中剩余的指令。
首先,一旦有指令在缓存中未命中(没有该指令对应的镜像层),则后续的整个构建过程将不再使用缓存。在编写Dockerfile时须特别注意这一点,尽量将易于发生变化的指令置于Dcokerfile文件的后方执行。这意味着缓存未命中的情况将直到构建的后期才会出现——从而构建过程能够经理从缓存中获益
通过对doker build
命令加入 --no-cache=true
参数可以强制忽略对缓存的使用
COPY和ADD指令也会检查上次一构建后是否发生了变化
Docker会计算每一个被复制文件的Checksum值,并与缓存镜像层中同一文件的Checksum进行对比,如果不匹配,那么就认为缓存无效并构建新的镜像层。
合并镜像
合并镜像并非一个最佳实践,因为这种方式利弊参半
总体来说,Docker 会遵循正常的方式构建镜像,但之后会增加一个额外的步骤,将所有的内容合并到一个镜像层中。
当镜像中层数太多时,合并是一个不错的优化方式。例如,创建一个新的基础镜像,以便基于它来构建其他镜像的时候,这个基础镜像就最好被合并为一层。
缺点是,合并的镜像将无法共享镜像层。这回导致存储空间的低效利用,而且push和pull操作的镜像体积更大。
执行docker build
命令时,可以通过增加 --squash
参数来创建一个合并的镜像。
上图,两个镜像内容完全一样,但一个经过合并,一个没有经过合并。如果将这两个镜像都上传到Docker Hub上,那么不经过合并的镜像只需要上传差异部分的镜像层即可,而经过合并的镜像则需要上传全部的镜像层,即使这些镜像层与已经上传的其他镜像的镜像层完全一样。
命令总结
docker build
命令会读取Dcokerfile,并将应用程序容器化。使用-t
参数位镜像打标签,使用 -f
参数指定Dockerfile 的路径与名称,使用-f
参数可以指定位于任何路径下的任意名称的Dockerfile。构建上下文是指应用文件存放的位置,可能是本地Docker主机上的一个目录或一个远程git仓库。
FROM # 指定基础镜像,例如 FROM centos,表示使用 Centos Linux 作为基础镜像
MAINTAINER # 镜像是谁写的,姓名+邮箱
RUN # 镜像构建的时候需要运行的命令
ADD # COPY文件会自动解压
WORKDIR # 镜像的工作目录
VOLUME # 挂载的目录
EXPOSE # 声明容器将监听哪些端口,例如 EXPOSE 80,表示容器将监听 80 端口。
CMD # 指定这个容器启动的时候要运行的命令,只有最后一个会生效,可被替代
ENTRYPOINT # 指定这个容器启动的时候要运行的命令,可以追加命令
ONBUILD # 当构建一个被继承 Dockerfile 这个时候就会运行ONBUILD 的指令,触发指令
COPY # 类似ADD,将我们文件拷贝到镜像中
ENV # 构建的时候设置环境变量