1 手动制作docker镜像
以制作sshd+nginx镜像为例
1.1 启动基础容器
docker run -it --name ssh_nginx centos:centos7
1.2 容器中安装服务
yum -y install openssh-server epel-release initscripts
yum -y install nginx
# 生成ssh server的公私钥
/usr/sbin/sshd-keygen
1.3 配置初始化脚本
mkdir /script
vi /script/init.sh
#! /bin/bash
if [ -z $SSH_PWD ]
then
SSH_PWD="centos"
fi
echo $SSH_PWD | passwd --stdin root
/usr/sbin/sshd start && /usr/sbin/nginx -g "daemon off;"
1.4 把容器提交为镜像
docker container commit [OPTIONS] 容器名字或ID 新的镜像名[:TAG]
docker container commit centos:centos7 ssh_nginx:v1
1.5 测试镜像功能
拉起docker镜像,使用-e
参数给容器内的变量SSH_PWD
赋值,-p
将物理机的端口映射给docker
docker run -d --name ssh_nginx_test -p 822:22 -p 880:80 -e "SSH_PWD=123" ssh_nginx:v1 /bin/bash /script/init.sh
测试镜像的功能
ssh 127.0.0.1 -p 822
curl 127.0.0.1:880
2 使用dockerfile
Dockerfile可以通过 docker build 命令来构建镜像,运行该命令需要指定DockerFile的位置以及要构建的镜像名称
2.1 FROM 指定基础镜像
是必备的指令,并且必须是第一条指令。
FROM 基础镜像名
如果没有基础镜像 FROM scratch
2.2 RUN 执行命令
RUN 命令
,就像直接在命令行中输入的命令一样。
Dockerfile 中每一个指令都会建立一层,每一个 RUN 的行为,就会新建立一层临时镜像,在其上执行这些命令,执行结束后,commit 这一层的修改,构成新的镜像。所以一般使用\
来执行多行命令
RUN set -x; buildDeps='gcc libc6-dev make wget' \
&& apt-get update \
&& apt-get install -y $buildDeps \
&& wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" \
&& mkdir -p /usr/src/redis \
&& tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \
&& make -C /usr/src/redis \
&& make -C /usr/src/redis install \
&& rm -rf /var/lib/apt/lists/* \
&& rm redis.tar.gz \
&& rm -r /usr/src/redis \
&& apt-get purge -y --auto-remove $buildDeps
2.3 COPY 复制文件
复制文件
COPY [--chown=user:group] 宿主机源路径... 容器内路径
例子:
COPY --chown=bin files* /mydir/
2.4 ADD 复制文件
复制文件的时候,支持自动解压缩gzip
,bzip2
以及xz
。
ADD [--chown=user:group] 宿主机源路径... 容器内路径
添加多个文件时,容器内路径需要以
/
结尾
在 COPY 和 ADD 指令中选择的时候,可以遵循这样的原则,所有的文件复制均使用 COPY 指令,仅在需要自动解压缩的场合使用 ADD
例子:
ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz /
2.5 CMD 容器启动命令
指定默认的容器主进程的启动命令。在运行时可以指定新的命令来替代
镜像设置中CMD的这个默认命令。
CMD ["可执行文件", "参数1", "参数2"...]
注意使用双引号
或CMD shell命令
例子:
CMD ["nginx", "-g", "daemon off;"]
2.6 ENTRYPOINT 容器启动命令
指定容器启动程序及参数,在运行时指定的新命令会接在ENTRYPOINT的命令后充当参数
或后续命令
ENTRYPOINT ["可执行文件", "参数1", "参数2"...]
注意使用双引号
或ENTRYPOINT shell命令
例子:
ENTRYPOINT ["nginx", "-g", "daemon off;"]
2.7 ENV 设置环境变量
设置环境变量,后面的其它指令和是运行时的应用,都可以直接使用这里定义的环境变量
ENV key1=value1 key2=value2...
例子:
ENV PWD=345!@ DEBUG=on \
NAME="Happy Feet"
2.8 VOLUME 定义匿名卷
容器运行时应该尽量避免在容器内进行写操作,对于需要经常保存数据的应用,可以将写入的目录定义为VOLUME,避免用户在使用时不指定挂载。VOLUME自动挂载到容器外部,避免向容器内大量写入。
在运行容器时,可以使用-v
参数替代VOLUME的挂载配置
VOLUME 路径1 路径2
或VOLUME ["路径1", "路径2"...]
例子:
VOLUME /myvol
2.9 EXPOSE 暴露端口
声明容器运行时提供服务的端口,有2个作用:
- 帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;
- 在运行容器使用随机端口映射时,也就是
docker run -P
时,会自动随机映射 EXPOSE 的端口。
EXPOSE 端口1 [端口2...]
例子:
EXPOSE 80 443
2.10 WORKDIR 指定工作目录
指定工作目录,以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR 会帮你建立目录
WORKDIR 工作目录路径
例子:
WORKDIR /app
RUN echo "hello" > world.txt
2.11 HEALTHCHECK 健康检查
当在一个镜像指定了 HEALTHCHECK 指令后,用其启动容器,初始状态会为 starting
,在 HEALTHCHECK 指令检查成功后变为 healthy
,如果连续一定次数失败,则会变为 unhealthy
。
CMD命令的返回值决定了该次健康检查的成功与否:0
成功、1
失败。
HEALTHCHECK [选项] CMD 命令
:设置检查容器健康状况的命令,CMD后面的命令格式["可执行文件", "参数1", "参数2"...]
或shell命令
HEALTHCHECK NONE
:如果基础镜像有健康检查指令,屏蔽掉其健康检查指令
支持下列选项:
--interval=间隔
:两次健康检查的间隔,默认为 30 秒;
--timeout=时长
:健康检查命令运行超时时间,默认 30 秒;
--retries=次数
:当连续失败指定次数后,则将容器状态视为 unhealthy,默认 3 次。
例子:
HEALTHCHECK --interval=5s --timeout=3s CMD cat /var/run/auditd.pid > /dev/null
2.12 LABEL 为镜像添加元数据
以键值对的形式为镜像添加一些元数据
LABEL key=value key=value key=value ...
2.13 docker build
以dockerfile创建镜像,PATH为dockerfile所在目录
docker build -t name:tag PATH
2.14 dockerfile 例子,制作ssh+nginx镜像
准备工作目录
mkdir -p /docker/ssh_nginx
准备dockerfile、初始化脚本、健康检查脚本。
- dockerfile中定义
WORKDIR
目录为/script
,并在其中传入脚本;定义环境变量SSH_PWD
;暴露端口22
和80
- 初始化脚本使用环境变量
SSH_PWD
初始化用户密码,也可以在docker run时使用-e
修改环境变量SSH_PWD
- 健康检查脚本,检查服务状态,返回
0
或1
# dockerfile
vim /docker/ssh_nginx/dockerfile
FROM centos:centos7
RUN mkdir /etc/yum.repos.d/backup \
&& mv /etc/yum.repos.d/*.repo /etc/yum.repos.d/backup \
&& curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo \
&& curl -o /etc/yum.repos.d/epel.repo http://mirrors.aliyun.com/repo/epel-7.repo \
&& yum -y install openssh-server initscripts nginx \
&& /usr/sbin/sshd-keygen
WORKDIR /script
ENV SSH_PWD="123456"
ADD init.sh health_check.sh ./
EXPOSE 22 80
ENTRYPOINT ["/bin/bash", "init.sh"]
HEALTHCHECK --interval=5s --timeout=3s CMD bash health_check.sh
# 初始化脚本
vim /docker/ssh_nginx/init.sh
#! /bin/bash
echo $SSH_PWD | passwd --stdin root
/usr/sbin/sshd && /usr/sbin/nginx -g "daemon off;"
# 健康检查脚本
vim /docker/ssh_nginx/health_check.sh
if curl 127.0.0.1:80 &> /dev/null && ps -ef | grep "/usr/sbin/ssh[d]" > /dev/null ;then
exit 0
else
exit 1
fi
创建镜像
docker build -t ssh_nginx:v2 /docker/ssh_nginx/
简单运行镜像,使用随机端口映射
docker run -d -P ssh_nginx:v2
3 docker镜像分层
3.1 分层结构
为什么说是镜像分层技术,因为Docker 镜像是以层来组织的,我们可以通过命令 docker image inspect <image>
或者 docker inspect <image>
来查看镜像包含哪些层。下面是一个示例。
[root@docker ~]# docker image inspect busybox:latest
...
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:195be5f8be1df6709dafbba7ce48f2eee785ab7775b88e0c115d8205407265c5"
]
},
如上图所示,其中 RootFS
就是镜像 busybox:latest
的镜像层,只有一层,那么这层数据是存储在宿主机哪里的呢?好问题。动手实践的同学会在上面的输出中看到一个叫做 GraphDriver 的字段内容如下。
"GraphDriver": {
"Data": {
"LowerDir": "/var/lib/docker/overlay2/cd7a.../diff",
"MergedDir": "/var/lib/docker/overlay2/da4c.../merged",
"UpperDir": "/var/lib/docker/overlay2/da4c../diff",
"WorkDir": "/var/lib/docker/overlay2/da4c.../work"
},
"Name": "overlay2"
},
GraphDriver 负责镜像本地的管理和存储以及运行中的容器生成镜像等工作,可以将 GraphDriver 理解成镜像管理引擎,我们这里的例子对应的引擎名字是 overlay2(overlay 的优化版本)。除了 overlay 之外,Docker 的 GraphDriver 还支持 btrfs、aufs、devicemapper、vfs 等。
我们可以看到其中的 Data 包含了多个部分,这个对应 OverlayFS 的镜像组织形式,在下面我们再进行详细介绍。虽然我们上面的例子中的 busybox 镜像只有一层,但是正常情况下很多镜像都是由多层组成的。
这个时候很多同学应该会有这么一个疑问,镜像中的层都是读写的,那么我们运行着的容器的运行时数据是存储在哪里的呢?
镜像和容器在存储上的主要差别就在于容器多了一个读写层。镜像由多个只读层组成,通过镜像启动的容器在镜像之上加了一个读写层。下图是官方的一个配图。我们知道可以通过 docker commit
命令基于运行时的容器生成新的镜像,那么 commit 做的其中一个工作就是将读写层数据写入到新的镜像中。下图是一个示例图:
Container最上面是一个可写的容器层,以及若干只读的镜像层组成,Container的数据就存放在这些层中,这样的分层结构最大的特性是Copy-On-Write(写时复制):
1、新数据会直接存放在最上面的Container层。
2、修改现有的数据会先从Image层将数据复制到容器层,修改后的数据直接保存在Container层,Image层保持不变。
所有写入或者修改运行时容器的数据都会存储在读写层,当容器停止运行的时候,读写层的数据也会被同时删除掉。因为镜像层的数据是只读的,所有如果我们运行同一个镜像的多个容器副本,那么多个容器则可以共享同一份镜像存储层,下图是一个示例。
3.2 UnionFS
Docker 的存储驱动的实现是基于 Union File System,简称 UnionFS,中文可以叫做联合文件系统。UnionFS 设计将其他文件系统联合到一个联合挂载点的文件系统服务。
所谓联合挂载技术,是指在同一个挂载点同时挂载多个文件系统,将挂载点的源目录与被挂载内容进行整合,使得最终可见的文件系统将会包含整合之后的各层的文件和目录。
举个例子:比如我们运行一个 ubuntu 的容器。由于初始挂载时读写层为空,所以从用户的角度来看:该容器的文件系统与底层的 rootfs 没有区别;然而从内核角度来看,则是显式区分的两个层。
当需要修改镜像中的文件时,只对处于最上方的读写层进行改动,不会覆盖只读层文件系统的内容,只读层的原始文件内容依然存在,但是在容器内部会被读写层中的新版本文件内容隐藏。当 docker commit 时,读写层的内容则会被保存。
写时复制(Copy On Write)
这里顺便介绍一下写实复制技术。
我们知道 Linux 系统内核启动时首先挂载的 rootfs 是只读的,在系统正式工作之后,再将其切换为读写模式。Docker 容器启动时文件挂载类似 Linux 内核启动的方式,将 rootfs 设置为只读模式。不同之处在于:在挂载完成之后,利用上面提到的联合挂载技术在已有的只读 rootfs 上再挂载一个读写层。
读写层位于 Docker 容器文件系统的最上层,其下可能联合挂载多个只读层,只有在 Docker 容器运行过程中文件系统发生变化时,才会把变化的文件内容写到可读写层,并隐藏只读层的老版本文件,这就叫做 写时复制,简称 CoW。
3.3 OverlayFS
OverlayFS 将镜像层(只读)称为 lowerdir,将容器层(读写)称为 upperdir,最后联合挂载呈现出来的为 mergedir。文件层次结构可以用下图表示。
举个例子,下图是我们运行中的 busybox 容器的 docker inspect
的结果。
"GraphDriver": {
"Data": {
"LowerDir": "/var/lib/docker/overlay2/cd7a.../diff",
"MergedDir": "/var/lib/docker/overlay2/da4c.../merged",
"UpperDir": "/var/lib/docker/overlay2/da4c../diff",
"WorkDir": "/var/lib/docker/overlay2/da4c.../work"
},
"Name": "overlay2"
},
我们在容器中做的改动,都会在 upperdir 和 mergeddir 中体现。比如我们在容器中的 /tmp
目录下新建一个文件,那么在 upperdir 和 mergeddir 中就能够看到该文件。