Docker 运行容器前需要本地存在对应的镜像,如果本地不存在该镜像,Docker 会从镜像仓库下载该镜像。

获取镜像

从Docker镜像仓库获取镜像的命令是docker pull,镜像也是一层层去下载的

  1. docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签]
  • Docker 镜像仓库地址:地址的格式一般是 <域名/IP>[:端口号]。默认地址是 Docker Hub(docker.io)。
  • 仓库名:如之前所说,这里的仓库名是两段式名称,即 <用户名>/<软件名>。对于 Docker Hub,如果不给出用户名,则默认为 library,也就是官方镜像。

运行镜像

以镜像为基础,启动并运行一个容器,docker run命令的具体使用后面会有。

  1. docker run -it --rm ubuntu:18.04 bash
  • -it:这是两个参数,-i表示交互式操作,-t表示终端,因为需要进入bash执行命令,所以需要交互式终端。
  • —rm:容器退出后随之将其删除。
  • ubuntu:18.04:镜像名
  • bash:启动交互式shell

进入容器后,通过exit退出。

列出镜像

使用docker image ls命令,可以列出已经下载下来的镜像,列表包含了仓库名、标签、镜像ID、创建时间以及所占用的空间。镜像ID是镜像的唯一标识,一个镜像可以对应多个标签。
镜像体积显示的是下载到本地后,镜像展开的大小,通过docker system df命令来便捷的查看镜像、容器、数据卷占用的空间。
仓库名和标签名均为的镜像,被称为虚悬镜像(dangling image),原因是新旧镜像同名,旧镜像名称被取消,从而出现均为none的镜像,可以通过docker image ls -f dangling=true专门显示这类镜像。这类镜像可以使用docker image prune命令进行删除。

中间层镜像

为了加速镜像构建、重复利用资源,Docker 会利用 中间层镜像。所以在使用一段时间后,可能会看到一些依赖的中间层镜像。默认的 docker image ls 列表中只会显示顶层镜像,如果希望显示包括中间层镜像在内的所有镜像的话,需要加 -a 参数。这些镜像没有必要动,因为在删除顶层镜像的时候,如果有依赖的,也会被一并删除。

列出部分镜像

根据仓库名列出镜像:docker image ls <仓库名>
列出特定的某个镜像:docker image ls <仓库名>:<标签>
查看mongo:3.2之后创建的镜像:docker image ls -f since=mongo:3.2,查看之前的用before

以特定形式显示

略过

删除本地镜像

可以通过镜像ID、镜像、镜像摘要来删除镜像。

  1. docker image rm 501 # 可以只用ID的前三位
  2. docker image rm centos
  3. docker image ls --digests
  4. docker image rm node@sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228

Untagged和Delted

删除镜像实际上是删除某个标签的镜像 ,因此首先需要做的是将满足要求的所有镜像标签都取消,这就是Untagged,当取消了所有标签后,就会触发删除行为,即Deleted。
还需要注意的是,如果有用这个镜像启动的容器存在(即使没有运行),那么同样不可以删除这个镜像。必须要删除容器,再删除镜像。

利用commit理解镜像构成

(本质上是用commit命令手动操作镜像来行程新的镜像,但实际环境不可以这么使用!)
暂时略过

构建镜像

在Dockerfile文件所在目录执行

  1. docker build [选项] <上下文路径/URL/->
  2. # 例
  3. $ docker build -t nginx:v3 .
  4. Sending build context to Docker daemon 2.048 kB
  5. Step 1 : FROM nginx
  6. ---> e43d811ce2f4
  7. Step 2 : RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
  8. ---> Running in 9cdc27646c7b
  9. ---> 44aa4490ce2c
  10. Removing intermediate container 9cdc27646c7b
  11. Successfully built 44aa4490ce2c

镜像构建上下文(Context)

docker build命令最后的【.】表示当前目录,这是在指定上下文路径而不是指定Dockerfile所在的路径。
Docker在运行时分为Docker引擎(也就是服务端守护进程)和客户端工具,Docker引擎提供了一组Docker Remote API,而入docker命令这样的客户端工具则是通过这组API与Docker引擎交互。
在进行镜像构建时,并非所有定制都会通过RUN指令完成,经常会需要将一些本地文件复制进镜像,其实并非在本地构建,而是在服务端,也就是Docker引擎中构建的,那么就需要让服务端获得本地文件。
这就引入了上下文的概念。当构建的时候,用户会指定构建镜像上下文的路径,docker build 命令得知这个路径后,会将路径下的所有内容打包,然后上传给Docker引擎。这样Docker引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。
以COPY命令为例,如果在Dockerfile中这么写

  1. COPY ./package.json /app/

这个命令本质上是复制上下文目录下的package.json到/app下,而上下文目录的路径是通过docker build命令最后的一段来指定的,例如【.】指代的就是当前目录。

一般来说,应该把Dockerfile置于一个空目录下,或者项目根目录下,如果该目录下没有所需文件,那么应该把所需文件复制一份过来。如果目录下有些东西确实不希望构建时传给 Docker 引擎,那么可以用 .gitignore 一样的语法写一个 .dockerignore,该文件是用于剔除不需要作为上下文传递给 Docker 引擎的。当Dockerfile置于镜像构建的上下文目录时候,也就可以使用【.】了,这是一个好习惯。

其它docker build的用法

直接用Git repo进行构建

  1. docker build -t hello-world https://github.com/docker-library/hello-world.git#master:amd64/hello-world

用给定的tar压缩包构建

  1. docker build http://server/context.tar.gz

从标准输入中读取Dockerfile进行构建

如果标准输入传入的是文本文件,则将其视为 Dockerfile,并开始构建。这种形式由于直接从标准输入中读取 Dockerfile 的内容,它没有上下文,因此不可以像其他方法那样可以将本地文件 COPY 进镜像之类的事情。

  1. docker build - < Dockerfile
  2. cat Dockerfile | docker build -

从标准输入中读取上下文压缩包进行构建

如果发现标准输入的文件格式是 gzip、bzip2 以及 xz 的话,将会使其为上下文压缩包,直接将其展开,将里面视为上下文,并开始构建。

  1. $ docker build - < context.tar.gz

Dockerfile定制镜像

镜像的定制实际上就是定制每一层所添加的配置、文件,把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像,那么之前提及的无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决。这个脚本就是 Dockerfile。
Dockerfile 是一个文本文件,其内包含了一条条的指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。

FROM 指定基础镜像

所谓定制镜像,就是以一个镜像为基础,在其上进行定制,因此FROM是必备的指令,也是Dockerfile的第一条指令。服务类镜像有nginx、redis等,运行语言的镜像有node、openjdk、python等,操作系统镜像有centos、alpine等。
除了以现有镜像为基础镜像外,Docker还存在一个特殊的镜像scratch,这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。

  1. FROM scratch

RUN 执行命令

RUN指令是用来执行命令行命令的,其格式有两种:

  • shell格式:RUN <命令>,就像直接在命令行中输入的命令一样。
  • exec 格式:RUN [“可执行文件”, “参数1”, “参数2”],这更像是函数调用中的格式。

但由于Dockerfile中,每一个指令都会建立一层,在其上执行命令,执行结束后,commit这一层的修改,构成新的镜像。因此不要写多行的RUN

  1. # 修改前
  2. FROM debian:stretch
  3. RUN apt-get update
  4. RUN apt-get install -y gcc libc6-dev make wget
  5. RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz"
  6. RUN mkdir -p /usr/src/redis
  7. RUN tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1
  8. RUN make -C /usr/src/redis
  9. RUN make -C /usr/src/redis install
  10. # 修改后
  11. FROM debian:stretch
  12. RUN set -x; buildDeps='gcc libc6-dev make wget' \
  13. && apt-get update \
  14. && apt-get install -y $buildDeps \
  15. && wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" \
  16. && mkdir -p /usr/src/redis \
  17. && tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \
  18. && make -C /usr/src/redis \
  19. && make -C /usr/src/redis install \
  20. && rm -rf /var/lib/apt/lists/* \
  21. && rm redis.tar.gz \
  22. && rm -r /usr/src/redis \
  23. && apt-get purge -y --auto-remove $buildDeps

构建镜像时,一定要确保每一层只添加真正需要添加的东西,任何无关的东西都应该清理掉。

COPY复制文件

命令格式为

  1. COPY [--chown=<user>:<group>] <源路径>... <目标路径>
  2. COPY [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]

从上下文目录中<源路径>的文件/目录复制到新的一层的镜像内的<目标路径>位置,<源路径> 可以是多个,甚至可以是通配符,其通配符规则要满足 Go 的filepath.Match规则。

<目标路径> 可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 WORKDIR 指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。

使用 COPY 指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。在使用该指令的时候还可以加上 —chown=: 选项来改变文件的所属用户及所属组。

  1. COPY --chown=55:mygroup files* /mydir/
  2. COPY --chown=bin files* /mydir/
  3. COPY --chown=1 files* /mydir/
  4. COPY --chown=10:11 files* /mydir/

如果源路径为文件夹,复制的时候不是直接复制该文件夹,而是将文件夹中的内容复制到目标路径。

CMD 容器启动命令

命令格式为

  1. # shell 格式
  2. CMD <命令>
  3. # exec 格式:
  4. CMD ["可执行文件", "参数1", "参数2"...]
  5. # 参数列表格式,在指定了 ENTRYPOINT 指令后,用 CMD 指定具体的参数。:
  6. CMD ["参数1", "参数2"...]

Docker 不是虚拟机,容器就是进程,CMD指令就是用于用于指定默认的容器主进程的启动命令的。在运行时可以指定新的命令来替代镜像设置中的这个默认命令,比如,ubuntu 镜像默认的 CMD 是 /bin/bash,如果我们直接 docker run -it ubuntu 的话,会直接进入 bash。我们也可以在运行时指定运行别的命令,如 docker run -it ubuntu cat /etc/os-release。这就是用 cat /etc/os-release 命令替换了默认的 /bin/bash 命令了,输出了系统版本信息。

在指令格式上,一般推荐使用 exec 格式,这类格式在解析时会被解析为 JSON 数组,因此一定要使用双引号 “,而不要使用单引号。如:

  1. # shell格式
  2. CMD echo $HOME
  3. # 实际执行时会被变更为
  4. CMD [ "sh", "-c", "echo $HOME" ]

Docker 不是虚拟机,容器中的应用都应该以前台执行,容器内没有后台服务的概念。对于容器而言,其启动程序就是容器应用进程,容器就是为了主进程而存在的,主进程退出,容器就失去了存在的意义,从而退出,例如:

  1. # shell格式的写法,容器执行后立即退出了
  2. CMD service nginx start
  3. # 实际上会被理解为下面的语句,也就是说主进程是sh,而当service nginx start 命令结束后,
  4. # sh 也就结束了,sh 作为主进程退出了,自然就会令容器退出。
  5. CMD [ "sh", "-c", "service nginx start"]
  6. # 正确的做法是,直接执行nginx 可执行文件,并且要求以前台形式运行
  7. CMD ["nginx", "-g", "daemon off;"]

ENTRYPOINT 入口点

ENTRYPOINT 的格式和 RUN 指令格式一样,分为 exec 格式和 shell 格式。

ENTRYPOINT 的目的和 CMD 一样,都是在指定容器启动程序及参数。ENTRYPOINT 在运行时也可以替代,不过比 CMD 要略显繁琐,需要通过 docker run 的参数 —entrypoint 来指定。当指定了 ENTRYPOINT 后,CMD 的含义就发生了改变,不再是直接的运行其命令,而是将 CMD 的内容作为参数传给 ENTRYPOINT 指令,换句话说实际执行时,将变为:

  1. <ENTRYPOINT> "<CMD>"

在描述场景前,我测试了一下这两个命令,在只使用entrypoint且不带参数docker run的情况下,会直接执行entrypoint里的语句,但是当带参数docker run的时候,所传的参数就会追加在entrypoint里语句的后面。

  1. [root@Setsuna test]# cat Dockerfile
  2. FROM centos
  3. # CMD [ "p in cmd" ]
  4. ENTRYPOINT [ "echo","xx" ]
  5. [root@Setsuna test]# docker run test
  6. xx
  7. [root@Setsuna test]# docker run test xx
  8. xx xx

而使用CMD又使用entrypoint,entrypoint中只含命令不含参数的情况是这样的:

  1. [root@Setsuna test]# cat Dockerfile
  2. FROM centos
  3. CMD [ "p in cmd" ]
  4. ENTRYPOINT [ "echo" ]
  5. [root@Setsuna test]# docker run test
  6. p in cmd
  7. [root@Setsuna test]# docker run test xx
  8. xx

当使用CMD又使用带参数的entrypoint的时候,是这样的:

  1. [root@Setsuna test]# cat Dockerfile
  2. FROM centos
  3. CMD [ "p in cmd" ]
  4. ENTRYPOINT [ "echo","xx" ]
  5. [root@Setsuna test]# docker run test
  6. xx p in cmd
  7. [root@Setsuna test]# docker run test xx
  8. xx xx

因此我们可以总结一下:

  • 当只使用CMD时,docker run 会执行CMD里的语句,带参run的时候相当于只执行这个参数,大概率会报错。
  • 当只使用ENTRYPOINT时,docker run 会执行ENTRYPOINT里的语句,带参run的时候会把参数追加到ENTRYPOINT里语句的末尾。
  • 当同时使用CMD和ENTRYPOINT时,无论ENTRYPOINT中只有命令还是带参命令,docker run 执行时都会把CMD中的语句当做传入ENTRYPOINT的语句里;而带参执行docker run时,传入的参数会把CMD中的语句覆盖掉,作为参数传入ENTRYPOINT的语句里。

接下来从场景的角度出发,来理解这两个指令。

场景一:让镜像变成像命令一样使用
假设我们需要一个得知自己当前公网 IP 的镜像,那么可以先用 CMD 来实现:

  1. FROM ubuntu:18.04
  2. RUN apt-get update \
  3. && apt-get install -y curl \
  4. && rm -rf /var/lib/apt/lists/*
  5. CMD [ "curl", "-s", "http://myip.ipip.net" ]

使用docker build -t myip .构建镜像后,如果需要查询当前公网IP,只需要执行:

  1. $ docker run myip
  2. 当前 IP61.148.226.66 来自:北京市 联通

但此时如果希望显示 HTTP 头信息,就需要加上 -i 参数,但会直接报错,因为上面说过,只用CMD的情况下,带参执行docker run时,参数会覆盖掉CMD里所有的内容。

  1. $ docker run myip -i
  2. docker: Error response from daemon: invalid header field value "oci runtime error: container_linux.go:247: starting container process caused \"exec: \\\"-i\\\": executable file not found in $PATH\"\n".

而此时就可以使用ENTRYPOINT来解决问题,即:

  1. FROM ubuntu:18.04
  2. RUN apt-get update \
  3. && apt-get install -y curl \
  4. && rm -rf /var/lib/apt/lists/*
  5. ENTRYPOINT [ "curl", "-s", "http://myip.ipip.net" ]

同样前面说过,只使用ENTRYPOINT时,带参执行docker run会追加把参数放到ENTRYPOINT内容的后面,因此可以正常运行。

  1. $ docker run myip -i
  2. HTTP/1.1 200 OK
  3. Server: nginx/1.8.0
  4. Date: Tue, 22 Nov 2016 05:12:40 GMT
  5. Content-Type: text/html; charset=UTF-8
  6. Vary: Accept-Encoding
  7. X-Powered-By: PHP/5.6.24-1~dotdeb+7.1
  8. X-Cache: MISS from cache-2
  9. X-Cache-Lookup: MISS from cache-2:80
  10. X-Cache: MISS from proxy-2_6
  11. Transfer-Encoding: chunked
  12. Via: 1.1 cache-2:80, 1.1 proxy-2_6:8006
  13. Connection: keep-alive
  14. 当前 IP61.148.226.66 来自:北京市 联通

场景二:应用运行前的准备工作
启动容器就是启动主进程,但有些时候,启动主进程前,需要一些准备工作。
比如 mysql 类的数据库,可能需要一些数据库配置、初始化的工作,这些工作要在最终的 mysql 服务器运行之前解决。
此外,可能希望避免使用 root 用户去启动服务,从而提高安全性,而在启动服务前还需要以 root 身份执行一些必要的准备工作,最后切换到服务用户身份启动服务。或者除了服务外,其它命令依旧可以使用 root 身份执行,方便调试等。
这些准备工作是和容器 CMD 无关的,无论 CMD 为什么,都需要事先进行一个预处理的工作。这种情况下,可以写一个脚本,然后放入 ENTRYPOINT 中去执行,而这个脚本会将接到的参数(也就是 )作为命令,在脚本最后执行。比如官方镜像 redis 中就是这么做的

  1. FROM alpine:3.4
  2. ...
  3. RUN addgroup -S redis && adduser -S -G redis redis
  4. ...
  5. ENTRYPOINT ["docker-entrypoint.sh"]
  6. EXPOSE 6379
  7. CMD [ "redis-server" ]

其中为了 redis 服务创建了 redis 用户,并在最后指定了 ENTRYPOINT 为 docker-entrypoint.sh 脚本。

  1. #!/bin/sh
  2. ...
  3. # allow the container to be started with `--user`
  4. if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then
  5. find . \! -user redis -exec chown redis '{}' +
  6. exec gosu redis "$0" "$@"
  7. fi
  8. exec "$@"

该脚本的内容就是根据 CMD 的内容来判断,如果是 redis-server 的话,则切换到 redis 用户身份启动服务器,否则依旧使用 root 身份执行。比如:

  1. $ docker run -it redis id
  2. uid=0(root) gid=0(root) groups=0(root)

ENV 设置环境变量

命令格式为

  1. ENV <key> <value>
  2. ENV <key1>=<value1> <key2>=<value2>...

用于设置环境变量,运行时的应用和后面的指令,都可以直接使用这里定义的环境变量。

  1. ENV VERSION=1.0 DEBUG=on \
  2. NAME="Happy Feet"

这个例子中演示了如何换行,以及对含有空格的值用双引号括起来的办法,这和 Shell 下的行为是一致的。
定义好变量后,就可以使用

  1. ENV NODE_VERSION 7.2.0
  2. RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \
  3. && curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \
  4. && gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \
  5. && grep " node-v$NODE_VERSION-linux-x64.tar.xz\$" SHASUMS256.txt | sha256sum -c - \
  6. && tar -xJf "node-v$NODE_VERSION-linux-x64.tar.xz" -C /usr/local --strip-components=1 \
  7. && rm "node-v$NODE_VERSION-linux-x64.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt \
  8. && ln -s /usr/local/bin/node /usr/local/bin/nodejs

ARG 构建参数

格式:

  1. ARG <参数名>[=<默认值>]

构建参数和 ENV 的效果一样,都是设置环境变量。所不同的是,ARG 所设置的构建环境的环境变量,在将来容器运行时是不会存在这些环境变量的。
Dockerfile 中的 ARG 指令是定义参数名称,以及定义其默认值。该默认值可以在构建命令 docker build 中用 —build-arg <参数名>=<值> 来覆盖。
ARG 指令有生效范围,如果在 FROM 指令之前指定,那么只能用于 FROM 指令中。

  1. # 在第一个 FROM 之前的所有 ARG , 在所有 FROM 中生效, 仅在 FROM 中生效
  2. ARG DOCKER_USERNAME=library
  3. FROM ${DOCKER_USERNAME}/alpine
  4. RUN set -x ; echo 1
  5. FROM ${DOCKER_USERNAME}/alpine
  6. RUN set -x ; echo 2
  7. # 要想在FROM之后使用,必须再次指定,且在FROM后的ARG仅在当前FROM作用于生效,即在当前阶段生效
  8. ARG DOCKER_USERNAME=library
  9. RUN set -x ; echo ${DOCKER_USERNAME}

VOLUME 定义匿名卷

格式:

  1. VOLUME ["<路径1>", "<路径2>"...]
  2. VOLUME <路径>

容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中,为了防止运行时用户忘记将动态文件所保存目录挂载为卷,在 Dockerfile 中,我们可以事先指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据。

  1. VOLUMNE /data

这里的 /data 目录就会在容器运行时自动挂载为匿名卷,任何向 /data 中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。当然,运行容器时可以覆盖这个挂载设置。比如:

  1. docker run -d -v mydata:/data xxxx

在这行命令中,就使用了 mydata 这个命名卷挂载到了 /data 这个位置,替代了 Dockerfile 中定义的匿名卷的挂载配置。

EXPOSE 声明端口

格式:

  1. EXPOSE <端口1> [<端口2>...]

EXPOSE 指令是声明容器运行时提供服务的端口,这只是一个声明,在容器运行时并不会因为这个声明应用就会开启这个端口的服务。在 Dockerfile 中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另一个用处则是在运行时使用随机端口映射时,也就是 docker run -P 时,会自动随机映射 EXPOSE 的端口。(注意这里是-P,大写的,小写的-p <宿主端口>:<容器端口>才是手动指定端口服务映射到宿主端口)。

WORKDIR 指定工作目录

格式:

  1. WORKDIR <工作目录路径>

使用 WORKDIR 指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR 会帮你建立目录。
一个典型的场景是,把 Dockerfile 等同于 Shell 脚本来书写

  1. RUN cd /app
  2. RUN echo "hello" > world.txt

进行构建镜像运行后,会发现找不到 /app/world.txt 文件,或者其内容不是 hello。在 Shell 中,连续两行是同一个进程执行环境,因此前一个命令修改的内存状态,会直接影响后一个命令;而在 Dockerfile 中,这两行 RUN 命令的执行环境根本不同,是两个完全不同的容器。这就是对 Dockerfile 构建分层存储的概念不了解所导致的错误。(ps:可恶,我现在也不了解)

因此如果需要改变以后各层的工作目录的位置,那么应该使用 WORKDIR 指令。(当cd用就完事了)

  1. WORKDIR /app
  2. RUN echo "hello" > world.txt

USER 指定当前用户

格式:

  1. USER <用户名>[:<用户组>]

USER 指令和 WORKDIR 相似,都是改变环境状态并影响以后的层。USER的作用是改变之后层的执行 RUN, CMD 以及 ENTRYPOINT 这类命令的身份。

  1. RUN groupadd -r redis && useradd -r -g redis redis
  2. USER redis
  3. RUN [ "redis-server" ]

如果以 root 执行的脚本,在执行期间希望改变身份,比如希望以某个已经建立好的用户来运行某个服务进程,不要使用 su 或者 sudo,这些都需要比较麻烦的配置,而且在 TTY 缺失的环境下经常出错。建议使用 gosu。

  1. # 建立 redis 用户,并使用 gosu 换另一个用户执行命令
  2. RUN groupadd -r redis && useradd -r -g redis redis
  3. # 下载 gosu
  4. RUN wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/1.12/gosu-amd64" \
  5. && chmod +x /usr/local/bin/gosu \
  6. && gosu nobody true
  7. # 设置 CMD,并以另外的用户执行
  8. CMD [ "exec", "gosu", "redis", "redis-server" ]

HEALTHCHECK 健康检查

格式:

  1. HEALTHCHECK [选项] CMD <命令>:设置检查容器健康状况的命令
  2. HEALTHCHECK NONE:如果基础镜像有健康检查指令,使用这行可以屏蔽掉其健康检查指令

HEALTHCHECK 指令指定一行命令,用这行命令来判断容器主进程的服务状态是否还正常,从而比较真实的反应容器实际状态。在没有 HEALTHCHECK 指令前,Docker 引擎只可以通过容器内主进程是否退出来判断容器是否状态异常。很多情况下这没问题,但是如果程序进入死锁状态,或者死循环状态,应用进程并不退出,但是该容器已经无法提供服务了。在 1.12 以前,Docker 不会检测到容器的这种状态,从而不会重新调度,导致可能会有部分容器已经无法提供服务了却还在接受用户请求。

当在一个镜像指定了 HEALTHCHECK 指令后,用其启动容器,初始状态会为 starting,在 HEALTHCHECK 指令检查成功后变为 healthy,如果连续一定次数失败,则会变为 unhealthy。

HEALTHCHECK 支持下列选项:

  • —interval=<间隔>:两次健康检查的间隔,默认为 30 秒;
  • —timeout=<时长>:健康检查命令运行超时时间,如果超过这个时间,本次健康检查就被视为失败,默认 30 秒;
  • —retries=<次数>:当连续失败指定次数后,则将容器状态视为 unhealthy,默认 3 次。

web服务的Dockerfile的HEALTHCHECK可以这么写:

  1. FROM nginx
  2. RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
  3. HEALTHCHECK --interval=5s --timeout=3s \
  4. CMD curl -fs http://localhost/ || exit 1

为了帮助排障,健康检查命令的输出(包括 stdout 以及 stderr)都会被存储于健康状态里,可以用 docker inspect 来查看。

ONBUILD 为他人做嫁衣裳

格式:

  1. ONBUILD <其它指令>

在一个Dockerfile文件中加上ONBUILD指令,该指令对利用该Dockerfile构建镜像(比如为A镜像)不会产生实质性影响。当以当前镜像为基础镜像,去构建下一级镜像的时候才会被执行。实际上就是相当于创建一个模板,后续可以根据该模板镜像创建特定的子镜像,在ONBUILD中指定一些通用操作,从而减少dockerfile文件的重复内容编写。

先编写一个Dockerfile,内容如下:

  1. FROM ubuntu
  2. MAINTAINER hello
  3. ONBUILD RUN mkdir mydir

然后构建镜像:docker build -t imagea .
再编写一个新的Dockerfile,内容如下:

  1. FROM imagea
  2. MAINTAINER hello1

然后构建镜像:docker build -t imageb .
创建容器后,发现根目录下有mydir目录,说明ONBUILD生效了。

LABEL 为镜像添加元数据

格式:

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

例如:

  1. LABEL org.opencontainers.image.authors="yeasy"

SHELL 指令

格式:

  1. SHELL ["executable", "parameters"]

用于指定RUN ENTRYPOINT CMD指令的shell,linux中默认为[“/bin/sh”, “-c”]
例如:

  1. SHELL ["/bin/sh", "-c"]
  2. RUN lll ; ls
  3. SHELL ["/bin/sh", "-cex"]
  4. RUN lll ; ls

Dockerfile多阶段构建

Docker 17.05版本前,构建Docker镜像时,通常会采用两种方式

  • 全部放入一个Dockerfile:将所有的构建过程包含在一个Dockerfile中,包括项目及其依赖库的编译、测试、打包。缺点是镜像层次多,镜像体积较大,部署时间变长,源代码存在泄露的风险。
  • 分散到多个Dockerfile:事先在一个Dockerfile将项目及其依赖库编译测试打包好后,再将其拷贝到运行环境中,这种方式需要编写两个Dockerfile和一些编译脚本才能将两个阶段整合起来。

而17.05版本开始,Docker开始支持多阶段构建,每个FROM至下一个FROM之间,就是一个阶段,as用于为某一阶段命名。多阶段构建实现的效果和方法2是一样的。

  1. FROM golang:alpine as builder
  2. RUN apk --no-cache add git
  3. WORKDIR /go/src/github.com/go/helloworld/
  4. RUN go get -d -v github.com/go-sql-driver/mysql
  5. COPY app.go .
  6. RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
  7. FROM alpine:latest as prod
  8. RUN apk --no-cache add ca-certificates
  9. WORKDIR /root/
  10. COPY --from=0 /go/src/github.com/go/helloworld/app .
  11. CMD ["./app"]

通过别名,我们可以只构建某个阶段的镜像,只需要增加—target参数即可

  1. $ docker build --target builder -t username/imagename:tag .

构建时,我们可以从上一阶段的镜像中复制文件,如上例中的,注意这里—from=0指的是base image number,0指的即就是第一个镜像,因此这里也可以写成—from=builder。

  1. COPY --from=0 /go/src/github.com/go/helloworld/app .

同样,也可以复制任意镜像中的文件。

  1. $ COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf

构建多种系统架构支持的Docker镜像

使用镜像创建一个容器,该镜像必须与 Docker 宿主机系统架构一致,例如 Linux x86_64 架构的系统中只能使用 Linux x86_64 的镜像创建容器。对于不同系统架构环境,通常采用的做法是通过镜像名区分不同系统架构的镜像,例如在 Linux x86_64 和 Linux arm64v8 分别构建 username/test 和username/arm64v8-test 镜像。运行时使用对应架构的镜像即可。

但这样的操作非常繁琐,这个时候就可以用到manifest。当用户获取一个镜像时,Docker 引擎会首先查找该镜像是否有 manifest 列表,如果有的话 Docker 引擎会按照 Docker 运行环境(系统及架构)查找出对应镜像,如果没有的话会直接获取镜像。

manifest列表包含了不同系统架构所对应的镜像 digest 值,这样 Docker 就可以在不同的架构中使用相同的manifest获取对应的镜像。下面讲述如何使用manifest。

构建镜像

首先在 Linux x86_64 构建 username/x8664-test 镜像。并在 Linux arm64v8 中构建 username/arm64v8-test 镜像,构建好之后推送到 Docker Hub。

创建manifest列表

当要修改一个 manifest 列表时,可以加入 -a 或 —amend 参数。

  1. # $ docker manifest create MANIFEST_LIST MANIFEST [MANIFEST...]
  2. $ docker manifest create username/test \
  3. username/x8664-test \
  4. username/arm64v8-test

设置manifest列表

—os参数指定系统,—arch参数指定对应系统架构

  1. # $ docker manifest annotate [OPTIONS] MANIFEST_LIST MANIFEST
  2. $ docker manifest annotate username/test \
  3. username/x8664-test \
  4. --os linux --arch x86_64
  5. $ docker manifest annotate username/test \
  6. username/arm64v8-test \
  7. --os linux --arch arm64 --variant v8

查看manifest列表

  1. $ docker manifest inspect username/test

推送manifest列表

  1. $ docker manifest inspect username/test

其他制作镜像的方式

从rootfs压缩包导入

格式:

  1. docker import [选项] <文件>|<URL>|- [<仓库名>[:<标签>]]

压缩包可以是本地文件、远程 Web 文件,甚至是从标准输入中得到。压缩包将会在镜像 / 目录展开,并直接作为镜像第一层提交。
例如下面命令会自动下载tar包,并作为根文件系统展开导入,保存为镜像:

  1. $ docker import \
  2. http://download.openvz.org/template/precreated/ubuntu-16.04-x86_64.tar.gz \
  3. openvz/ubuntu:16.04
  4. Downloading from http://download.openvz.org/template/precreated/ubuntu-16.04-x86_64.tar.gz
  5. sha256:412b8fc3e3f786dca0197834a698932b9c51b69bd8cf49e100c35d38c9879213

Docker 镜像的导入和导出

使用docker save可以将镜像保存为归档文件,注意,如果同名则会覆盖。

  1. docker save alpine -o filename

使用docker load加载镜像

  1. $ docker load -i alpine-latest.tar.gz

镜像的实现原理

每个镜像都由很多层次构成,Docker 使用 Union FS 将这些不同的层结合到一个镜像中去。

通常 Union FS 有两个用途, 一方面可以实现不借助 LVM、RAID 将多个 disk 挂到同一个目录下,另一个更常用的就是将一个只读的分支和一个可写的分支联合在一起,Live CD 正是基于此方法可以允许在镜像不变的基础上允许用户在其上进行一些写操作。

Docker 在 OverlayFS 上构建的容器也是利用了类似的原理。