看到文章标题,有一定云原生相关技术经验的读者可能会说,都2021
年了,怎么还在写Docker
相关的文章?
的确如此,现如今容器引擎Docker
的替代品有很多。加上用了k8s
之后,大家对Docker
的关注或许没有那么多了,很多场景下为了方便也没有自己做镜像的强烈需求,乃拿来主义
~
k8s
也在新版本中说 “不再支持” Docker
,详情可以参考Don‘t Panic: Kubernetes and Docker
这个话题也的确被不少标题党炒作了一番,实际上k8s
仅仅是放弃其对dockershim
组件的支持,更推荐的k8s
运行时是兼容CRI
的containerd
之类的底层运行时
因此,Docker
作为我从大学开始接触云原生相关领域的第一个开源工具,同时也作为容器技术和云原生领域中家喻户晓的名词。除去其经营线路外,不得不说,作为Docker
技术本身还是值得肯定的,因此我觉得还是有必要再提一下的
根据个人经验和官方文档阅读,之前整理写过Jenkins Pipeline语法概要,现在来写一篇用于制作Docker
镜像的Dockerfile
语法概要,部分内容翻译自官方文档Dockerfile reference
Dockerfile
可以认为是一个脚本,包含如何构建Docker
镜像的说明。实际上,这些指令是一组在Docker
环境中自动执行的命令,以构建特定的Docker
镜像
Docker
镜像有着分层的概念,因此制作任何一个Docker
镜像都需要有一个基础镜像,FROM
用于指定基础镜像,语法为
FROM <image>:<tag>
其中基础镜像的tag
可以不指定,即默认使用latest
,在制作时尽量要使用官方的镜像作为基础镜像,如果想制定一个小型轻量的镜像,基础镜像可以选择Alpine
另外需要提到的是,这里说了任何镜像都需要有一个基础镜像,那么问题来了,就好比是先有鸡还是先有蛋的问题,基础镜像的“祖宗”是什么呢?能不能在构建时不以任何镜像为基础呢?答案是肯定的,可以选用scratch
,具体我就不展开了,可以参考:baseimages
FROM scratch
LABEL
一般用来添加镜像的 “元数据” ,没有实际作用。常用于声明镜像作者,licensce
等信息,写法为<key>=<value>
,语法为
LABEL "com.example.vendor"="ACME Incorporated"
LABEL com.example.label-with-value="foo"
LABEL version="1.0"
LABEL description="This text illustrates that label-values can span multiple lines."
在镜像构建后并成功运行容器,可以通过inspect
查看
# docker image inspect --format=‘‘ myimage
{
"com.example.vendor": "ACME Incorporated",
"com.example.label-with-value": "foo",
"version": "1.0",
"description": "This text illustrates that label-values can span multiple lines."
}
如果要声明作者,语法为
LABEL maintainer="SvenDowideit@home.org.au"
之前直接通过指令MAINTAINER
指定,这种写法已经废弃掉了
ENV
用来设置环境变量,一旦环境变量设置,就可以在Dockerfile
后面的内容及容器运行后的应用中获取使用这个环境变量,ENV
的写法也是<key>=<value>
,语法为
ENV MY_NAME="John Doe"
ENV MY_DOG=Rex\ The\ Dog
ENV MY_CAT=fluffy
COPY
和ADD
都是用于在构建时往镜像中复制文件或目录的,并且两者都支持在复制时修改文件或目录的属主和属组,语法为
ADD [--chown=<user>:<group>] <src>... <dest>
ADD [--chown=<user>:<group>] ["<src>",... "<dest>"]
COPY [--chown=<user>:<group>] <src>... <dest>
COPY [--chown=<user>:<group>] ["<src>",... "<dest>"]
两者的使用差不多,但ADD
功能更丰富
支持URL
例如源路径是文件的URL
链接,构建时自动进行下载,下载后放到目标路径下,文件权限为600
压缩包自动解压
例如tar
、gzip
、bzip2
、xz
格式的压缩包,ADD
指令将会自动解压缩这个压缩文件到目标路径去
ARG
设置的是构建时的参数,也可以理解为构建时的环境变量,与ENV
的不同是只在构建时生效,生成的镜像中是不存在的
可以在ARG
中同时声明参数名和参数值
也可以只声明参数名,在构建时通过–build-arg
<参数名>=<值>的形式来赋值,赋值的前提是在Dockerfile
中进行了声明,否则会出现警告
语法为
ARG <name>[=<default value>]
RUN
指令表示在当前的镜像层之上的新层中执行命令并将结果提交,用户Dockerfile
的下一步操作,语法为
RUN /bin/bash -c ‘source $HOME/.bashrc; RUN mkdir /test
在RUN
命令中也可以使用exec
格式来避免shell
字符串损坏,语法为
RUN ["/bin/bash", "-c", "echo hello"]
RUN
作为Dockerfile
中最为常用的指令,在使用时有以下建议:
在RUN
指令执行过程中,产生的中间镜像会被当做缓存在下一次构建时使用,如果不想使用缓存,使其失效,可以在build
时添加--no-cache
尽量把所有的RUN
指令写到一起,如果是多条shell
命令,可以不用每条命令都添加RUN
,更好的做法是通过\
换行,通过&&
连接多个指令,这样对构建生成的镜像的大小优化是很有帮助的,语法为
RUN set -x && yum install -y epel-release make gcc gcc-c++
WORKDIR
指令为Dockerfile
中的任何RUN
、CMD
、ENTRYPOINT
、COPY
和ADD
指令设置工作目录。如果工作目录不存在,即使它没有在后续的Dockerfile
指令中使用,它也会被创建
WORKDIR
指令可以在Dockerfile
中使用多次。如果提供了一个相对路径,它将相对于前一个WORKDIR
指令的路径,语法为
WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd
这个Dockerfile
中的最后一个pwd
命令的输出是/a/b/c
WORKDIR
指令也可以解析之前使用ENV
设置的环境变量,只能使用在Dockerfile
中显式设置的环境变量,语法为
ENV DIRPATH=/path
WORKDIR $DIRPATH/$DIRNAME
RUN pwd
这里的最终路径是/path/$DIRNAME
EXPOSE
指令声明了容器在运行时监听指定的网络端口,可以指定端口是监听TCP
还是UDP
,默认为TCP
EXPOSE
指令实际上并不发布端口,即端口限制,它的作用仅仅是作为构建映像的人和运行容器的人之间的一种文档,关于要发布哪些端口。当运行容器时,要实际发布端口,使用docker
运行中的-p
参数来发布和映射一个或多个端口,或者直接使用-P
来自动随机映射EXPOSE
声明的端口
语法为
EXPOSE <port> [<port>/<protocol>...]
这个话题稍微复杂,实际上用的场景也不会有这么多
CMD
和ENTRYPOINT
都是指定容器将如何运行
CMD
的主要目的是为执行容器提供默认值。这些默认值可以包括可执行文件,也可以省略可执行文件,在这种情况下,必须指定一个ENTRYPOINT
指令
CMD
指令有三种形式
CMD ["executable","param1","param2"] # 首选的exec格式
CMD ["param1","param2"] # 只传递参数,作为ENTRYPOINT的默认参数
CMD command param1 param2 (shell form) # shell的形式
在Dockerfile
中只能有一条CMD
指令,如果指定了多条,只有最后一条会生效
ENTRYPOINT
有两种形式
ENTRYPOINT ["executable", "param1", "param2"] # exec格式
ENTRYPOINT command param1 param2 # shell形式
如果指定了ENTRYPOINT
,则CMD
将只是提供参数,传递给ENTRYPOINT
docker run <image>
的命令行参数将被附加在exec
类型的ENTRYPOINT
的所有元素之后,并将覆盖使用CMD
指定的所有元素。这允许参数被传递给ENTRYPOINT
例如,docker run <image> -d
将传递-d
参数给ENTRYPOINT
也可以使用docker run --entrypoint
覆盖ENTRYPOINT
指令
官方有一段关于CMD
和ENTRYPOINT
组合出现时的结果
https://docs.docker.com/engine/reference/builder/#understand-how-cmd-and-entrypoint-interact
没有 ENTRYPOINT | ENTRYPOINT exec_entry p1_entry | ENTRYPOINT [“exec_entry”, “p1_entry”] | |
---|---|---|---|
没有 CMD | 报错 | /bin/sh -c exec_entry p1_entry | exec_entry p1_entry |
CMD [“exec_cmd”, “p1_cmd”] | exec_cmd p1_cmd | /bin/sh -c exec_entry p1_entry | exec_entry p1_entry exec_cmd p1_cmd |
CMD [“p1_cmd”, “p2_cmd”] | p1_cmd p2_cmd | /bin/sh -c exec_entry p1_entry | exec_entry p1_entry p1_cmd p2_cmd |
CMD exec_cmd p1_cmd | /bin/sh -c exec_cmd p1_cmd | /bin/sh -c exec_entry p1_entry | exec_entry p1_entry /bin/sh -c exec_cmd p1_cmd |
k8s
中可以通过资源清单的command
、args
也可以为Pod
指定一些运行参数,四者组合出现时的最终结果如下
https://kubernetes.io/zh/docs/tasks/inject-data-application/define-command-argument-container/#notes
如果要覆盖默认的Entrypoint
与Cmd
,需要遵循如下规则:
command
或者 args
,那么将使用Docker
镜像自带的命令及其参数command
但是没有设置 args
,那么容器启动时只会执行该命令, Docker
镜像中自带的命令及其参数会被忽略args
,那么Docker
镜像中自带的命令会使用该新参数作为其执行时的参数command
与 args
,那么Docker
镜像中自带的命令及其参数会被忽略。 容器启动时只会执行配置中设置的命令,并使用配置中设置的参数作为命令的参数镜像 Entrypoint | 镜像 Cmd | 容器 command | 容器 args | 命令执行 |
---|---|---|---|---|
[/ep-1] |
[foo bar] |
[ep-1 foo bar] |
||
[/ep-1] |
[foo bar] |
[/ep-2] |
[ep-2] |
|
[/ep-1] |
[foo bar] |
[zoo boo] |
[ep-1 zoo boo] |
|
[/ep-1] |
[foo bar] |
[/ep-2] |
[zoo boo] |
[ep-2 zoo boo] |
HEALTHCHECK
用于检查容器的健康状态,Docker
可通过健康状态来决定是否对容器进行重新调度
语法为
HEALTHCHECK [选项] CMD <命令>
可选项为
–interval=<间隔> :两次健康检查的间隔,默认为30秒
–timeout=<时长> :执行健康检查命令的超时时间,如果超时,则本次健康检查就被视为失败,默认30秒
–retries=<次数> :当连续失败指定的次数后,将容器状态置为unhealthy ,默认3次
对一些可能造成假死情况的服务建议提供健康检查,以便及时重新调度恢复服务
如果基础镜像有健康检查指令,想要屏蔽掉其健康检查,可以使用HEALTHCHECK NONE
实际上有了k8s
更为丰富的健康检查探针之后,Docker
自带的健康检查就不用了
当我们在一个Dockerfile
文件中加上ONBUILD
指令,该指令对利用该Dockerfile
构建的镜像不会产生实质性影响
但是当我们编写一个新的Dockerfile
文件来基于上面通过包含ONBUILD
构建的基础镜像构建一个新镜像时,这时构造基础镜像的Dockerfile
文件中的ONBUILD
指令就生效了,在构建新镜像的过程中,首先会执行ONBUILD
指令指定的指令,然后才会执行其它指令
要注意的是ONBUILD
仅仅能 ‘子代遗传’ ,并不能 ‘隔代遗传’ ,即传递到 ‘孙子镜像’
构建上下文build context
,“上下文” 意为和现在这个工作相关的周围环境。在docker
镜像的构建过程中有构建上下文build context
这一概念,通俗的来说就是指执行docker build
时当前的工作目录,不管构建时有没有用到当前目录下的某些文件及目录,默认情况下这个上下文中的文件及目录都会作为构建上下文内容发送给Docker Daemon
当docker build
开始执行时,控制台会输出Sending build context to Docker daemon xxxMB
,这就表示将当前工作目录下的文件及目录都作为了构建上下文
前面提到可以在RUN
指令中添加--no-cache
不使用缓存,同样也可以在执行docker build
命令时添加该指令以在镜像构建时不使用缓存
和git
忽略文件.gitignore
一样的道理,在docker
构建镜像时也有.dockerignore
,可以用来排除当前工作目录下不需要加入到构建上下文build context
中的文件
例如,在构建npm
前端的镜像时项目时,在 Dockerfile
的同一个文件夹中创建一个 .dockerignore
文件,带有以下内容,这样在构建时就可以避免将本地模块以及调试日志被拷贝进入到Docker
镜像中
node_modules
npm-debug.log
多阶段构建的应用场景及优势就是为了降低复杂性并减少依赖,避免镜像包含不必要的软件包
例如,应用程序的镜像中一般不需要安装开发调试软件包。如果需要从源码编译构建应用,最好的方式就是使用多阶段构建
简单来说,多阶段构建就是允许一个Dockerfile
中出现多条FROM
指令,只有最后一条FROM
指令中指定的基础镜像作为本次构建镜像的基础镜像,其它的阶段都可以认为是只为中间步骤
每一条FROM
指令都表示着多阶段构建过程中的一个构建阶段,后面的构建阶段可以拷贝利用前面构建阶段的产物
这里我列举一个编译构建npm
项目,利用多阶段构建最终把静态资源制作成nginx
镜像的Dockerfile
#### Stage 1: npm build
FROM node:12.4.0-alpine as build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
# Copy the main application
COPY . ./
# Arguments
# Build the application
RUN npm run build
#### Stage 2: Serve the application from Nginx
FROM nginx:latest
COPY --from=build /app/build /var/www
# Copy our custom nginx config
COPY nginx.conf /etc/nginx/nginx.conf
# Expose port 3000 to the Docker host, so we can access it
# from the outside.
EXPOSE 80
ENTRYPOINT ["nginx","-g","daemon off;"]
本文总结以及整理了常用的Dockerfile
指令以及在构建镜像时的一些经验,当然也还有一些指令没有提及,更多内容可以参考官方文档
See you ~
原文:https://www.cnblogs.com/ssgeek/p/14683562.html