5. 制作docker镜像

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 复制文件

复制文件的时候,支持自动解压缩gzipbzip2 以及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 路径2VOLUME ["路径1", "路径2"...]

例子:

VOLUME /myvol

2.9 EXPOSE 暴露端口

声明容器运行时提供服务的端口,有2个作用:

  1. 帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;
  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;暴露端口2280
  • 初始化脚本使用环境变量SSH_PWD初始化用户密码,也可以在docker run时使用-e修改环境变量SSH_PWD
  • 健康检查脚本,检查服务状态,返回01
# 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 或者 docker inspect 来查看镜像包含哪些层。下面是一个示例。

[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 还支持 btrfsaufsdevicemappervfs 等。

我们可以看到其中的 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"
        },

我们在容器中做的改动,都会在 upperdirmergeddir 中体现。比如我们在容器中的 /tmp 目录下新建一个文件,那么在 upperdirmergeddir 中就能够看到该文件。

你可能感兴趣的:(5. 制作docker镜像)