DockerFile最佳实践及docker常规操作

官方文档的概述

Best practices for writing Dockerfiles​

容器的每一个阶段都应该是短暂的

通过 Dockerfile 构建的镜像所启动的容器应该尽可能短暂 (ephemeral)。短暂意味着可以很快地启动并且终止

使用 .dockerignore 排除构建无关文件

.dockerignore 语法与 .gitignore 语法一致。使用它排除构建无关的文件及目录,如 node_modules

使用multistage构建

多阶段构建可以有效减小镜像体积,特别是对于需编译语言而言,一个应用的构建过程往往如下

安装编译工具
安装第三方库依赖
编译构建应用

在前两步会有大量的镜像体积冗余,使用多阶段构建可以避免这一问题,参考前端应用的dockerfile示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
FROM artifactory.momenta.works/docker/node:lts-alpine as builder 

# env set
ENV EVA_ENTRYPOINT=/api


WORKDIR /
COPY package.json /
RUN npm install --registry=https://registry.npm.taobao.org

RUN npm run build

FROM nginx:alpine
LABEL maintainer="ZengPing An <anzengping@momenta.ai>"

COPY /nginx/ /etc/nginx/

COPY --From=builder /usr/share/nginx/html/

EXPOSE 80

避免安装不必要的包

减小体积,减少构建时间。

如前端应用使用 npm install --production 只装生产环境所依赖的包。

减小容器耦合

如一个web应用将会包含三个部分,web 服务,数据库与缓存。
把他们解耦到多个容器中,方便横向扩展。
如果你需要网络通信,则可以将他们至于一个网络下。

减少镜像层数

只有 RUN, COPY, ADD 会创建层数, 其它指令不会增加镜像的体积尽可能使用多阶段构建

使用以下方法安装依赖

1
RUN yum install -y node python go

错误的方法安装依赖,这将增加镜像层数

1
2
3
RUN yum install -y node 
RUN yum install -y python
RUN yum install -y go

充分利用构建缓存

在镜像的构建过程中 docker 会遍历 Dockerfile 文件中的所有指令,顺序执行。
对于每一条指令,docker 都会在缓存中查找是否已存在可重用的镜像,否则会创建一个新的镜像

我们可以使用

1
docker build --no-cache

跳过缓存

ADD 和 COPY 将会计算文件的 checksum 是否改变来决定是否利用缓存RUN 仅仅查看命令字符串是否命中缓存,
如 RUN apt-get -y update 可能会有问题

如一个 node 应用,可以先拷贝 package.json 进行依赖安装,然后再添加整个目录,可以做到充分利用缓存的目的。

1
2
3
4
5
6
7
FROM node:10-alpine as builder  
WORKDIR /code
ADD package.json /code
# 此步将可以充分利用 node_modules 的缓存
RUN npm install --production
ADD . /code
RUN npm run build

以上就是使用docker部署前端应用的全部流程及注意事项。

遇到的问题

目前的镜像存在两个问题,导致每次部署时间过长,不利于产品的快速交付,没有快速交付,也就没有敏捷开发 (Agile)

构建镜像时间过长构建镜像大小过大,多时甚至 1G+

解决办法

从devdependencies下手

对于每次部署,如果能够减少无用包的下载,便能够节省很多镜像构建时间。eslint,mocha,chai等代码风格测试模块可以放到devDependencies中。在生产环境中使用npm install –production装包。

我们注意到,相对于项目的源文件来讲,package.json是相对稳定的。如果没有新的安装包需要下载,则再次构建镜像时,无需重新装包。则可以在 npm install 上节省一半的时间。

利用镜像缓存

对于 ADD 来讲,如果需要添加的文件内容的 checksum 没有发生变化,则可以利用缓存。把 package.json 与源文件分隔开写入镜像是一个很好的选择。目前,如果没有新的安装包更新的话,可以节省一半时间

多阶段构建

得益于缓存,现在镜像构建时间已经快了不少。但是,此时镜像的体积依旧过于庞大,这也将会导致部署时间的加长。原因如下

考虑下每次 CI/CD 部署的流程

在构建服务器构建镜像把镜像推至镜像仓库服务器在生产服务器拉取镜像,启动容器

显而易见,镜像体积过大会造成传输效率低下,增加每次部署的延时

即使构建服务器与生产服务器在同一节点下,没有延时的问题。减少镜像体积也能够节省磁盘空间(使用较早期的镜像)

使用文件存储服务

分析一下 50M+ 的镜像体积,nginx:10-alpine 的镜像是16M,剩下的40M是静态资源。

如果把静态资源给上传到文件存储服务,即OSS,并使用 CDN 对 OSS 进行加速。则没有必要打入镜像了,此时镜像大小会控制在 20M 以下

关于静态资源,可以分类成两部分

/static,此类文件在项目中直接引用根路径,打包时复制进 /public 下,需要被打入镜像/build,此类文件需要 require/import 引用,会被 webpack 打包并加 hash 值,并通过 publicPath 修改资源地址。可以把此类文件上传至 oss,并加上永久缓存,不需要打入镜像
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
FROM node:10-alpine as builder

ENV PROJECT_ENV production
ENV NODE_ENV production

# http-server 不变动也可以利用缓存
WORKDIR /code

ADD package.json /code
RUN npm install --production

ADD . /code

# npm run uploadOss 是把静态资源上传至 oss 上的脚本文件
RUN npm run build && npm run uploadOss

# 选择更小体积的基础镜像
FROM nginx:10-alpine
COPY --from=builder code/public/index.html code/public/favicon.ico /usr/share/nginx/html/
COPY --from=builder code/public/static /usr/share/nginx/html/static

Docker常用命令大全

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
Management Commands:
builder Manage builds
config Manage Docker configs
container Manage containers
context Manage contexts
image Manage images
network Manage networks
node Manage Swarm nodes
plugin Manage plugins
secret Manage Docker secrets
service Manage services
stack Manage Docker stacks
swarm Manage Swarm
system Manage Docker
trust Manage trust on Docker images
volume Manage volumes

Commands:
attach Attach local standard input, output, and error streams to a running container
build Build an image from a Dockerfile
commit Create a new image from a container's changes
cp Copy files/folders between a container and the local filesystem
create Create a new container
deploy Deploy a new stack or update an existing stack
diff Inspect changes to files or directories on a container's filesystem
events Get real time events from the server
exec Run a command in a running container
export Export a container's filesystem as a tar archive
history Show the history of an image
images List images
import Import the contents from a tarball to create a filesystem image
info Display system-wide information
inspect Return low-level information on Docker objects
kill Kill one or more running containers
load Load an image from a tar archive or STDIN
login Log in to a Docker registry
logout Log out from a Docker registry
logs Fetch the logs of a container
pause Pause all processes within one or more containers
port List port mappings or a specific mapping for the container
ps List containers
pull Pull an image or a repository from a registry
push Push an image or a repository to a registry
rename Rename a container
restart Restart one or more containers
rm Remove one or more containers
rmi Remove one or more images
run Run a command in a new container
save Save one or more images to a tar archive (streamed to STDOUT by default)
search Search the Docker Hub for images
start Start one or more stopped containers
stats Display a live stream of container(s) resource usage statistics
stop Stop one or more running containers
tag Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE
top Display the running processes of a container
unpause Unpause all processes within one or more containers
update Update configuration of one or more containers
version Show the Docker version information
wait Block until one or more containers stop, then print their exit codes

docker 释放空间的命令(同时清除镜像与容器)

1
docker system prune -a

该命令会将全部内容删的干干净净,包括你的自定义image和container

docker 注意事项

dockerfile无法识别env中的空格,换行要使用\来进行。

docker 批量删除

docker 使用一段时间之后,可能堆积很多用不着的,或者编译错误的镜像,一个一个删除就很麻烦,需要一个批量删除的方法,如下:

1
docker rmi $(docker images | grep "none" | awk '{print $3}')

上面这条命令,可以删除所有名字中带 “none” 关键字的镜像,即可以把所有编译错误的镜像删除。

含义:

1
2
3
4
5
6
7
docker images:查询本地镜像列表;

grep 后面的参数,就是筛选出名字中包含这个参数的镜像。

awk:文本分析工具,这里的
'$1 == &quot;&lt;none>&quot; &amp;&amp; $2 == &quot;&lt;none>&quot; {print $3}'
是它的判断条件和具体操作;

为了方便理解,这里再简化下语句:

1
2
3
4
docker images | awk '{print $0}'
# 返回结果:
REPOSITORY TAG IMAGE ID CREATED SIZE
mono/jexus 6.24 1972cdc31613 4 days ago 727.9 MB

然后将结果与下面的两个语句进行对比

1
2
3
4
5
6
7
8
9
docker images | awk '{print $1}'
# 返回结果:
REPOSITORY
mono/jexus

docker images | awk '{print $3}'
# 返回结果:
IMAGE
1972cdc31613

不难看出 “$” 后的参数分别对应的结果是有规律的。实际上,awk认为文本文件都是结构化的,它将每一个输入行定义为一个记录,行中的每个字符串定义为一个域(段),域和域之间使用分割符分割。awk会把每行进行一个拆分,用相应的命令对拆分出来的“段”进行处理。

行工作模式,读入文件的每一行,会把一行的内容,存到$0里;
使用内置的变量FS(段的分隔符,默认用的是空白字符),分割这一行,把分割出来的每个段存到相应的变量$(1-100);
输出的时候按照内置变量OFS(out FS);读入下一行继续操作;

Happy Hacking~