Docker入门基础和镜像构建

  1. 1. Docker架构
    1. 1.1. Docker daemon
    2. 1.2. Docker client
    3. 1.3. Docker Registries
    4. 1.4. Docker 对象
  2. 2. Docker基础之Image构建
    1. 2.1. 基于Dockerfile的镜像构建
    2. 2.2. Dockerfile语法
      1. 2.2.1. 注释
      2. 2.2.2. 解析器指令
      3. 2.2.3. 构建指令
        1. A. FROM指令
        2. B. RUN指令
        3. C. CMD指令
        4. D. LABEL指令
        5. E. MAINTAINER指令
        6. F. EXPOSE指令
        7. G. ENV指令
        8. H. ADD指令
        9. I. COPY指令
        10. J. ENTRYPOINT指令
        11. K. VOLUME指令
        12. L. USER指令
        13. M. WORKDIR指令
        14. N. ARG指令
    3. 2.3 Image构建之最佳实践

Docker是什么?Docker有什么用?

按照Docker官方的如下简介

Docker是一个能够提供开发,交付,运行程序的开放平台,Docker可以将应用程序和底层基础设施进行隔离,这样可以快速的进行软件交付;通过Docker,可以像管理应用程序一样管理基础设施,通过利用Docker用于快速交付、测试和部署代码的方法,我们可以显著的减少Coding到上线运行之间的延迟;

如果刚接触Docker概念的人,看了上面的简介可能还不知道它是用来干嘛的,其实简单的说,Docker提供了两个基本能力:

  • 打包应用程序:
  • 提供一个松散隔离的环境称之为Container,用来运行程序;

Docker提供的隔离和安全性允许同时很多个Container同时运行在宿主机上(物理机或者VM),Container是一个很轻量的包含程序运行所需的集合;

1. Docker架构

Docker的架构是一个经典的C/S模型,Docker client直接和Docker daemon通过REST API通信,底层根据是否是本机会有UNIX sockets或者网络的选择。Docker的架构图如下:主要有一下几部分组成:

1.1. Docker daemon

Docker daemon(dockerd)负责镜像的构建,运行和分发;dockerd监听client的API请求,然后对Docker对象进行管理,这些对象包括:images,containers,networks和volumes;

1.2. Docker client

Docker的client(docker)是用户和Docker daemon交互的主要途径;当执行命令docker run时,client会通过REST API接口将此命令发送给dockerd;Docker client可以和多个Docker daemon交互;

Docker的client和daemon可以本机部署,也可以分离部署;

1.3. Docker Registries

Docker的注册中心用来存储Docker的镜像,Docker Hub 是一个公共的注册中心,任何人都可以使用,默认配置下,Docker将会在这里寻找镜像;你也可以搭建自己私有的注册中心;

当执行docker pull或者docker run命令时,会从配置的注册中心拉取镜像;当执行docker push命令,会将Docker Host构建的本地镜像推送到配置的远端注册中心;

默认的Docker Registry是docker.io,且是不可修改的,原因可参考Docker的一个issue,大意为:不支持通过config的方式修改默认注册中心,是因为私有注册中心无法全部覆盖Docker安装过程中所需要的所有镜像;我们可以在需要从私有注册中心pull或者push的操作中带上registry的地址,例如:

1
2
$ docker login https://<YOUR-DOMAIN>:8080
$ docker pull <YOUR-DOMAIN>:8080/test-image

1.4. Docker 对象

前面提到过,Docker将内部的资源定义成了不同的对象,包括:images,containers,networks,volumes,plugins等;

  • Images

image即镜像,是一个只读模板,内容包含用于创建Docker container的指令;通常情况下,会基于其他镜像进行额外的定制化来构建我们自己的镜像;例如基于ubuntu镜像来构建自己的镜像,此镜像额外的安装apache web server和我们的app,以及相关配置,然后对此镜像进行部署运行;

  • Containers

container即容器,是一个镜像的运行实例;可以通过docker API或者docker client对一个容器进行create,start,stop,move,delete等操作;可以为container连接多个网络,挂载存储设备,也可以根据container当前运行状态创建一个新的image

宿主机上的多个containers是相对隔离的;这个隔离性是我们可以控制的,例如网络,存储,其他子系统;对于运行的container,当进行remove后,所有没有进行持久化的修改状态都会在remove后直接丢弃;

2. Docker基础之Image构建

Docker提供了两种方式来构建镜像:

  • 通过 Dockerfile 自动构建镜像;
  • 通过容器操作,并执行 Commit 打包生成镜像;

2.1. 基于Dockerfile的镜像构建

Docker可以通过解析Dockerfile指令来进行Image的构建。Dockerfile是一个文本文件,包含了用于构建镜像和运行所需要的全部指令;Docker通过docker build命令来读取Dockerfile进行Image的构建;

镜像构建是在Docker daemon上进行的,docker build构建命令需要获得Dockerfile文件和相关上下文;然后将获取到的全部上下文发送给Docker daemon,由daemon负责镜像构建;

构建的上下文是由PATHURL指定位置的一个文件列表集合;PATH变量是docker client本地的文件系统,URL是一个Git的仓库地址;构建上下文是递归处理的,所以PATH会包含其所有的子目录,URL包含会repo和submodules;所以一般都是将Dockerfile放入一个空目录中,只添加构建所有需要的文件

不要将根目录/作为PATH来进行构建,这将会把文件系统所有文件全部作为构建上下文传递给Docker daemon进行构建

下面看一下如何构建一个看可以运行的镜像,创建一个helloworld目录,目录内创建如下两个文件:

1
2
3
helloworld/
├── Dockerfile
└── helloworld

helloworld/helloworld为Go编译生成的可执行文件,运行时每秒往本地的helloworld.txt写入当前的时间戳。

Dockerfile内容如下:

1
2
3
FROM ubuntu
COPY helloworld .
CMD ./helloworld

基于ubuntu镜像进行新的镜像打包,这里只是简单的把本地的可执行文件helloworld打入镜像,并在启动镜像时执行该文件;在开始构建helloworld镜像前,先看一下docker build 的语法,如下:

1
2
3
4
docker build [OPTIONS] PATH | URL 
常用的选项有两个:
-f:指定Dockerfile的路径,默认为PATH/Dockerfile;
-t: 为镜像指定仓库名和tag,格式为name:tag, tag不写默认是latest;

下面开始构建镜像,在helloworld目录下,执行如下docker build进行镜像构建:

1
2
3
4
5
6
7
# PATH为当前目录,默认使用当前目录下的Dockerfile
# 指定构建的镜像的仓库名为dockertest/helloworld,tag由于没有指定,默认是latest
$docker build -t dockertest/helloworld .

Sending build context to Docker daemon 2.044MB
Step 1/3 : FROM ubuntu
toomanyrequests: You have reached your pull rate limit. You may increase the limit by authenticating and upgrading: https://www.docker.com/increase-rate-limit

由于使用的是公司网络,所以触发了docker hub对于匿名用户的pull限流,具体可参考官方限流说明常见的解决方案;这里使用Amazon的公共注册中心,将Dockerfile 进行如下修改:

1
2
#FROM ubuntu
FROM public.ecr.aws/lts/ubuntu

继续进行构建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ docker build -t dockertest/helloworld .
Sending build context to Docker daemon 2.044MB # 将构建上下文传送给Docker daemon,由Docker daemon负责构建
Step 1/3 : FROM public.ecr.aws/lts/ubuntu
latest: Pulling from lts/ubuntu
aeb3f02e9374: Pull complete
db978cae6a05: Pull complete
c20d459170d8: Pull complete
Digest: sha256:bc3229b3f0aa1db47706ee4769d4f26f6c004c2bdd6eda23fb7ca12fdc01ee0d
Status: Downloaded newer image for public.ecr.aws/lts/ubuntu:latest
---> 49bb8dc881b2
Step 2/3 : COPY helloworld .
---> 2d813f12a359
Step 3/3 : CMD ./helloworld
---> Running in af99c06ffb55
Removing intermediate container af99c06ffb55
---> 7f205e9f45f2
Successfully built 7f205e9f45f2
Successfully tagged dockertest/helloworld:latest # 生成镜像仓库为dockertest/helloworld, tag为latest

可以通过docker images查看本地构建的镜像:

1
2
3
4
$docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
dockertest/helloworld latest e20012cb18bc About a minute ago 74.9MB
public.ecr.aws/lts/ubuntu latest 49bb8dc881b2 5 months ago 72.9MB

可以通过docker run启动容器运行对应的镜像,

1
2
3
4
$docker run -d dockertest/helloworld:v1
$docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
676c35929eed dockertest/helloworld:v1 "/bin/sh -c ./hellow…" 6 seconds ago Up 5 seconds

然后通过docker exec登录容器查看对应程序的运行情况,

1
$docker exec -it 676c35929eed bash

2.2. Dockerfile语法

本节的Dockerfile语法主要是参考官方手册,在其基础上进行了一些总结和注意事项,下面进入整体:

Dockerfile指令文本的语法格式如下:

1
2
# Comment
INSTRUCTION arguments

Dockerfile的每一条命令由:指令参数两部分组成;指令不区分大小写,但是习惯上我们会用大写的指令,以和参数部分进行区分;Docker按照Dockerfile的指令书写顺序依次进行解析和执行;

Dockerfile的第一条构建指令必须是FROM指令(除了以下三种特例);FROM指令用来指定构建本Image所需要依赖的父Image;FROM指令的前面可以存在三类语句:

  • 解析器指令(parser directives);
  • 注释;
  • 全局参数ARG指令;

2.2.1. 注释

Docker中除了解析器指令外,所有以#开头的行都被认为是注释行;但其他位置的#会被认为是指令参数的一部分,不会被认为是注视;如下示例,RUN指令会完整的输出后面所有的字符串;

1
2
# this is a Comment
RUN echo 'we are running some # of cool things'

执行结果为:

1
2
3
Step 2/4 : RUN echo 'we are running some # of cool things'
---> Running in b8db21a2be48
we are running some # of cool things

每条Dockerfile指令执行的时候都会先移除含有的注释,如下两条语句是等价的:

1
2
3
RUN echo hello \
# comment
world
1
2
RUN echo hello \
world

对于注释行需要注意的是,任何前导空白符都会被忽略,关于前导空白符的说明如下:

任何注释或者指令行前面的空白符都会被忽略,如下:

1
2
3
       # this is a comment-line
RUN echo hello
>RUN echo world

等价于

1
2
3
># this is a comment-line
>RUN echo hello
>RUN echo world

但是需要注意的是,任何在指令参数中的空白符是会保留的,如下:

1
2
3
>RUN echo "\
hello\
world"

执行结果如下:

1
2
3
>Step 2/4 : RUN echo "     hello     world"
---> Running in c672a42f820e
hello world

对于空白符,这点和注释的解析是有区别的,前面说了对于任何以#开头(除了解析器指令)都会被认为是注释。

2.2.2. 解析器指令

前面已经提到了以#开头的行除了是注释以外,就是解析器指令了,那什么是解析器指令呢?

解析器指令是可选的,会影响Dockerfile随后指令行的执行;解析器指令不会增加Image构建的layers;并且不会作为构建step显示;解析器指令的写法上一种特殊的注释,如下:

1
# directive=value

解析器指令一定要放在Dockerfile最开始,一旦Docker处理到任何注释,空白行,构建指令之后,Docker后面的指令处理不会再处理任何看上去是解析器指令的行,而是把他们当成注释;解析器指令同样大小写不敏感,但是习惯上用小写,且解析器指令后面留一行空白行;对于解析器指令不支持换行转义符;

如下由于不支持换行转义符,下面的语句会被当做是注释;

1
2
# direc \
tive=value

同样的解析器指令会被认为无效:

1
2
3
4
# directive=value1
# directive=value2

FROM ImageName

非放在最开始的解析器指令都会被认为是注释,如下,全部都是注释:

1
2
3
4
5
# About my dockerfile
# directive=value

FROM ImageName
# directive=value

目前Docker只支持两种解析器指令:syntaxescape指令;其他不支持的指令都会被认为是注释;

  • syntax指令

syntax的格式如下:

1
# syntax=[remote image reference]

例如:

1
2
3
# syntax=docker/dockerfile:1
# syntax=docker.io/docker/dockerfile:1
# syntax=example.com/user/repo:tag@sha256:abcdef...

2.2.3. 构建指令

A. FROM指令

前面说了,Dockerfile的第一条构建指令必须是FROM指令(除了上面提到的三种特例);FROM指令用来指定构建本Image所需要依赖的父Image;FROM构建指令的格式如下:

1
2
3
FROM [--platform=<platform>] <image> [AS <name>]
FROM [--platform=<platform>] <image>[:<tag>] [AS <name>]
FROM [--platform=<platform>] <image>[@<digest>] [AS <name>]
  • --platfrom参数:用于指定依赖的特定平台的父镜像,因为有些镜像可能存在多个平台的版本;例如:linux/amd64linux/arm64windows/amd64;默认情况下,使用Docker daemon所使用的的平台来进行构建;
  • image参数:就是所依赖的父镜像名字;对于只image是镜像名字的,回去Docker Hub中查找和拉取镜像,如果image是镜像的URL,那么会去该Registry搜索和拉取镜像;
  • AS name参数:本构建的别名,可以在后面的构建指令中进行引用,例如COPY --from=<name>
  • tagdigest参数:用来指定所依赖的特定版本的父镜像,不填,默认使用名为latesttag的镜像版本;

在同一个Dockerfile构建文件中,可以出现多条FROM指令;每一条FROM指令都会清除之前所有指令设置的状态,开始一个新的stage。你可以选择将前面构建的产物拷贝到新的stage中,之前的构建stage全部丢弃,如下官方示例

1
2
3
4
5
6
7
8
9
10
11
12
13
# syntax=docker/dockerfile:1
FROM golang:1.16
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html
COPY app.go ./
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
# 从前面构建的镜像中拷贝产物app到新的镜像中
COPY --from=0 /go/src/github.com/alexellis/href-counter/app ./
CMD ["./app"]

B. RUN指令

RUN构建指令会在新的layer中执行指定的命令,并将执行结果进行提交,用于后面step的构建;RUN指令有如下两种格式:

1
2
3
4
# shell模式:执行shell命令, 默认 /bin/sh -c on Linux or cmd /S /C on Windows
RUN <command>
# exec模式:执行可执行文件,即exec调用
RUN ["executable", "param1", "param2"]

相对于shell模式,exec模式可以避免shell语法中字符串重整的问题;shell模式默认的shell解析器可以通过SHELL指令进行修改;

shell模式下的命令语法就是正常的shell语法,docker会拉起shell解析器解析后面的shell语句,例如\反斜线是转义符号,如下:

1
2
RUN /bin/bash -c 'source $HOME/.bashrc; \
echo $HOME'

等价于

1
RUN /bin/bash -c 'source $HOME/.bashrc; echo $HOME'

同样可以通过exec模式来指定shell进行命令执行,如下:

1
RUN ["/bin/bash", "-c", "echo hello"]

需要注意的是:exec模式的后面的参数是一个JSON数组,所以里面的参数都必须用双引号包裹,且需要符合JSON字符串的语法,对于特殊字符需要进行转义;例如:

1
2
3
RUN ["c:\windows\system32\tasklist.exe"]
# 必须写成如下格式
RUN ["c:\\windows\\system32\\tasklist.exe"]

RUN指令执行的cache在一下次docker build仍然是可见的;这样在一下次构建的时候,可以快速使用cache,而不用重新进行耗时的构建,如下,每个step都是使用上一次构建的cache:

1
2
3
4
5
6
7
8
9
Step 2/10 : MAINTAINER walkerdu
---> Using cache
---> 1dc72169f3dc
Step 3/10 : RUN yum install cmake3 curl-devel hiredis readline-devel subversion hiredis-devel python-devel -y
---> Using cache
---> fdbd2a9d038a
Step 4/10 : RUN yum install git-lfs -y
---> Using cache
---> d79dbbd081bb

RUN指令的cache在以下情况下会失效:

  • ADDCOPY指令后面的RUN构建指令的cache可能(判断文件发生变化)会失效;
  • 如果想强制进行重新构建,需要显示的在构建时带上no-cache参数,如:docker build --no-cache

当某一步的cache被判断失效后,后面所有的step都会重新进行构建,所以设计Docker的时候,应该尽量:不常变动的,放在前面构建,常变动的,放在后面构建

分层的RUN指令和生成的提交符合Docker的核心设计理念:提交成本很低,且可以从Image历史的任何位置进行容器的创建;这个代码的版本控制很相似;

C. CMD指令

CMD指令是用来指定容器运行时默认要执行的命令CMD指令也可以只指定特定的参数,由ENTRYPOINT指令指定可执行文件;CMD指令的格式如下:

1
2
3
4
5
6
# exec模式:建议使用的格式
CMD ["executable","param1","param2"]
# ENTRYPOINT模式:作为ENTRYPOINT指令在exec模式下提供的参数
CMD ["param1","param2"]
# shell模式:默认和RUN指令一样,使用/bin/sh进行解析
CMD command param1 param2

CMD指令在整个Dockerfile中只能存在一条有效的,如果有多条CMD指令,只有最后一条有效果;对于exec模式和shell模式,CMD指令都是来设置Image运行时要执行的命令;使用参数模式:CMD指令为ENTRYPOINT指令提供默认参数,CMD指令和ENTRYPOINT指令的参数都必须要是JSON数组格式;

对于exec模式和shell模式,的相关用法和RUN指令是一样的;需要注意的是:不要把RUN指令和CMD指令搞混了RUN指令只是在构建Image的时候执行特定的命令,然后提交结果;而CMD指令不会在构建的时候执行,而是设置Image运行时期望的执行的指令;

如果执行docker run启动container的时候指定了特定的命令,那么会忽略CMD指令的设置;

D. LABEL指令

LABEL指令目的是为了给Image增加一些元数据。一个LABEL由一个键值对组成,格式如下:

1
LABEL <key>=<value> <key>=<value> <key>=<value> ...

对于含有空格的value,需要通过双引号或者反斜线进行转义;多标签可以放在同一行指令中;在Docker 1.10之前,多标签放在同一行可以减少Image的大小,不过现在不存在这个问题了;如下是简单的额示例:

1
2
3
4
LABEL "com.example.vendor"="ACME Incorporated"
LABEL com.example.label-with-value="foo"
LABEL version="1.0" description="This text illustrates \
that label-values can span multiple lines."

标签会从父镜像继承(FROM 指令)其设置,如果有标签发生冲突,那么最新的设置会覆盖之前的设置;可以通过docker image inspect来查看镜像的标签;

1
docker image inspect --format='' imageid

E. MAINTAINER指令

MAINTAINER指令用来设置镜像构建的作者,已经不建议使用LABEL指令可以更灵活的做这个事情,他可以添加任何你需要的元数据;如下:

1
LABEL org.opencontainers.image.authors="SvenDowideit@home.org.au"

F. EXPOSE指令

EXPOSE指令用于告知Docker,Container在运行时监听指定的端口;可以指定监听端口在TCP或UDP上,默认不指定为监听TCP端口;EXPOSE指令的格式如下:

1
EXPOSE <port> [<port>/<protocol>...]

注意:EXPOSE指令并不实际让Container暴露对应的端口,它的作用只是一个Image构建者给一个Image使用者的文档说明;旨在说明Image需要暴露的端口,而实际Container运行时实际的端口暴露,需要Container运行时通过指定的配置来使其对应的端口暴露;

1
2
3
EXPOSE 80
EXPOSE 81/udp
EXPOSE 82/tcp 83/udp

image inspect的结果如下:

1
2
3
4
5
6
"ExposedPorts": {
"80/tcp": {},
"81/udp": {},
"82/tcp": {},
"83/udp": {}
},

docker run -p/-P客户端命令可以使容器在运行的时候绑定指定的container端口到host的端口上,以保证服务对外提供访问能力;

G. ENV指令

ENV指令用于设置环境变量,环境变量的值设置后在随后的构建step中都是可见的,可以将环境变量用在后面的构建命令参数中,作为变量值来进行后面构建命令的构建使用;ENV指令格式如下,多个键值对可以在同一行;

1
ENV <key>=<value> ...

ENV指令还有一种不建议使用的语法,只是为了向后兼容,未来可能被移除,如下

1
ENV key value

这种语法省略了=,不允许在同一行定了多个环境变量

如下示例,和命令解释类似,引号和转义符号可以用在包含空格的值中;

1
2
3
4
5
6
ENV MY_NAME="John Doe"
ENV MY_DOG=Rex\ The\ Dog
ENV MY_CAT=fluffy

ENV MY_NAME="John Doe" MY_DOG=Rex\ The\ Dog \
MY_CAT=fluffy

构建指令中对于环境变量的引用格式为:$variable_name或者${variable_name},熟悉shell语法的人会发现这个和shell语法是一致的;${variable_name}语法可以支持一些标准的bash修饰符,如下:

  • ${variable:-word}:如果变量variable已经存在,那么返回其值,否则返回word(word可以是任何字符串,包括其他环境变量);
  • ${variable:+word}:如果变量variable已经存在,那么返回word字符串;否则返回空串;

如下是环境变量在后续构建指令中引用的示例:

1
2
3
4
5
FROM busybox
ENV FOO=/bar
WORKDIR ${FOO} #等价于 WORKDIR /bar
ADD . $FOO #等价于 ADD . /bar
COPY \$FOO /quux #等价于 COPY $FOO /quux

环境变量的引用范围不能在同一条构建指令中,如下:

1
2
3
ENV abc=hello
ENV abc=bye def=$abc # def的值为hello,而不是bye
ENV ghi=$abc # ghi的值为bye

环境变量的支持在以下构建命令中进行引用:ADDCOPYENVEXPOSEFROMLABELSTOPSIGNALUSERVOLUMEWORKDIRONBUILD测试发现RUN指令里面也可以进行环境变量的操作;详细参考environment replcaement手册

通过ENV设置的环境变量会在Container运行的时候一直存在,而ARG变量在image构建结束后就失效了;可以通过docker inspect进行查看,也可以在Container运行的时候进行重新设定,如:docker run --env <key>=<value>

H. ADD指令

ADD指令用于将本地的文件,目录或者是远端的URLs拷贝到构建的Image中的文件系统中;指令的格式如下:

1
2
ADD [--chown=<user>:<group>] <src>... <dest>
ADD [--chown=<user>:<group>] ["<src>",... "<dest>"]

src...dest:拷贝源文件和目标目录;可以指定多个src资源,如果他们是本地的文件或目录,那么他们的相对路径是构建上下文的PATHsrc资源可以包含通配符用来匹配多个资源,通配符匹配规则采用Go的filepath.Matchdest目标可以是一个绝对路径,或者是一个和WORKDIR指令关联的相对路径;如下是一些简单的使用示例:

1
2
3
4
5
6
7
8
# 拷贝以hom开头的源文件
ADD hom* /mydir/
# 拷贝hom后面有一个字符的源文件,例如:home.txt
ADD hom?.txt /mydir/
# 拷贝源文件PATH/test.txt到目标WORKDIR/relativeDir/
ADD test.txt relativeDir/
# 拷贝源文件PATH/test.txt到目标/absoluteDir/
ADD test.txt /absoluteDir/

--chown用来在拷贝文件到Image的文件系统中时设置对应文件新的用户名和用户组;默认不设置时,拷贝文件的UID和GID都是0,即root超级用户权限;该选项只支持构建Linux的Image

如果拷贝的是本地文件或目录,拷贝时,并不会修改文件和目录的访问权限,如果拷贝的src是URL的话,目标文件的访问权限是600;

--chown选项支持设置用户名,用户组或者直接是UID,GID的组合的方式来设置文件权限;如果设置了用户名,但是没有设置用户组,或者设置了UID,但没有设置GID,此时用户组或者GID都会自动根据用户名/UID,设置为相同的值;这个设置过程Docker会通过Image的文件系统的/etc/passwd/etc/group两个文件进行相关的翻译和转换;所以要设置的权限必须是Image已经存在的,否则会构建出错;如下相关示例:

1
2
3
4
ADD --chown=55:mygroup files* /somedir/
ADD --chown=bin files* /somedir/
ADD --chown=1 files* /somedir/
ADD --chown=10:11 files* /somedir/

ADD指令在拷贝的时候遵循以下原则:

  • src路径必须是构建上下文中的,不能有类似ADD ../something /dest的构建,因为docker build会将构建上下文全部发送给Docker daemon进行构建,所有PATH之外的文件时找不到的,无法执行构建;
  • 如果src是URL且dest没有以斜线结尾,那么一个文件会从URL进行下载并拷贝为dest
  • 如果src是URL且dest以斜线结尾,那么一个文件会被拷贝到dest目录下,例如:ADD http://example.com/foobar /会创建dest/filename
  • 如果src是一个目录,则所有该目录下的文件都会进行拷贝,包括元数据;注意:只拷贝目录下的所有文件,不会拷贝目录本身
  • 如果src是一个可被识别的压缩文件,那么将会被解压作为一个目录处理(URL指向的压缩包不会执行此操作);类似执行tar -x的结果;
  • 如果有多个src被指定,无论是直接指定多个,还是通过通配符匹配的结果,dest都必须是一个目录且以正斜线结尾;
  • 如果dest没有以斜线结尾,那么其会被认为是一个文件,src会直接写入dest文件中;
  • 如果dest不存在,构建时会自动创建所有不存在的目录;

I. COPY指令

如果你看官方手册,会发现其内容基本和ADD指令基本一样,ADD指令相对于COPY指令多了两个功能,其他完全一样:

  • ADD指令允许src是URL;
  • ADD指令可以识别src是压缩文件;

官方最佳实践建议首选采用COPY指令,因为COPY指令相对于ADD指令更加简单,仅支持文件的基本拷贝;如果需要上门两个特性可以使用ADD指令来提供高级功能;

J. ENTRYPOINT指令

ENTRYPOINT指令也是用于配置container运行时要执行的命令,ENTRYPOINT指令的有如下两种格式:

1
2
3
4
# exec模式
ENTRYPOINT ["executable", "param1", "param2"]
# shell模式
ENTRYPOINT command param1 param2

docker run <image>启动时传入的命令行参数都会被添加到ENTRYPOINT的exec模式的参数中,且CMD指令参数模式的所有参数会被全部覆盖;可以通过docker run --enterypoint来覆盖ENTRYPOINT指令;

如下示例,将docker run <image>输入的参数在Container中输出到文件中:

1
2
3
FROM public.ecr.aws/lts/ubuntu
COPY helloworld .
ENTRYPOINT ["/helloworld"]

以如下方式运行Image:

1
2
$docker build -t dockertest/test .
$docker run -d dockertest/test walker -a -b -c -d du

可以看到输出的参数如下:

1
2
3
$docker exec -it 2fde1891bb9b cat /helloworld.txt

2021-08-17 09:30:41Params: [walker -a -b -c -d du]

ENTRYPOINT的shell模式下,CMD指令的参数和docker run命令的参数都会被忽略的;使用ENTRYPOINT的shell模式有一个很大的缺点(CMD的Shell模式一样):作为/bin/sh -c的shell命令,不会传递信号给子命令;执行的程序不会以PID=1运行,且无法接收到Unix信号;例如:docker stop <container>无法将SIGTERM信号传递给运行的进程;

CMD指令一样,对于多条ENTRYPOINT指令,只有最后一条有效;

对于CMD指令和ENTRYPOINT指令的功能有很多相似之处,他们都是用来定义Container运行时要执行的命令;对于它们两个的使用方式和协作建议如下:

  1. Dockerfile必须含有至少一条CMD指令或者ENTRYPOINT指令;
  2. ENTRYPOINT指令应该作为可执行Container的指令;
  3. CMD指令应该作为提供默认参数的方式为ENTRYPOINT指令运行程序
  4. CMD指令提供的参数可以在Container运行的时候被覆盖;

下表说明了CMD指令和ENTRYPOINT指令不同组合的使用情况:

No ENTRYPOINT ENTRYPOINT exec_entry p1_entry(shell模式) **ENTRYPOINT [“exec_entry”, “p1_entry”]**(exec模式)
No CMD 构建错误 /bin/sh -c exec_entry p1_entry exec_entry p1_entry
**CMD [“exec_cmd”, “p1_cmd”]**(exec模式) exec_cmd p1_cmd /bin/sh -c exec_entry p1_entry exec_entry p1_entry exec_cmd p1_cmd
**CMD [“p1_cmd”, “p2_cmd”]**(ENTRYPOINT参数模式) p1_cmd p2_cmd /bin/sh -c exec_entry p1_entry exec_entry p1_entry p1_cmd p2_cmd
CMD exec_cmd p1_cmd(shell模式) /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

K. VOLUME指令

VOLUME指令用于在Container运行时在宿主机上持久化一些数据,当然也可以和其他Container共享数据;当含有VOLUME指令的Image运行时,Docker会在本地磁盘上创建一个匿名的volume,并将此volume挂载到Container中VOLUME指令声明的挂载点上;如果 Container中的挂载点上有数据,则会拷贝到本地匿名的volume中;

VOLUME指令的格式如下:

1
2
3
4
# json格式
VOLUME ["/dir1", "/dir2"]
# 纯文本格式
VOLUME /dir1 /dir2

如下示例:

1
2
3
4
FROM ubuntu
RUN mkdir /myvol
RUN echo "hello world" > /myvol/greeting
VOLUME /myvol

在容器运行后,Docker deamon会在本地磁盘上创建一个匿名的volume,并挂载到Container的/myvol目录,同时把/myvol内的文件拷贝到volume中;我们可以看到在 /var/lib/docker/volumes 目录下创建了一个匿名的volume,里面含有Container创建的greeting文件;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ docker volume ls
DRIVER VOLUME NAME
local 7b0bb98617cde21c044a91b31794a62417f1dc3779c726ba1901313b41ecf654

$ docker volume inspect 7b0bb98617cde21c044a91b31794a62417f1dc3779c726ba1901313b41ecf654
[
{
"CreatedAt": "2021-08-18T15:47:48+08:00",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/7b0bb98617cde21c044a91b31794a62417f1dc3779c726ba1901313b41ecf654/_data",
"Name": "7b0bb98617cde21c044a91b31794a62417f1dc3779c726ba1901313b41ecf654",
"Options": null,
"Scope": "local"
}
]

$ sudo ls /var/lib/docker/volumes/7b0bb98617cde21c044a91b31794a62417f1dc3779c726ba1901313b41ecf654/_data
greeting

这里需要注意的是:VOLUME指令默认在Container运行的时候创建的匿名Volume,虽然是持久化的,但是无法与其他Container共享;可以通过docker run -v volume_name:/dir来为VOLUME指令创建有名的volume,然后挂载到VOLUME指令设置的挂载点;如下:

1
2
3
4
5
6
7
8
$ docker run -it -v test_volume:/myvol dockertest/test /bin/bash
$ docker volume ls
DRIVER VOLUME NAME
local 7b0bb98617cde21c044a91b31794a62417f1dc3779c726ba1901313b41ecf654
local test_volume

$sudo ls /var/lib/docker/volumes/test_volume/_data
greeting

L. USER指令

USER指令用于指定用户名和用户组来运行后面的RUNCMDENTRYPOINT构建指令;当然这里的用户名和用户组必须是已经存在;

如果用户名没有所属的主用户组,那么会以root用户组的身份执行命令

如下示例:

1
2
3
4
5
FROM ubuntu
RUN useradd walker
USER walker
RUN mkdir /tmp/myvol
RUN echo "hello world" > /tmp/myvol/greeting

M. WORKDIR指令

WORKDIR指令用于设置后面构建指令操作的工作目录,如果目录不存在,会自动创建;工作目录的设置对于RUNCMDENTRYPOINTCOPYADD构建指令生效;指令格式如下:

1
2
WORKDIR /path/to/workdir
WORKDIR relativeDir

WORKDIR指令可以设置多次,如果参数是一个相对路径,那么其相对的路径是相对于最近一条WORKDIR指令所设置的路径,如下:

1
2
3
4
5
6
7
FROM ubuntu
RUN pwd
RUN mkdir -p /tmp/a
WORKDIR /tmp
RUN pwd
WORKDIR a
RUN pwd

执行结果如下,中间省略了很多输出;

1
2
3
4
5
6
7
8
9
Step 2/7 : RUN pwd
/
Step 3/7 : RUN mkdir -p /tmp/a
Step 4/7 : WORKDIR /tmp
Step 5/7 : RUN pwd
/tmp
Step 6/7 : WORKDIR a
Step 7/7 : RUN pwd
/tmp/a

默认工作目录是根目录;如果不指定,默认会继承FROM xxx基础镜像设置的WORKDIR,所以为了避免不可控的工作目录,显示设置WORKDIR是一个好的习惯;

N. ARG指令

ARG指令用于申明一个变量可以在后面的构建过程中使用的,通过docker build --build-arg <var>=<value>命令在Image构建过程中设置ARG参数变量的值; --build-arg传入的变量必须已经在Dockfile中通过ARG 指令定义,否则会在构建过程中触发Warning;

ARG指令格式如下:

1
ARG <name>[=<default value>]

一个Dockerfile可以包含多个ARG指令,ARG指令定义的参数名可以包含默认值,如果docker build --build-arg为设置参数的值,构建会使用ARG的默认值;如下简单的示例:

1
2
3
4
FROM ubuntu
ARG var1
ARG var2=val2
RUN echo ${var1} ${var2}

执行如下构建:

1
2
3
4
$docker build --build-arg var1=val1 -t dockertest/test  .

Step 4/4 : RUN echo ${var1} ${var2}
val1 val2

需要注意的是,不建议在构建的时候传入密钥,证书等参数信息,因为构建过程中传入的参数信息可以通过docker history查看构建过程中的所有信息;

ARG变量定义从Dockerfile中定义它的行开始生效,而不是通过在命令行或其他地方使用参数而生效;在本构建Stage结束后,ARG变量生命周期会结束;而ENV变量会在Container运行时一直生效;需要注意的是:ARG支持使用的指令和ENV一样,只能在特定的指令后面使用,具体参考environment replcaement手册ENV指令一节;

ARG指令和ENV指令都可以定义变量在RUN命令中使用,这里需要注意:ENV定义的环境变量会覆盖ARG定义的同名变量;如下示例:

1
2
3
4
FROM ubuntu
ENV var1=env
ARG var1=arg
RUN echo ${var1} #构建时输出:env

Docker提供一个预定义的ARG变量集合,可以直接使用,而不需要通过ARG指令进行定义:如下预定义变量集合:

  • HTTP_PROXY
  • http_proxy
  • HTTPS_PROXY
  • https_proxy
  • FTP_PROXY
  • ftp_proxy
  • NO_PROXY
  • no_proxy

如下构建示例:

1
2
FROM ubuntu
RUN echo ${http_proxy}

构建过程如下:

1
2
3
4
5
6
$docker build --build-arg http_proxy=a.b.c -t dockertest/test  .

...
Step 2/2 : RUN echo ${http_proxy}
---> Running in cb22c7950f34
a.b.c

2.3 Image构建之最佳实践

Docker官方给出了很多Docker在构建的时候一些比较好的Dockerfile设计规范和Image构建的最佳方案,如下几点:

关于Image构建的一些比较好的推荐方案,如下:

还有一些参考资料:

后面会逐渐展开学习和分享;