使用 Dockerfile 可以生成镜像,他由固定的语法组成,所以这里基本是摘录

语法

Dockerfile 的文件格式一般都是以一个关键字开头,比如上面例子中的 FROM 和 COPY,其中 FROM 表示引用的基础镜像,COPY 表示拷贝一个文件到镜像中。Dockerfile 支持的常用关键字主要有如下几种:

  • FROM : 指定引用的基础镜像;
  • MAINTAINER ~~: ~~指定该 Dockerfile 的维护者信息,这个在公司内部的时候比较有用,我们可以将公司内部的邮箱或者工号写上;(过时了)
  • LABEL : 用来指定镜像的一些元信息;
  • RUN : 运行该关键字之后紧跟着的命令,比如 RUN cp 1 /tmp;
  • COPY : 文件或者文件夹拷贝;
  • ADD : 该命令也是文件拷贝,后面我们会详细比较它和 COPY 的区别;
  • EXPOSE:设置容器的监听端口;
  • WORKDIR: 指定工作目录;
  • ENV: 指定环境变量;
  • VOLUME:指定磁盘挂载点;
  • USER: 指定命令的执行用户;
  • CMD: 指定该镜像的默认启动命令,CMD 只需要指定一个即可,形如 CMD [“executable”, “param1”, “param2”] 。如果指定了多个 CMD,只有最后一个会生效;
  • ENTRYPOINT:容器的默认启动入口。比如我们使用 docker run 启动容器时,容器的默认启动入口就是镜像中通过该指令指定的命令或者脚本,我们可以在外面使用参数将其覆盖掉;
  • ARG:在 build 的时候指定参数;
  • 注释:注释以 # 开始。

    FROM

    FROM 必须是 Dockerfile 的第一条有效命令,所谓有效是指非注释命令。FROM 后面跟随的 docker 镜像可以有多种格式,比如:
    1. FROM nginx
    2. FROM nginx:1.19
    3. FROM nginx@digest
    如果没有指定 tag 或者 digest,则表示把该镜像的 latest 版本作为基础镜像。当然在生产环境下,latest 版本永远不应该作为基础镜像,因为如果将 latest 版本作为基础镜像就意味着镜像会一直变化。

    任何时候,base 镜像尽量使用官方的镜像,比如 Alpine 镜像,作为一个完整的 Linux 发行版,大小不足 5MB。

LABEL

正如上面说说,LABEL 用来添加镜像的 metadata,格式也比较简单,如下,也就是说这里的 metadata 都是 key value 对。

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

一旦 Dockerfile 中增加了 LABEL 信息,build 出来的镜像则可以通过 docker inspect 命令进行查看。下面就是我们通过 docker inspect 查看到的 nginx 镜像的 label 信息。

我们可以通过给镜像添加 label 来管理我们的镜像,比如记录 license 信息等。下面是集中比较好的 LABEL 编写格式。

Set multiple labels on one lineLABEL com.example.version=”0.0.1-beta” com.example.release-date=”2015-02-12”

Set one or more individual labelsLABEL com.example.version=”0.0.1-beta”

LABEL vendor1=”ACME Incorporated” LABEL vendor2=ZENITH\ Incorporated LABEL com.example.release-date=”2015-02-12” LABEL com.example.version.is-production=””

RUN

RUN 命令表示运行后面紧跟的命令,有如下两种形式。

  1. RUN <command>
  2. RUN ["executable", "param1", "params"]

这个命令应该是 Dockerfile 中最常用的命令了,比如我们要在镜像中通过 yum 安装 redis,那么我们可以通过如下方式编写我们的 Dockerfile。

  1. RUN yum install redis
  2. or
  3. RUN ["yum", "install", "redis"]

RUN 还有一点需要注意的是 Dockerfile 中的每一个 RUN 命令都会生成一个新的镜像层

注意事项

RUN 指令的一个典型应用就是和 apt-get 结合起来使用,我们这里看一下 apt-get 的使用注意事项。

  1. 不要在 dockerfile 中使用 RUN apt-get upgrade 或者 dist-upgrade ,因为 upgrade 会升级镜像中安装的所有包(如果包有更新的话)。取而代之的是,我们可以使用 apt-get update 获取更新的软件包列表,然后如果确定要升级的话再使用 apt-get install -y foo 去自动更新
  2. 将 apt-get udpate 和 apt-get install 写到一条 RUN 的指令中,也就是像下面这样。
    1. RUN apt-get update && apt-get install -y \
    2. package-bar \
    3. package-baz \
    4. package-foo
    如果把 apt-get update 和 apt-get install 分开编写的话可能会因为 docker build cache 的问题导致没有安装最新的包,举个例子。
    1. FROM ubuntu:18.04
    2. RUN apt-get update
    3. RUN apt-get install -y curl
    通过 docker build 之后,上面 dockerfile 生成的所有文件层都在 Docker cache 中。如果你之后想安装其他的软件包,比如 nginx,然后将 dockerfile 修改成如下的样子。
    1. FROM ubuntu:18.04
    2. RUN apt-get update
    3. RUN apt-get install -y curl nginx
    重新执行 docker build 的时候,由于 cache 的原因,RUN apt-get update 这一行并不会被重新执行,也就是说我们可能会 apt-get install 安装的不是最新版本软件包。

将 apt-get update 和 apt-get install 写在一行就是典型的 cache-busting 技术。

使用pipes

有些 RUN 指令后面的命令涉及的 Linux 的管道(pipe),比如将一个命令执行的输出作为下一个命令的输入。比如下面这个例子:先 wget 下载一个文件,然后使用 wc 统计行数。

  1. RUN wget -O - https://some.site | wc -l > /number

Docker 执行 RUN 后面的指令是使用 /bin/sh -c 来执行的,对于上面的管道情况,只会把最后一个命令的返回值来作为整个管道链接起来的这条命令的返回值。也就是说上面这条 dockerfile 的指令,只要 wc -l 执行成功 Docker 就认为这条指令 docker build 成功了。但是这个不是符合预期的,比如前面的 wget 执行失败,应该导致 build 失败才是预期的。

为了解决这个问题,或者说解决此类问题:对于管道中的任何阶段的命令失败都导致 build 失败,我们可以使用 set -o pipefail 来解决。

  1. RUN set -o pipefail && wget -O - https://some.site | wc -l > /number

COPY & ADD

COPY 用来拷贝文件或者文件夹。如

  1. COPY hom* /mydir/

ADD 类似 COPY,可以认为是增强版的 COPY。区别主要体现在两个地方:

  • 当 ADD 后面跟压缩文件时,拷贝的时候会将压缩文件进行解压。
  • ADD 可以用来下载网上的文件
    1. ADD html.tar.gz /var/www/html
    2. ADD https://xxx.com/html.tar.gz /var/www/html

    最佳实践

    ADD 的最佳实践是将本地的 tar 文件提取到镜像中,例如 ADD rootfs.tar.xz ,这里所说的提取包括拷贝和解压。
    如果需要拷贝多个文件,那么在 dockerfile 文件中最好每次拷贝一个单独的文件,这样的好处是我们可以利用 Docker 的 build cache,每次一个文件变化只会影响单个层的 build cache 失效。举个例子。
    1. COPY . /tmp/
    2. RUN ...
    上面的 dockerfile 只要当前目录的任何一个文件变化都会导致 COPY . /tmp 层重新构建,导致后面的指令的 build cache 缓存失效。
    为了让镜像尽量小,最好不要使用 ADD 指令从远程 URL 获取包,而是使用 curl 或者 wget 先下载包,使用完之后将包删除掉。

    EXPOSE

    表明 Docker 应用内部监听的端口,可以指定端口的协议是 TCP 还是 UDP,没有指定认为是 TCP。
    比如我们要暴露 80 端口。
    1. EXPOSE 80
    不过这个只是做了一个声明,到时候可以使用 -p命令将端口映射出去
    1. docker run -p 80:80 ...

    WORKDIR

    指定工作目录。一旦指定,则后面的命令(比如 RUN)的工作目录都是 WORKDIR 指定的目录,也就是说命令 pwd 的输出就是 WORKDIR

    ENV

    用来指定环境变量,格式如下:
    1. ENV <key> <value> ENV <key>=<value> ...
    其意义类似我们在 Linux 中使用的 export 导入环境变量。

比如对于 nginx 镜像我们可以将 nginx 的 bin 加到环境变量 PATH 中,然后 CMD 指定 nginx 就可以直接使用了。

  1. ENV PATH /usr/local/nginx/bin:$PATH
  2. ...
  3. CMD ["nginx"]

ENV 除了设置用户的自定义环境变量,有时候还可以用来设置版本号,类似于我们编程中的常量。

  1. ENV PG_MAJOR 9.3
  2. ENV PG_VERSION 9.3.4
  3. RUN curl -SL http://example.com/postgres-$PG_VERSION.tar.xz | tar -xJC /usr/src/postgress && …
  4. ENV PATH /usr/local/postgres-$PG_MAJOR/bin:$PATH

VOLUME

VOLUME 会为镜像生成一个新的挂载点。但是我们也可以不使用 VOLUME,而是在 docker run 的时候通过 -v 参数指定。

  1. VOLUME ["/data"]
  2. VOLUME /data

CMD

CMD 用来为镜像指定一个默认的启动命令,所谓默认是说我们可以通过 docker run 命令行参数通过其他的命令来覆盖 CMD。CMD 支持的格式如下:

  1. CMD ["executable","param1","param2"]
  2. CMD command param1 param2
  3. CMD ["param1","param2"]

其中第三种方式,会将 param1 和 param2 作为参数传递给 ENTRYPOINT。我们可以通过如下的方式通过 docker run 指定 docker 的启动命令为 /bin/bash 覆盖 CMD。

  1. docker run <params> <image> /bin/bash

ENTRYPOINT

ENTRYPOINT 类似 CMD,也是指定镜像的默认启动命令,但是不能像上面那样 docker run 的方式来覆盖。那么问题来了,如果我想通过其他的启动程序来验证我的镜像,我怎么做呢?举个例子,ENTRYPOINT 指定的是应用程序启动,但是我们启动的过程中一直启动不了,所以我想通过 /bin/bash 想把 docker 启动起来,然后再检测依赖环境,那么怎么做呢?
答案是通过 —entrypoint 参数来覆盖。

  1. docker run --entrypoint /bin/bash ...

最佳实践

ENTRYPOINT 的最佳实践是设置镜像的主命令,使用该镜像启动容器的时候将会执行 ENTRYPOINT 中指定的命令。CMD 可以作为 ENTRYPOINT 的补充,指定主命令的默认参数。

  1. ENTRYPOINT ["s3cmd"]
  2. CMD ["--help"]

ENTRYPOINT 还可以结合一个辅助脚本使用,下面是 Postgres 官方镜像使用的脚本和 ENTRYPOINT 设置。

  1. #!/bin/bash
  2. set -e
  3. if [ "$1" = 'postgres' ]; then
  4. chown -R postgres "$PGDATA"
  5. if [ -z "$(ls -A "$PGDATA")" ]; then
  6. gosu postgres initdb
  7. fi
  8. exec gosu postgres "$@"
  9. fi
  10. exec "$@"
  11. COPY ./docker-entrypoint.sh /
  12. ENTRYPOINT ["/docker-entrypoint.sh"]
  1. COPY ./docker-entrypoint.sh /
  2. ENTRYPOINT ["/docker-entrypoint.sh"]

shell 脚本的意思当启动参数中的第一个参数是 postgres 时,会做一些和 $PGDATA 相关的工作,最后再调用 Linux 的系统命令 exec 执行所有参数。借助于这个 ENTRYPOINT ,我们就可以以多种方式启动容器。

ARG

例子

这里给一个实际项目的例子

  1. FROM adoptopenjdk/openjdk11:alpine
  2. LABEL gxz=469648819@qq.com
  3. #复制上下文目录下的target/demo-1.0.0.jar 到容器里
  4. COPY /target/sync-0.0.1.jar /app/sync-0.0.1.jar
  5. ENV TZ Asia/Shanghai
  6. RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
  7. VOLUME /app/sync_log
  8. EXPOSE 4000
  9. ENTRYPOINT ["java","-jar","/app/sync-0.0.1.jar"]