文章目录
本文介绍了如何容器化(Docker化)一个应用。
- 介绍了Dockerfile基本的工作机制,并用docker image build命令创建了一个新的镜像。镜像创建后,基于该镜像启动了一个容器。
- 多阶段构建提供了一种简单的方式,能够构建更加精简的生产环境镜像。
- Dockerfile是一个将应用程序文档化的有力工具:
首先编写应用代码,然后创建一个Dockerfile来定义这个应用,最后使用docker image build命令构建镜像。
一. 应用的容器化——简介
完整的应用容器化过程主要分为以下几个步骤。
- 创建Dockerfile,其中包括当前应用的描述、依赖以及该如何运行这个应用。
- 创建镜像:对Dockerfile执行docker image build命令构建镜像
- 应用容器化完成(即应用被打包为一个Docker镜像),就能以镜像的形式交付并以容器的方式运行了。
二. 单体应用容器化
1. 获取代码与分析Dockfile
代码地址:https://github.com/nigelpoulton/psweb.git 。如下代码结构:
$ cd psweb
$ ls -l
total 28
-rw-r--r-- 1 root root 341 Sep 29 16:26 app.js
-rw-r--r-- 1 root root 216 Sep 29 16:26 circle.yml
-rw-r--r-- 1 root root 338 Sep 29 16:26 Dockerfile
。。。
Dockerfile包含了对当前应用的描述,并且能指导Docker完成镜像的构建。在Docker当中,包含应用文件的目录通常被称为构建上下文(Build Context)。通常将Dockerfile放到构建上下文的根目录下
。
有一点是必须的:文件开头字母是大写D,这里是一个单词。像“dockerfile”或者“Docker file”这种写法都是不允许的。
$ cat Dockerfile
FROM alpine # 以alpine镜像作为当前镜像基础,
LABEL maintainer="nigelpoulton@hotmail.com" # 维护者(maintainer)
RUN apk add --update nodejs nodejs-npm # 安装Node.js和NPM,
COPY . /src # 将应用的代码复制到镜像当中,设置新的工作目录,安装依赖包,
WORKDIR /src
RUN npm install
EXPOSE 8080 # 网络端口
ENTRYPOINT ["node", "./app.js"] # 将app.js设置为默认运行的应用。
Dockerfile主要包括两个用途
- 描述当前应用
- 指导Docker完成应用的容器化(创建一个包含当前应用的镜像)
Dockerfile能实现开发和部署两个过程的无缝切换、帮助新手快速熟悉这个项目。如下Dockerfile配置描述:
配置 | 说明 |
---|---|
基础镜像层 | 1. 每个Dockerfile文件第一行都是FROM指令。FROM指定的镜像,会作为当前镜像的基础镜像层,当前应用的剩余内容会作为新增镜像层添加到基础镜像层之上。 2. 本例中的应用基于Linux操作系统,所以在FROM指令当中引用Linux基础镜像; |
label标签 | 1. 一种描述信息,沟通方式。每个标签是一个键值对(Key-Value),在一个镜像当中可以通过增加标签的方式来为镜像添加自定义元数据。 2. 通过标签(LABEL)方式指定了当前镜像的维护者为“nigelpoulton@hotmail. com”。 |
安装镜像层 | 1. RUN apk add --update nodejs npm 指令使用alpine的apk包管理器将nodejs和nodejs-npm安装到当前镜像之中 。2. RUN指令会在FROM指定的alpine基础镜像之上,新建一个镜像层来存储这些安装内容。 |
工作目录 | Dockerfile通过WORKDIR指令,为Dockerfile中尚未执行的指令设置工作目录。该目录与镜像相关,并且会作为元数据记录到镜像配置中,但不会创建新的镜像层。 |
安装相关依赖 | 1. run npm install指令会根据package.json中的配置信息,使用npm来安装当前应用的相关依赖包。 2. npm命令会在工作目录中执行,并且在镜像中新建镜像层来保存相应的依赖文件。 |
暴露端口 | 通过EXPOSE 8080设置应用端口 |
指定程序入口 | 1. 通过ENTRYPOINT指令来指定当前镜像的入口程序。 2. ENTRYPOINT指定的配置信息也是通过镜像元数据的形式保存下来,而不是新增镜像层。 |
如下产生了四个镜像
2. 容器化当前应用(构建具体的镜像)
如下命令会构建并生成一个名为web:latest的镜像。命令最后的点(.)表示Docker在进行构建的时候,使用当前目录作为构建上下文
。
docker image build -t web:latest .
[+] Building 55.2s (10/10) FINISHED docker:desktop-linux
=> [internal] load build definition from Dockerfile
=> => transferring dockerfile: 400B
=> [internal] load metadata for docker.io/library/alpine:latest
=> [internal] load .dockerignore
=> => transferring context: 2B
=> CACHED [1/5] FROM docker.io/library/alpine:latest
=> [internal] load build context
=> => transferring context: 7.65kB
=> [2/5] RUN apk add --update nodejs npm curl
=> [3/5] COPY . /src
=> [4/5] WORKDIR /src
=> [5/5] RUN npm install
=> exporting to image
=> => exporting layers
=> => writing image sha256:673850491a21f99a0f063f9e69f6649ba5dcdc
=> => naming to docker.io/library/web:latest
View build details: docker-desktop://dashboard/build/desktop-linux/desktop-linux/b80dnc9ketudf6y5nyoiob5vs
命令执行结束后,检查本地Docker镜像库是否包含了刚才构建的镜像。
$ docker image ls
REPO TAG IMAGE ID CREATED SIZE web latest
fc69fdc4c18e 10 seconds ago 64.4MB
恭喜,应用容器化已经成功了!
可以通过docker image inspect web:latest
来确认刚刚构建的镜像配置是否正确。
3.推送镜像到仓库
Docker Hub是一个开放的公共镜像仓库服务,并且这也是docker image push命令默认的推送地址。
在推送镜像之前,需要先使用Docker ID登录Docker Hub。除此之外,还需要为待推送的镜像打上合适的标签。
登录
docker login
Authenticating with existing credentials...
Login Succeeded
# 或
docker login
Login with **your** Docker ID to push and pull images from Docker Hub...
Username: nigelpoulton
Password:
Login Succeeded
Docker在镜像推送的过程中需要如下信息:
- ● Registry(镜像仓库服务)● Repository(镜像仓库)● Tag(镜像标签)。
- 当Registry和Tag不指定值时,Docker会默认Registry=docker.io、Tag=latest。但是Docker并没有给Repository提供默认值,而是从被推送镜像中的REPOSITORY属性值获取。
push
执行docker image push命令,会尝试将镜像推送到docker.io/web:latest中。
二级命名空间:gaoliang00这个用户并没有web这个镜像仓库的访问权限,所以只能尝试推送到gaoliang00这个二级命名空间(Namespace)之下。因此需要使用这个ID,为当前镜像重新打一个标签。
额外的标签
docker image tag web:latest gaoliang00/web:latest
docker image list
REPOSITORY TAG IMAGE ID CREATED SIZE
gaoliang00/web latest 673850491a21 17 minutes ago 94.9MB
web latest 673850491a21 17 minutes ago 94.9MB
为镜像打标签命令的格式是docker image tag <current-tag> <new-tag>
,其作用是为指定的镜像添加一个额外的标签,并且不需要覆盖
已经存在的标签。
docker image push gaoliang00/web
Using default tag: latest
The push refers to repository [docker.io/gaoliang00/web]
744f820bd5f4: Pushed
。。。
9110f7b5208f: Pushed
latest: digest: sha256:4c822e80a71c328a1f2aeaf11221c6c724dca7bd88822da35bb9160925823f56 size: 1366
4. 运行应用程序
从app.js这个文件内容中可以看出,这其实就是一个在8080端口提供Web服务的应用程序。下面的命令会基于web:latest这个镜像,启动一个名为c1的容器。
docker container run -d --name c1 -p 81:8080 web:latest
docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
9d9b6776cbe9 web:latest "node ./app.js" About a minute ago Up About a minute 0.0.0.0:8888->8080/tcp c2
49ccc5e94a9a web:latest "node ./app.js" 3 minutes ago Up 3 minutes 0.0.0.0:81->8080/tcp c1
这样应用程序已经容器化并成功运行了!
如果没有出现这样的界面,尝试执行下面的检查来确认原因所在。
- 使用docker container ls指令来确认容器已经启动并且正常运行。容器名称是c1,并且从输出内容中能看到0.0.0.0:81->8080/tcp。
- 确认防火墙或者其他网络安全设置没有阻止访问Docker主机的80端口。
5. 小结
1.如何区分命令是否会新建镜像层
一个基本的原则是,如果指令的作用是向镜像中增添新的文件或者程序,那么这条指令就会新建镜像层;如果只是告诉Docker如何完成构建或者如何运行应用程序,那么就只会增加镜像的元数据。
2.查看构建镜像过程中执行了哪些指令
docker image history web:latest
IMAGE CREATED BY SIZE
fc6..18e /bin/sh -c #(nop) ENTRYPOINT ["node" "./a... 0B
334..bf0 /bin/sh -c #(nop) EXPOSE 8080/tcp 0B
b27..eae /bin/sh -c npm install 14.1MB
932..749 /bin/sh -c #(nop) WORKDIR /src 0B
052..2dc /bin/sh -c #(nop) COPY dir:2a6ed1703749e80... 22.5kB
c1d..81f /bin/sh -c apk add --update nodejs nodejs-npm 46.1MB
336..b92 /bin/sh -c #(nop) LABEL maintainer=nigelp... 0B
3fd..f02 /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
/bin/sh -c #(nop) ADD file:093f0723fa46f6c... 4.15MB
可以观察到只有4条指令会新建镜像层(就是那些SIZE列对应的数值不为零的指令),其他指令只在镜像中新增了元数据信息。
docker image inspect指令来确认确实只有4个层被创建了。
docker image inspect web:latest
<Snip>
},
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:cd7100...1882bd56d263e02b6215",
"sha256:b3f88e...cae0e290980576e24885",
"sha256:3cfa21...cc819ef5e3246ec4fe16",
"sha256:4408b4...d52c731ba0b205392567"
]
},
3.了解镜像构建的过程
运行临时容器>在该容器中运行Dockerfile中的指令>将指令运行结果保存为一个新的镜像层>删除临时容器。
Step 3/8 : RUN apk add --update nodejs nodejs-npm
---> Running in e690ddca785f << Run inside of temp container
fetch http://dl-cdn...APKINDEX.tar.gz
fetch http://dl-cdn...APKINDEX.tar.gz
(1/10) Installing ca-certificates (20171114-r0)
<Snip>
OK: 61 MiB in 21 packages
---> c1d31d36b81f << Create new layer
Removing intermediate container << Remove temp container
Step 4/8 : COPY . /src
三. 生产环境中的多阶段构建
在传统的 Docker 构建中,你可能会将所有构建依赖和工具保留在最终镜像中。这通常会导致镜像体积较大。多阶段构建允许你在多个阶段中进行构建,最终只将所需的部分复制到最终镜像中
。这可以显著减少镜像的体积。
多阶段构建方式使用一个Dockerfile,其中包含多个FROM指令。每一个FROM指令都是一个新的构建阶段(Build Stage),并且可以方便地复制之前阶段的构件。
使用仓库如下来举例说明,https://github.com/david-gao1/atsea-sample-shop-app/blob/master/app/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 AS production
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叫作storefront。● 阶段1叫作appserver。● 阶段2叫作production。
storefront阶段拉取了大小超过600MB的node:latest镜像,然后设置了工作目录,复制一些应用代码进去,然后使用2个RUN指令来执行npm操作。这会生成3个镜像层并显著增加镜像大小。
appserver阶段拉取了大小超过700MB的maven:latest镜像。然后通过2个COPY指令和2个RUN指令生成了4个镜像层。
production阶段拉取java:8-jdk-alpine镜像,这个镜像大约150MB,明显小于前两个构建阶段用到的node和maven镜像。这个阶段会创建一个用户,设置工作目录,从store front阶段生成的镜像中复制一些应用代码过来。之后,设置一个不同的工作目录,然后从appserver阶段生成的镜像中复制应用相关的代码。最后,production设置当前应用程序为容器启动时的主程序。
重点在于COPY --from
指令,它从之前的阶段构建的镜像中仅(how?)复制生产环境相关的应用代码,而不会复制生产环境不需要的构件。
可见它明显比之前阶段拉取和生成的镜像要小。最终,无须额外的脚本,仅对一个单独的Dockerfile执行docker image build命令,就创建了一个精简的生产环境镜像。多阶段构建是随Docker 17.05版本新增的一个特性,用于构建精简的生产环境镜像。
四. 应用容器化命令
docker image build 。命令会读取Dockerfile,并将应用程序容器化。使用-t参数为镜像打标签,使用-f参数指定Dockerfile的路径和名称,使用-f参数可以指定位于任意路径下的任意名称的Dockerfile。构建上下文是指应用文件存放的位置,可能是本地Docker主机上的一个目录或一个远程的Git库。 | |
Dockerfile中的FROM指令用于指定要构建的镜像的基础镜像。它通常是Dockerfile中的第一条指令。 | |
Dockerfile中的RUN指令用于在镜像中执行命令,这会创建新的镜像层。每个 RUN指令创建一个新的镜像层。 | |
Dockerfile中的COPY指令用于将文件作为一个新的层添加到镜像中。通常使用 COPY指令将应用代码赋值到镜像中。 | |
Dockerfile中的EXPOSE指令用于记录应用所使用的网络端口。 | |
Dockerfile中的ENTRYPOINT指令用于指定镜像以容器方式启动后默认运行的程序。 | |
其他的Dockerfile指令还有LABEL、ENV、ONBUILD、HEALTHCHECK、CMD等。 |