Docker 应用容器化简介

将应用整合到容器并且运行起来的这个过程,成为“容器化”(Containerizing),有时也叫做“docker 化”

容器是为应用而生的,具体来说,容器能够简化应用的构建、部署和运行过程。

容器是为应用而生的,具体来说容器能够简化应用的构建、部署和运行过程。

完整的应用容器化过程

  • 编写应用代码
  • 创建一个 dockerfile,其中包含当前应用的描述、依赖以及该如何运行这个应用
  • 对该 Dockerfile 执行 docker image buuild 命令
  • 等待 Docker 将应用程序构建到 Docker 镜像中

一旦完成了应用容器化(就是将一个应用打包成为一个 Docker 镜像),就能以镜像的形式交付并以容器的方式运行了。

容器化的基本过程

1.获取应用代码

应用代码可以从网盘获取(https://pan.baidu.com/s/150UgIJPvuQUf0yO3KBLegg 提取码:pkx4)

安装解压工具
yum install unzip zip

解压后目录树
[root@centos7 wenjian1]# tree psweb-master/
psweb-master/
├── app.js
├── circle.yml
├── Dockerfile
├── package.json
├── README.md
├── test
│   ├── mocha.opts
│   └── test.js
└── views
    └── home.pug

该目录下包含了全部的应用源码,以及包含界面的单元测试的子目录。应用代码准备完成后开始准备 dockerfile 文件。

2. 分析 Dockerfile

如何区分指令是否会新建镜像层

基本原则,如果指令的作用是向镜像中添加新的文件或程序,那么这条指令就会新建镜像层,如果只是告诉 Docker 如何完成构建或者如何运行应用程序,那么就只会增加镜像的元数据。(查看 docker image history 指令中 SIZE字段不为零的指令都会新建镜像层)

可以通过 docker image history 指令查看构建镜像过程中都执行了哪些指令。

[root@centos7 ~]# docker image history web
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
5b6332f09295        2 hours ago         /bin/sh -c #(nop)  ENTRYPOINT ["node" "./app…   0B
c80feb912f62        2 hours ago         /bin/sh -c #(nop)  EXPOSE 8080                  0B
8505939396ef        2 hours ago         /bin/sh -c npm install                          20.9MB
4b407abc2177        2 hours ago         /bin/sh -c #(nop) WORKDIR /src/                 0B
a8746c9b5964        2 hours ago         /bin/sh -c #(nop) COPY multi:9f92f0273eda962…   2.54kB
77b46ae1192a        2 hours ago         /bin/sh -c npm --registry http://registry.cn…   71.2kB
c83f30273ef6        2 hours ago         /bin/sh -c npm info underscore                  70kB
98c0d8eb0498        2 hours ago         /bin/sh -c npm config set registry http://re…   108B
0fff0b2b1d2c        2 hours ago         /bin/sh -c npm config set strict-ssl false      72B
a0523d77bc2e        2 hours ago         /bin/sh -c apk add --update nodejs nodejs-npm   51.1MB
6dec65e1e96b        2 hours ago         /bin/sh -c sed -Ei "s@dl-cdn.alpinelinux.org…   93B
65dec364fcb5        37 hours ago        /bin/sh -c #(nop)  LABEL maintainer=13470549…   0B
d6e46aa2470d        5 days ago          /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B
<missing>           5 days ago          /bin/sh -c #(nop) ADD file:f17f65714f703db90…   5.57MB

从命令中可以看出,有九条命令的 SIZE 字段都不是为零,那么就会创建九个镜像层,使用 docker image inspect 查看镜像层

"Layers": [
    "sha256:ace0eda3e3be35a979cec764a3321b4c7d0b9e4bb3094d20d3ff6782961a8d54",
    "sha256:1ff38156f5541426a8a52b42c0aff406f8b1afbd407fcee72ccb8afbd99c1287",
    "sha256:34742d0c10b1377e76fc616f018ea034c4b401af71d62e6811e8733d380ed893",
    "sha256:bd3ff3de8971ff64db73109fdf549ffc9a6603414a1dbcbfbb36685ca5939672",
    "sha256:fbce533dbce32ef955fe3cb1229fa420ef14cd6af086a13ac35391c045943297",
    "sha256:fa54955e9bd8468249b289240676fe96210f582f6217dbbede965b66fd3f21e5",
    "sha256:603546c4c0c8b2e38ba5d83f72e6eecec6299cb5a5304a38f4546bae9a775b1d",
    "sha256:1c4f1c0632d25e9ca4661cac242a44151d8817daaf1968c8bc95f4fa9080159c",
    "sha256:e543d313776b466e29c12523e065f0446bb106bd1a4f96e5fd61ac1b1d73ca75"
]

创建自制镜像时最好可以使用官方的镜像 docker image build 创建镜像时查看镜像的创建过程更加清楚 。

Dockerfile 的使用小技巧

第一个问题,命令执行的方法

因为每次创建都会新建一层镜像层,很容易就会增加镜像的大小。对于 Docker 来说镜像越小越好,因为越大的镜像就意味着启动越慢,并且也会意味着不安全,所以镜像是越小越好。可以在 Dockerfile 中定义 run 命令时使用 && 符号或者 || 符号将多个命令组合到一起,这样可以将多个命令创建在一个镜像层中。

第二个问题,拉取工具的残留占用的空间

当使用某些命令,例如 wget 命令下载的软件包或者是文件,而我们想实现的功能只是将软件安装,但是下载完成后软件包也残留在了镜像当中,这就极大的增加了镜像的大小。所以可以使用一些其他方法将这些拉取工具进行清除或者卸载。

建造者模式

建造者模式(Builder Pattern)。无论采用哪种方式,通常都需要额外的培训,并且会增加构建的复杂度。

建造者模式最少要两个 Dockerfile ,一个用于开发环境,一个用于生产环境。

首先需要编写 Dockerfile.dev,它基于一个大型基础镜像(Base Image),拉取所需的构建工具,并构建应用。

接下来,需要基于 Dockerfile.dev 构建一个镜像,并用这个镜像创建一个容器。

这时再编写 Dockerfile.prod,它基于一个较小的基础镜像开始构建,并从刚才创建的容器中将应用程序相关的部分复制过来。

整个过程需要编写额外的脚本才能串联起来。

这种方式是可行的,但是比较复杂。

多阶段构建

多阶段构建方式使用一个 Dockerfile,其中包含多个 FROM 指令。每一个 FROM 指令都是一个新的构建阶段(Build Stage),并且可以方便的复制之前阶段的构件。

Dockerfile 简介

不可以因为 Dockerfile 是一个描述文件就轻视,因为 Dockerfile 能够实现开发和部署的无缝切换。

Dockerfile 应用

在代码目录中,有个名称为 Dockerfile 的文件。这个文件包含了对当前应用的描述,并且能知道 Docker 完成对容器的创建。

在 Docker 中,包含应用文件的目录通常被称为构建上下文(Build Context)。通常将 Dockerfile 放到构建上下文的根目录下。

文件开头字母是大写 D,这里是一个单词。像 dockerfile 或者是 Docker file 这种写法都不支持,仅支持 Dockerfile 这种写法。

[root@centos7 psweb-master]# cat Dockerfile 
# Test web-app to use with Pluralsight courses and Docker Deep Dive book
# Linux x64
FROM alpine
LABEL maintainer="1347054988@qq.com"
RUN sed -Ei "s@dl-cdn.alpinelinux.org@mirrors.aliyun.com@g" /etc/apk/repositories

# Install Node and NPM
RUN apk add --update nodejs nodejs-npm
RUN npm config set strict-ssl false 
RUN npm config set registry http://registry.cnpmjs.org
RUN npm info underscore
RUN npm --registry http://registry.cnpmjs.org info underscore

# Copy app to /src
COPY ./* /src/

WORKDIR /src/

# Install dependencies
RUN  npm install

EXPOSE 8080

ENTRYPOINT ["node", "./app.js"]

FROM 指令作用

相当于镜像的妈妈,指定一个基础镜像。后续的命令都会基于这个基础命令进行改变,或者进行一系列的操作。会在镜像上形成一个可写层。

截至目前,镜像的层级如下

基础镜像的结构

LABEL 指令作用

一个指令集的作用,在其中的指令都是以 key=vlaue 来指定的。之前的镜像制作者邮箱都是通过 MAINTAINER 来指定的,现在可以使用 LABEL 然后通过 k=v 的方式被 LABEL 标签包含。

在 LABEL 标签中可以添加多个信息,以镜像元数据的方式存在。

RUN 指令作用

在镜像内部执行一个命令,这个命令可能会生成一个新的镜像层。通过 yum 或者其他方式执行命令。

这里的 RUN apk add --update nodejs nodejs-npm 指的是将镜像内部的 apk 都会交给 nodejs nodejs-npm 来管理安装。

当前镜像的结构

COPY 指令作用

将宿主机的文件或者是执行文件复制到镜像中,然后可以使用其它指令在镜像内部执行。

当前的3层镜像

WORKDIR 指令作用

为镜像中之后运行命令的目录,指定工作目录,类似于 ansible 的 chdir 指令。

此命令不会新建镜像层,因为只是切换了工作目录。

该目录与镜像相关,并且会作为元数据记录到镜像配置中,不会生成新的镜像层。

然后,RUN npm install 指令会根据 package.json 中定义的配置信息,使用 npm 来安装当前的相关依赖包。

npm 命令会在前文设置的工作目录中执行,并且在新建镜像层来保存相应的依赖文件。

当前的4层镜像

EXPOSE 指令作用

暴露容器运行的端口,因为应用运行时的端口是 8080 所以直接使用 EXPOSE 指令映射到本地主机即可。

这样当用户访问时,可以直接访问宿主机的 IP 地址就可以访问。

因为只是进行了端口映射,信息同样会保存到镜像的元数据中,并不会生成一个新的镜像层。

ENTRYPOINT 指令作用

类似于 CMD 指令,但其不会被 docker run 的命令行参数指定的指令所覆盖,而且这些命令行参数会被当做参数给 ENTRYPOING

通过 ENTRYPOING 指令来指定当前镜像的入口程序。ENTRYPOING 指定的配置信息,也是通过镜像元数据的形式保存下来,并不是新增加镜像层。

制作镜像

下面的命令会构建并生成一个名为 web:latest 的镜像。命令最后的点(.)表示 Docker 在进行构建的时候,使用当前目录作为构建上下文。

docker build 命令会按行解析 Dockerfile 中的指令并顺序执行,部分指令只会增加或修改镜像的元数据信息。

[root@centos7 ~]# docker build -f Dockerfile .

查看创建出的镜像

[root@centos7 ~]# docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
<none>              <none>              5b6332f09295        17 minutes ago      77.7MB

为镜像命名

[root@centos7 ~]# docker tag 5b6332f09295 web
[root@centos7 ~]# docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
web                 latest              5b6332f09295        18 minutes ago      77.7MB

可以使用 docker image inspect web 来查看构建的镜像配置是否正确。

推送镜像到仓库

创建一个镜像后,可以直接将其保存在一个镜像仓库服务中。这样存储镜像会比较安全,并且可以被其他人访问使用。

Docker Hub 就是这样的一个开放的公共镜像仓库服务,并且这也是docker image push 命令默认的推送地址。

在推送镜像之前,先要使用 Docker ID 登录 Docker Hub。还需要为待推送的镜像打上合适的标签。

接下来介绍一下如何登录 Docker Hub,并将镜像推送到其中。

在后续的例子中,需要用自己的 Docker ID 替换示例中所使用的 ID。所以每当看到“nigelpoulton”时,记得替换为自己的 Docker ID。

$ docker login
Login with **your** Docker ID to push and pull images from Docker Hub...
Username: nigelpoulton
Password:
Login Succeeded

推送 Docker 镜像之前,还需要为镜像打标签。这是因为 Docker 在镜像推送的过程中需要如下信息。

  • Registry(镜像仓库服务)。
  • Repository(镜像仓库)。
  • Tag(镜像标签)

无须为 Registry 和 Tag 指定值。当没有为上述信息指定具体值的时候,Docker 会默认 Registry=docker.io、Tag=latest。

但是 Docker 并没有给 Repository 提供默认值,而是从被推送镜像中的 REPOSITORY 属性值获取。

这一点可能不好理解,下面会通过一个完整的例子来介绍如何向 Docker Hub 中推送一个镜像。

在前面的例子中执行了 docker image ls 命令。在该命令对应的输出内容中可以看到,镜像仓库的名称是 web。

这意味着执行 docker image push 命令,会尝试将镜像推送到 docker.io/web:latest 中。

但是其实 nigelpoulton 这个用户并没有 web 这个镜像仓库的访问权限,所以只能尝试推送到 nigelpoulton 这个二级命名空间(Namespace)之下。

为镜像打标签命令,为指定的镜像添加一个额外的标签,并且不需要覆盖已经存在的标签。

子啊此执行 docker image ls 命令,可以看到这个镜像现在已经有了两个标签,其中一个包含 Docker ID

[root@centos7 ~]# docker image tag web jasonchen/web
[root@centos7 ~]# docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
web                 latest              5b6332f09295        2 hours ago         77.7MB
jasonchen/web       latest              5b6332f09295        2 hours ago         77.7MB

提交镜像

$ docker image push nigelpoulton/web:latest
The push refers to repository [docker.io/nigelpoulton/web]
2444b4ec39ad: Pushed
ed8142d2affb: Pushed
d77e2754766d: Pushed
cd7100a72410: Mounted from library/alpine
latest: digest: sha256:68c2dea730...f8cf7478 size: 1160

因为权限问题,所以需要把上面例子中出现的 ID(jasonchen)替换为自己的 Docker ID,才能进行推送操作。

在接下来的例子当中,将使用 web:latest 这个标签。

运行应用程序

[root@centos7 ~]# docker container run -d --name web1 -p 80:8080 web
  • -d 后台运行
  • –name 为运行的镜像起名
  • -p 指定映射端口,将 8080 映射到 80 端口启动 web 镜像
[root@centos7 ~]# docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED              STATUS              PORTS                  NAMES
38f9ebc2c40c        web                 "node ./app.js"     About a minute ago   Up About a minute   0.0.0.0:80->8080/tcp   web1

从运行结果中可以看出,该容器的运行是将容器中的 8080 映射到了 80 端口之上

多阶段构建实现

示例源码可从百度网盘获取(https://pan.baidu.com/s/1M2paPY0f0lE5wm48HBk-Zw 提取码: 2e7s ),Dockerfile 位于app目录。

基于 linux 系统的应用,只能运行在 Linux 容器环境上。

Dockerfile 内容

FROM node:latest AS storefront
WORKDIR /usr/src/atsea/app/react-app
COPY react-app .
RUN npm install
RUN npm run build

FROM maven:latest AS appserver
WORKDIR /usr/src/atsea
COPY pom.xml .
RUN mvn -B -f pom.xml -s /usr/share/maven/ref/settings-docker.xml dependency:resolve
COPY . .
RUN mvn -B -s /usr/share/maven/ref/settings-docker.xml package -DskipTests

FROM java:8-jdk-alpine
RUN adduser -Dh /home/gordon gordon
WORKDIR /static
COPY --from=storefront /usr/src/atsea/app/react-app/build/ .
WORKDIR /app
COPY --from=appserver /usr/src/atsea/target/AtSea-0.0.1-SNAPSHOT.jar .
ENTRYPOINT ["java", "-jar", "/app/AtSea-0.0.1-SNAPSHOT.jar"]
CMD ["--spring.profiles.active=postgres"]

Dockerfile 中有 3 个 FROM 指令,每一个 FROM 指令构成一个单独的构建阶段。

各个阶段在内部从 0 开始编号,不过,示例中针对每个阶段定义了便于理解的名字。

  • 阶段 0 叫做 storefront
  • 阶段 1 叫做 appserver
  • 阶段 2 叫做 production

阶段 0 storefront 会首先拉取一个 600MB 的镜像,然后定义了一个工作目录。复制了一些代码到镜像中,并在之后使用 RUN 命令执行了软件。

阶段 1 appserver 会首先拉取了超过 700MB 的 maven:latest 镜像。然后通过 2 个 COPY 指令和 2 个 RUN 指令生成了 4 个镜像层。

这个阶段同样会创建出一个非常大的包含许多构建工具和少量应用程序代码的镜像。

阶段 2 production 阶段首先会拉取一个 Java 镜像,大概 150 MB 然后会创建一个用户,并切换了一个工作目录。通过 –from 指向镜像中的内容复制到本地的镜像上。并复制到不同的目录中。

最后通过 ENTRYPOING 指令指定切入点,使用 CMD 指令执行命令