目 录CONTENT

文章目录

Docker构建镜像

简中仙
2023-01-18 / 0 评论 / 0 点赞 / 81 阅读 / 0 字 / 正在检测是否收录...
温馨提示:
本文最后更新于2023-09-28,若内容或图片失效,请留言反馈。 本文如有错误或者侵权的地方,欢迎您批评指正!

一、Dockerfile简介

什么是 Dockerfile?

Dockerfile 是一个用来构建镜像的文本文件,文本内容包含了构建镜像所需的指令和说明。

在构建时docker Engine 会在当前目录中查找名为“Dockerfile”的文件,注意这个文件名必须为“Dockerfile”首字母大写。当您运行docker build 命令的时候,Dockerfile文件以及与Dockerfile文件处于同级路径下的所有文件都将一并提交至docker Engine来处理,docker Engine将逐行解析Dockerfile文件,并执行相关操作。Dockerfile的解析是由上而下的顺序进行。

注意您放置Dockerfile的目录最好是一个空目录,或者只放置与本次构建相关的文件即可,因为在执行build时会将Dockerfile以及Dockerfile路径下的所有文件提交至docker Engine处理,因此您不应该在Dockerfile目录中放置一些无关的文件,这是无意义的并且这将会严重拖慢整个构建过程。

二、Dockerfile指令

1、常用指令

Docker镜像可以通过Docker hub或者阿里云等仓库中获取,这些镜像是由官方或者社区人员提供的,对于Docker用户来说并不能满足我们的需求,但是从无开始构建镜像成本大。常用的数据库、中间件、应用软件等都有现成的Docker官方镜像或社区创建的镜像,我们只需要稍作配置就可以直接使用。

使用现成镜像的好处除了省去自己做镜像的工作量外,更重要的是可以利用前人的经验。特别是使用那些官方镜像,因为Docker的工程师知道如何更好的在容器中运行软件。

当然,某些情况下我们也不得不自己构建镜像,比如找不到现成的镜像,比如自己开发的应用程序,需要在镜像中加入特定的功能。

在编写Dockerfile时,您需要用到Dockerfile指令来定义构建过程中所要执行的操作,这里列举几个常用的指令:

常用指令说明
FROMFROM指令是定义本阶段构建所要使用的基础镜像。
MAINTAINER镜像维护者姓名或邮箱地址
RUN构建镜像时运行的shell命令
CMD运行容器时执行的shell命令
EXPOSE仅仅只是声明端口,声明容器内应用所使用的端口号
ENV设置容器环境变量,当然在您使用该镜像运行容器时,ENV所定义的变量仍然对容器有效
ADD拷贝文件或目录到镜像,如果是URL或压缩包会自动下载或自动解压
COPY拷贝文件或目录到镜像容器内,跟ADD类似,但不具备自动下载或解压功能
ENTRYPOINT运行容器时执行的shell命令
VOLUME指定容器挂载点到宿主机自动生成的目录或其他容器
USER为RUN、CMD、和ENTRYPOINT执行命令指定运行用户
WORKDIR为RUN、CMD、ENTRYPOINT、COPY和ADD设置工作目录,意思为切换目录
HEALTHCHECK健康检查
ARG构建时指定的一些参数

RUN、CMD 与 ENTRYPOINT 的区别

  • RUN:执行命令并创建新的镜像层,常用于安装软件包,在 docker build过程中执行;
  • CMD:设置容器启动后默认执行的命令及其参数,在docker run 时运行,但 docker run 后跟参数时会替换(忽略) CMD,CMD可以被覆盖,如果有ENTRYPIOINT的话,CMD就是ENTRYPIOINT的参数。;
  • ENTRYPOINT:配置容器启动时运行的命令,不管 docker run … 后是否运行有其他命令,ENTRYPOINT 指令后的命令一定会被执行, 后面如果再接命令,会报错多余的参数。
  • CMD和ENTRYPIOINT 必须要有一个

2、指令详解

1、FROM

FROM指令是定义本阶段构建所要使用的基础镜像。例如您需要将您的java开发的项目构建为一个镜像,那么您可能需要tomcat镜像,而制作tomcat镜像就可能需要jdk镜像,那么jdk镜像就需要一个基本的操作系统镜像,这可能是utuntu、debian、alpine等。

根据以上,我们应该可以知道由于容器镜像是可以增量叠加的,那么tomcat镜像中就应该包含了jdk以及一个基本的操作系统。

您可以从hub.docker.com网站上来挑选一个合适的镜像来作为您的基础镜像。

2、COPY

复制指令,从上下文目录中复制文件或者目录到容器里指定路径

(上下文目录是指放置Dockerfile文件的目录)

格式如下:

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

注意[--chown=<user>:<group>]:可选参数,用户改变复制到容器内文件的拥有者和属组。

<源路径>:源文件或者源目录,这里可以是通配符表达式,其通配符规则要满足 Go 的 filepath.Match 规则。例如:

COPY hom* /mydir/

COPY hom?.txt /mydir/

<目标路径>:容器内的指定路径,该路径不用事先建好,路径不存在的话,会自动创建。

3、ADD

ADD 指令和 COPY 的使用格式一致(同样需求下,官方推荐使用 COPY)。功能也类似,不同之处如下:

ADD 的优点:在执行 <源文件> 为 tar 压缩文件的话,压缩格式为 gzip, bzip2 以及 xz 的情况下,会自动复制并解压到 <目标路径>。

ADD 的缺点:在不解压的前提下,无法复制 tar 压缩文件。会令镜像构建缓存失效,从而可能会令镜像构建变得比较缓慢。具体是否使用,可以根据是否需要自动解压来决定。

对于使用ADD指令下载远程服务器上的tar包并解压,建议使用以下方式代替

RUN curl -s http://192.168.1.7/repository/tools/jdk-8u241-linux-x64.tar.gz | tar -xC /opt/

4、RUN

当我们在构建镜像过程中,需要执行一些配置,例如使用mkdir命令新建一个目录,用sed命令来替换一些文本内容等,当然可能在实际过程中需要更复杂的命令,那么此时您可以使用RUN指令来定义将要运行的命令。

RUN指令在Dockerfile中可以出现多次,docker Engine在构建过程中会读取Dockerfile然后由上而下依次执行RUN指令所标记的命令。

RUN后面跟着的命令行命令。有以下俩种格式:

Shell格式:

RUN <命令行命令>
# <命令行命令> 等同于,在终端操作的 shell 命令。

Exec格式:

RUN ["可执行文件", "参数1", "参数2"]
# 例如:
# RUN ["./test.php", "dev", "offline"] 等价于 RUN ./test.php dev offline

注意在构建过程中docker Engine会为Dockerfile中的每个RUN指令创建一个镜像层(layer)来记录这种改变。因此为了减少不必要的镜像层数,通常的做法是将多个命令定义在一个RUN指令中,如下所示:

RUN mkdir demo \
	&& cd demo \
	&& wget https://www.demo.com/download/demo.tar.gz \
	&& tar -xf demo.tar.gz \
	&& rm -rf demo.tar.gz

如上,以 && 符号连接命令,这样执行后,只会创建 1 层镜像

5、CMD

类似于 RUN 指令,用于运行程序,但二者运行的时间点不同:

CMD 在docker run 时运行。

RUN 是在 docker build过程中执行。

作用:为启动的容器指定默认要运行的程序,程序运行结束,容器也就结束。CMD 指令指定的程序可被 docker run 命令行参数中指定要运行的程序所覆盖。

::: warning
如果 Dockerfile 中如果存在多个 CMD 指令,仅最后一个生效。
:::

格式:

CMD <shell 命令> 
CMD ["<可执行文件或命令>","<param1>","<param2>",...] 
CMD ["<param1>","<param2>",...]  # 该写法是为 ENTRYPOINT 指令指定的程序提供默认参数

推荐使用第二种格式,执行过程比较明确。第一种格式实际上在运行的过程中也会自动转换成第二种格式运行,并且默认可执行文件是 sh。

例如您有一个nodejs程序,启动该程序的命令是node server.js,然后您希望在使用该镜像运行容器时也以这种方式运行,那么您可以在Dockerfile中使用CMD来标记,如下:

CMD ["node","server.js"]

通常CMD指令出现在Dockerfile末尾处。当您将以上定义写入Dockerfile中并打包成镜像(demo:v1),然后您使用该镜像运行时,那么该容器将使用Dockerfile中CMD定义的命令来运行

docker  run  -d  --name=demo  deom:v1

当然您可以在运行时覆盖CMD指令中的命令,例如这里改成npm start,命令如下

docker  run  -d  --name=demo  deom:v1  npm  start

6、ENTRYPOINT

类似于 CMD 指令,但其不会被 docker run 的命令行参数指定的指令所覆盖,而且这些命令行参数会被当作参数送给 ENTRYPOINT 指令指定的程序。

但是, 如果运行 docker run 时使用了 --entrypoint 选项,将覆盖 CMD 指令指定的程序。

优点:在执行 docker run 的时候可以指定 ENTRYPOINT 运行所需的参数。

注意:如果 Dockerfile 中如果存在多个 ENTRYPOINT 指令,仅最后一个生效。

格式:

ENTRYPOINT ["<executeable>","<param1>","<param2>",...]

可以搭配 CMD 命令使用:一般是变参才会使用 CMD ,这里的 CMD 等于是在给 ENTRYPOINT 传参,以下示例会提到。

示例:

假设已通过 Dockerfile 构建了 nginx:test 镜像:

FROM nginx
ENTRYPOINT ["nginx", "-c"] # 定参
CMD ["/etc/nginx/nginx.conf"] # 变参

不传参运行

docker run nginx:test

容器内会默认运行以下命令,启动主进程。

nginx -c /etc/nginx/nginx.conf

传参运行

docker run nginx:test -c /etc/nginx/new.conf

容器内会默认运行以下命令,启动主进程(/etc/nginx/new.conf:假设容器内已有此文件)

nginx -c /etc/nginx/new.conf

通常更好的做法是使用ENTRYPOINT 来定义一个docker-entrypoint.sh脚本,然后在该脚本中定义一些预处理及条件判断,来应对更多未知情况。例如cups镜像的Dockerfile

ARG ARCH=amd64
FROM $ARCH/debian:buster-slim
# environment
ENV ADMIN_PASSWORD=admin
# ……内容太多,中间内容已省略
ENTRYPOINT [ "docker-entrypoint.sh" ]
# default command
CMD ["cupsd", "-f"]
# volumes
VOLUME ["/etc/cups"]
# ports
EXPOSE 631

该镜像的docker-entrypoint.sh文件内容如下:

#!/bin/bash -e
echo -e "${ADMIN_PASSWORD}\n${ADMIN_PASSWORD}" | passwd admin
if [ ! -f /etc/cups/cupsd.conf ]; then
  cp -rpn /etc/cups-skel/* /etc/cups/
fi
exec "$@"

7、ENV

设置环境变量,定义了环境变量,那么在后续的指令中,就可以使用这个环境变量。当然在您使用该镜像运行容器时,ENV所定义的变量仍然对容器有效,例如MySQL镜像的Dockerfile中就包含root用户的初始密码,使用ENV来指定。

格式:

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

以下示例设置 NODE_VERSION = 16.19.0 , 在后续的指令中可以通过 $NODE_VERSION 引用:

ENV NODE_VERSION 16.19.0
RUN curl -SLO "https://nodejs.org/download/release/latest-v16.x/node-v$NODE_VERSION-linux-x64.tar.gz" 

Dockerfile中ENV指令像RUN指令一样,每一个都会创建一个临时层。

ENV JAVA_HOME=/opt/jdk1.8.0_241 \
    CLASSPATH=.:$JAVA_HOME/lib:$JAVA_HOME/jre/lib 
ENV PATH=$PATH:$JAVA_HOME/bin

8、ARG

构建参数,与 ENV 作用一至。不过作用域不一样。ARG 设置的环境变量仅对 Dockerfile 内有效,也就是说只有 docker build 的过程中有效,构建好的镜像内不存在此环境变量。

构建命令 docker build 中可以用 --build-arg <参数名>=<值> 来覆盖。

格式

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

9、VOLUME

定义匿名数据卷。在启动容器时忘记挂载数据卷,会自动挂载到匿名卷。

作用:

避免重要的数据,因容器重启而丢失,这是非常致命的。

避免容器不断变大。

格式:

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

在启动容器 docker run 的时候,我们可以通过 -v 参数修改挂载点。

10、EXPOSE

仅仅只是声明端口。

作用:

帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射。

在运行时使用随机端口映射时,也就是 docker run -P 时,会自动随机映射 EXPOSE 的端口。

格式:

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

11、WORKDIR

指定工作目录。用 WORKDIR 指定的工作目录,会在构建镜像的每一层中都存在。(WORKDIR 指定的工作目录,如果目录不存在则会自动创建)。

格式:

WORKDIR <工作目录路径>

12、USER

用于指定执行后续命令的用户和用户组,这边只是切换后续命令执行的用户(用户和用户组必须提前已经存在)。

格式:

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

13、HEALTHCHECK

用于指定某个程序或者指令来监控 docker 容器服务的运行状态。

格式:

HEALTHCHECK [选项] CMD <命令>:设置检查容器健康状况的命令
HEALTHCHECK NONE:如果基础镜像有健康检查指令,使用这行可以屏蔽掉其健康检查指令
HEALTHCHECK [选项] CMD <命令> : 这边 CMD 后面跟随的命令使用,可以参考 CMD 的用法。
HEALTHCHECK 支持下列选项:
--interval=<间隔>:两次健康检查的间隔,默认为 30 秒;
--timeout=<时长>:健康检查命令运行超时时间,如果超过这个时间,本次健康检查就被视为失败,默认 30 秒;
--retries=<次数>:当连续失败指定次数后,则将容器状态视为 unhealthy,默认 3 次。

和 CMD, ENTRYPOINT 一样,HEALTHCHECK 只可以出现一次,如果写了多个,只有最后一个生效。

在 HEALTHCHECK [选项] CMD 后面的命令,格式和 ENTRYPOINT 一样,分为 shell 格式,和 exec 格式。命令的返回值决定了该次健康检查的成功与否:0:成功;1:失败;2:保留,不要使用这个值。

假设我们有个镜像是个最简单的 Web 服务,我们希望增加健康检查来判断其 Web 服务是否在正常工作,我们可以用 curl 来帮助判断,其 Dockerfile 的 HEALTHCHECK 可以这么写:

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

当然您可以在 docker run 命令中,直接指明healthcheck相关策略,如下:

docker run --rm -d \
    --name=elasticsearch \
    --health-cmd="curl --silent --fail localhost:9200/_cluster/health || exit 1" \
    --health-interval=5s \
    --health-retries=12 \
    --health-timeout=2s \
    elasticsearch:7

14、ONBUILD

用于延迟构建命令的执行。简单的说,就是 Dockerfile 里用 ONBUILD 指定的命令,在本次构建镜像的过程中不会执行(假设镜像为 test-build)。当有新的 Dockerfile 使用了之前构建的镜像 FROM test-build ,这是执行新镜像的 Dockerfile 构建时候,会执行 test-build 的 Dockerfile 里的 ONBUILD 指定的命令。

格式:

ONBUILD <其它指令>

假设我们要制作 Node.js 所写的应用的镜像。我们都知道 Node.js 使用 npm 进行包管理,所有依赖、配置、启动信息等会放到 package.json 文件里。在拿到程序代码后,需要先进行 npm install 才可以获得所有需要的依赖。将项目相关的指令加上 ONBUILD,这样在构建基础镜像的时候,这三行并不会被执行。基础镜像变化后,各个项目都用这个 Dockerfile 重新构建镜像,会继承基础镜像的更新。

FROM node:slim
RUN mkdir /app
WORKDIR /app
ONBUILD COPY ./package.json /app
ONBUILD RUN [ "npm", "install" ]
ONBUILD COPY . /app/
CMD [ "npm", "start" ]

15、LABEL

LABEL 指令用来给镜像以键值对的形式添加一些元数据(metadata)

格式:

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

我们还可以用一些标签来申明镜像的作者、文档地址等,例如:

LABEL org.opencontainers.image.authors="deamon"
LABEL org.opencontainers.image.documentation="https://daemon.gitbooks.io"
或者
LABEL vendor=ACME\ Incorporated \
      com.example.is-beta= \
      com.example.is-production="" \
      com.example.version="0.0.1-beta" \
      com.example.release-date="2015-02-12"

16、.dockerignore 文件

执行 docker build 命令时,当前的工作目录被称为构建上下文。默认情况下,Dockerfile 就位于该路径下。也可以通过 -f 参数来指定 dockerfile ,但 docker 客户端会将当前工作目录下的所有文件发送到 docker 守护进程进行构建。

所以来说,当执行 docker build 进行构建镜像时,当前目录一定要 干净 ,切记不要在家里录下创建一个 Dockerfile 紧接着 docker build 一把梭。

正确做法是为项目建立一个文件夹,把构建镜像时所需要的资源放在这个文件夹下。比如这样:

mkdir project
cd !$
vi Dockerfile
# 编写 Dockerfile

也可以通过 .dockerignore 文件来忽略不需要的文件发送到 docker 守护进程

在docker CLI将上下文发送到docker守护程序之前,它会在上下文的根目录中查找一个名为.dockerignore的文件。如果此文件存在,CLI将修改上下文以排除与其中模式匹配的文件和目录。这有助于避免不必要地将大型或敏感文件和目录发送到守护程序,并可能使用或将它们添加到镜像中。

例如:

# comment
*/temp*
*/*/temp*
temp?
*.md
README-secret.md

三、镜像优化

1、基础镜像选择

使用体积较小的基础镜像,比如 alpine 或者 debian:buster-slim,像 openjdk 可以选用 openjdk:xxx-slim,由于 openjdk 是基于 debian 的基础镜像构建的,所以向 debian 基础镜像一样,后面带个 slim 就是基于 debian:xxx-slim 镜像构建的。

目前 Docker 官方已开始推荐使用 Alpine 替代之前的 Ubuntu 做为基础镜像环境。这样会带来多个好处。包括镜像下载速度加快,镜像安全性提高,主机之间的切换更方便,占用更少磁盘空间等。

REPOSITORY                  TAG                 IMAGE ID            CREATED             SIZE
debian                      buster-slim         e1af56d072b8        4 days ago          69.2MB
alpine                      latest              cc0abc535e36        8 days ago          5.59MB

不过需要注意的是,alpine 的 c 库是 musl libc ,而不是正统的 glibc,另外对于一些依赖 glibc 的大型项目,像 openjdk 、tomcat、rabbitmq 等都不建议使用 alpine 基础镜像,因为 musl libc 可能会导致 JVM 一些奇怪的问题。这也是为什么 tomcat 官方没有给出基础镜像是 alpine 的 Dockerfile 的原因。

制作前端镜像时一定不要使用centos、Ubuntu等系统镜像,我们可以直接使用官方提供的nginx镜像来作为基础镜像,这样我们只需把制作好的web静态文件拷贝一下就可以了。

您可以在hub.docker.com网站上搜索更小的镜像或者说更符合您要求的镜像,然后作为基础镜像来完成构建。

基础镜像优点缺点备注
Alpine占用空间小基于musl libc和busybox官方推荐
busybox占用空间小,极度轻量版工具太少不推荐,组件不全
scratch空镜像,镜像大小约等于执行文件大小没有sh或bash,无法进入容器内进行交互式调试适合go语言

docker镜像常见的参数

参数说明
buster适用与 debian 10
stretch适用于 debian 9
jessie适用于 debian 8
slim表示最小安装包,仅包含需要运行指定容器的特定工具集
Alphine/alpine专门为容器构建的操作系统,比其他的操作系统更小,但是其上会缺少很多软件包并且使用的 glibc 等都是阉割版
bullseye开发版本,处于未稳定状态

2、配置国内软件源

使用默认的软件源安装构建时所需的依赖,对于绝大多数基础镜像来说,可以通过修改软件源的方式更换为国内的软件源镜像站。目前国内稳定可靠的镜像站主要有,华为云、阿里云、腾讯云、163 等。

对于 alpine 基础镜像修改软件源

RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories

debian 基础镜像修改默认软件源

RUN sed -i s@/archive.ubuntu.com/@/mirrors.aliyun.com/@g /etc/apt/sources.list
RUN sed -i "s@http://deb.debian.org@http://mirrors.aliyun.com@g" /etc/apt/sources.list && \
    rm -Rf /var/lib/apt/lists/* && \

Ubuntu 基础镜像修改默认软件源

RUN sed -i 's/archive.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.list && \
    sed -i 's/security.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.list

对于 CentOS ???

你确定要用 230MB 大小的基础镜像?

REPOSITORY                               TAG           IMAGE ID       CREATED          SIZE
centos                                   latest        5d0da3dc9764   16 months ago    231MB

建议这些命令就放在 RUN 指令的第一条,update 以下软件源,之后再 install 相应的依赖。

四、构建镜像

1、命名

原则是见名知意。可使用三段式

镜像仓库地址/类型库/镜像名:版本号

  • registry/runtime/Java:8.1.2
  • registry/runtime/php-fpm-nginx:7.3-1.14
  • registry/cicd/kubctl-helm:1.17-3.0
  • registry/cicd/git-compose-docker:v1
  • registry/applications/demo:git_commit_id

2、基于镜像部署服务

那么现在我们就可以利用以上指令,将我们在宿主机上执行的操作,写在一个Dockerfile中,示例如下:

FROM centos:7
MAINTAINER 759600963@qq.com
#执行下面命令,安装基础环境
RUN yum install -y gcc gcc-c++ make \
    openssl-devel pcre-devel gd-devel \
    iproute net-tools telnet wget curl && \
    yum clean all && \
    rm -rf /var/cache/yum/*
RUN wget http://nginx.org/download/nginx-1.20.1.tar.gz
RUN tar -zxf nginx-1.20.1.tar.gz -C /usr/src
RUN useradd -M -s /sbin/nologin nginx
WORKDIR /usr/src/nginx-1.20.1
RUN ./configure --prefix=/usr/local/nginx --user=nginx --group=nginx && make && make install
RUN ln -s /usr/local/nginx/sbin/* /usr/local/sbin/
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
WORKDIR /usr/local/nginx
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

如上所示,FROM定义了基于centos7镜像来构建,这里使用了yum方式来安装依赖,并通过源码编译的方式进行安装。然后使用WORKDIR定义了进程的工作目录,使用EXPOSE声明了应用要使用的端口,最后使用CMD指定了容器启动时默认命令。

# docker pull centos:7

那么此时我们可以创建一个空目录例如nginx,然后将Dockerfile文件放置于该目录中,然后执行构建命令

# docker build -t web:centos .

::: warning
这里的-t参数是为这个构建的镜像取一个名字,设置一个标签,在自动生成镜像的命令指定镜像后,一定不要忘记写新生成镜像的存放路径,也就是空格后的 一 个“.”代表当前路径,否则会报错。
:::

当我们执行build后,docker会将build文件夹连同Dockerfile一起所有的文件(如果有其他文件)提交至docker engine来处理。docker Engine将逐行解析Dockerfile文件,并执行相关操作。Dockerfile的解析是由上而下的顺序进行。

注意您放置Dockerfile的目录最好是一个空目录,或者只放置与本次构建相关的文件即可,因为在执行build时会将Dockerfile以及Dockerfile路径下的所有文件提交至docker Engine处理,因此您不应该在Dockerfile目录中放置一些无关的文件,这是无意义的并且这将会严重拖慢整个构建过程。

使用新的镜像运行容器

# docker run -itd --name testweb -p 80:80 web:centos
73277ceaa7a9a72d7a729029360413cfc0364effb4062b3df25aa960c67b894a

3、使用Makefile操作Dockerfile

IMAGE_BASE = registry/runtime
IMAGE_NAME = php-fpm
IMAGE_VERSION = 7.3
IMAGE_TAGVERSION = $(GIT_COMMIT)

all: build tag push

build:
  docker build --rm -f Dockerfile -t ${IMAGE_BASE}/${IMAGE_NAME}:${IMAGE_VERSION} .

tag:
  docker tag ${IMAGE_BASE}/${IMAGE_NAME}:${IMAGE_VERSION} ${IMAGE_BASE}/${IMAGE_NAME}:${IMAGE_TAGVERSION}

push:
  docker push ${IMAGE_BASE}/${IMAGE_NAME}:${IMAGE_TAGVERSION}

# 构建并推送
make 
# 仅构建
make build 
# 仅打tag
make tag
# 仅推送
make push

makefile中的命令必须以tab作为开头(分隔符),不能用扩展的tab即用空格代替的tab。(如果是vim编辑的话,执行 set noexpandtab)。否则会报如下错误:Makefile:10: *** multiple target patterns. Stop.

五、多阶段构建镜像

在编写Dockerfile构建docker镜像时,常遇到以下问题:

  1. RUN命令会让镜像新增layer,导致镜像变大,虽然通过&&连接多个命令能缓解此问题,但如果命令之间用到docker指令例如COPY、WORKDIR等,依然会导致多个layer;
  2. 有些工具在构建过程中会用到,但是最终的镜像是不需要的(例如用npm编译构建前端工程),这要求Dockerfile的编写者花更多精力来清理这些工具,清理的过程又可能导致新的layer;

为了解决上述问题,从17.05版本开始Docker在构建镜像时增加了新特性:多阶段构建(multi-stage builds),将构建过程分为多个阶段,每个阶段都可以指定一个基础镜像,这样在一个Dockerfile就能将多个镜像的特性同时用到

我们可以在一个Dockerfile中使用多个FROM指令并将一次构建分成多个阶段来完成

# build stage
FROM node:14.18.2-stretch-slim AS build-env
# 工作目录
WORKDIR /app
# 将git仓库下所有文件拷贝到工作目录
COPY . .
RUN npm install
RUN npm audit fix
RUN npm run build:production

# production stage
# 生产环境基础nginx镜像(上面的镜像已经打包为了静态文件)
FROM nginx:alpine
ADD  prod.conf /etc/nginx/conf.d/
# 使用--from把上面产生的静态文件复制到nginx的运行目录
COPY --from=build-env /app/dist /usr/share/nginx/html
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
	echo 'Asia/Shanghai' >/etc/timezone
# nginx容器内部暴露的端口
EXPOSE 80
# 运行的命令
CMD ["/bin/sh", "-c", "nginx -g \"daemon off;\"" ]

在第一个FROM指令后使用as build-env 意思是为该阶段取一个标记名为build-env,然后在第二个FROM之后使用了COPY --from=build-env,意思是从上一个名为builder的阶段中拷贝文件至本阶段中。

六、Dockerfile 传参

使用 ARG 和 build-arg 传入动态变量:

# base image 
FROM centos:7 
# MAINTAINER dot # deprecated 
 
LABEL maintainer="dot" version="demo" 
LABEL multiple="true" 
 
ARG USERNAME 
ARG DIR="defaultValue" 

RUN useradd -m $USERNAME -u 1001 && mkdir $DIR 
 
# docker build --build-arg USERNAME="test_arg" -t test:arg . 
# docker run -ti --rm test:arg bash 
# ls 
bin             dev  home  lib64       media  opt   root  selinux  sys  usr 
defaultValue  etc  lib   lost+found  mnt    proc  sbin  srv      tmp  var
# tail -1 /etc/passwd 
test_arg:x:1001:1001::/home/test_arg:/bin/bash

七、Dockerfile 优化

1、尽量不使用root用户

在做基础运行时镜像时,创建运行时普通用户和用户组,并做工作区与权限限制,启动服务时尽量使用普通用户。

gosu

FROM alpine:3.11.5
RUN sed -i "s/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g" /etc/apk/repositories \
    && apk add --no-cache gosu

2、移除所有缓存等不必要信息

  • 删除解压后的源压缩包(参考第二章第二节)
  • 清理包管理器下载安装软件时的缓存
    • 使用Alipine镜像中APK命令安装包时记得加上--no-cache
    • 使用Ubuntu镜像中的APT命令安装软件后记得 rm -rf /var/lib/apt/lists/*

3、使用合理的ENTRYPOINT脚本

示例:

#!/bin/bash
set -e

if [ "$1" = 'postgres' ]; then
    chown -R postgres "$PGDATA"

    if [ -z "$(ls -A "$PGDATA")" ]; then
        gosu postgres initdb
    fi

    exec gosu postgres "$@"
fi

exec "$@"

4、设置时区

由于绝大多数基础镜像都是默认采用 UTC 的时区,与北京时间相差 8 个小时,这将会导致容器内的时间与北京时间不一致,因而会对一些应用造成一些影响,还会影响容器内日志和监控的数据。

因此对于东八区的用户,最好在构建镜像的时候设定一下容器内的时区,以免以后因为时区遇到一些 bug。

可以通过环境变量设置容器内的时区。在启动的时候可以通过设置环境变量 -e TZ=Asia/Shanghai 来设定容器内的时区。

1、基于 Alpine 镜像

FROM alpine:3.15
ENV TZ=Asia/Shanghai
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
    && apk add --no-cache tzdata \
    && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime  \
    && echo "Asia/Shanghai" > /etc/timezone

2、基于 Centos7 镜像

FROM centos:7
#定义时区参数
ENV TZ=Asia/Shanghai
#设置时区
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo '$TZ' > /etc/timezone

3、基于 Debian 镜像

# 由于 Debian 镜像中已经包含了tzdata,所以只需添加环境变量TZ即可。
FROM debian:latest

ENV TZ=Asia/Shanghai

4、基于 Ubuntu 镜像

FROM ubuntu:bionic

ENV TZ=Asia/Shanghai
RUN echo "${TZ}" > /etc/timezone
	&& ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime
	&& apt update
	&& apt install -y tzdata
	&& rm -rf /var/lib/apt/lists/*

5、设置系统语言

1、基于 Alpine 镜像

FROM alpine:3.15
ENV LANG=en_US.UTF-8 \
    LANGUAGE=en_US.UTF-8
    
RUN apk --no-cache add ca-certificates wget \ 
    && wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub \ 
    && wget -q https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.33-r0/glibc-2.33-r0.apk \ 
    && wget -q https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.33-r0/glibc-bin-2.33-r0.apk \
    && wget -q https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.33-r0/glibc-i18n-2.33-r0.apk \
    && apk add glibc-bin-2.33-r0.apk glibc-i18n-2.33-r0.apk glibc-2.33-r0.apk \
    && rm -rf /usr/lib/jvm glibc-2.29-r0.apk glibc-bin-2.29-r0.apk  glibc-i18n-2.29-r0.apk \
    && /usr/glibc-compat/bin/localedef --force --inputfile POSIX --charmap UTF-8 "$LANG" || true \
    && echo "export LANG=$LANG" > /etc/profile.d/locale.sh \
    && apk del glibc-i18n

2、基于 Centos7 镜像

FROM centos:7
#安装必要应用
RUN yum -y install kde-l10n-Chinese glibc-common
#设置编码
RUN localedef -c -f UTF-8 -i zh_CN zh_CN.utf8
#设置环境变量
ENV LC_ALL zh_CN.utf8

3、基于 Debian 镜像

# 由于 Debian 镜像中已经包含了tzdata,所以只需添加环境变量TZ即可。
FROM debian:latest

ENV LANG C.UTF-8
RUN apt-get update; \
	apt-get install -y --no-install-recommends fontconfig;

4、基于 Ubuntu 镜像

FROM ubuntu:bionic

ENV LANG C.UTF-8

6、使用Label标注作者、软件版本等元信息

FROM alpine:3.11.5
LABEL Author=Curiouser \
      Mail=****@163.com \
      PHP=7.3 \
      Tools=“git、vim、curl” \
      Update="添加用户组"

7、指定工作区

WORKDIR /var/wwww

8、RUN指令显示优化

RUN set -eux ; \
    ls -al

9、使用 URL 添加源码

如果不采用分阶段构建,对于一些需要在容器内进行编译的项目,最好通过 git 或者 wegt 的方式将源码打入到镜像内,而非采用 ADD 或者 COPY ,因为源码编译完成之后,源码就不需要可以删掉了,而通过 ADD 或者 COPY 添加进去的源码已经用在下一层镜像中了,是删不掉滴啦。

也就是说 git & wget source 然后 build,最后 rm -rf source/ 这三部放在一条 RUN 指令中,这样就能避免源码添加到镜像中而增大镜像体积啦。

10、使用虚拟编译环境

对于只在编译过程中使用到的依赖,我们可以将这些依赖安装在虚拟环境中,编译完成之后可以一并删除这些依赖,比如 alpine 中可以使用 apk add --no-cache --virtual .build-deps,后面加上需要安装的相关依赖。

apk add --no-cache --virtual .build-deps gcc libc-dev make perl-dev openssl-dev pcre-dev zlib-dev git

构建完成之后可以使用 apk del .build-deps 命令,一并将这些编译依赖全部删除。

需要注意的是,.build-deps 后面接的是编译时以来的软件包,并不是所有的编译依赖都可以删除,不要把运行时的依赖包接在后面,最好单独 add 一下。

11、最小化层数

docker 在 1.10 以后,只有 RUN、COPY 和 ADD 指令会创建层,其他指令会创建临时的中间镜像,但是不会直接增加构建的镜像大小了。

前文提到了建议使用 git 或者 wget 的方式来将文件打入到镜像当中,但如果我们必须要使用 COPY 或者 ADD 指令呢?

还是拿 FastDFS 为例:

# centos 7
FROM centos:7
# 添加配置文件
# add profiles
ADD conf/client.conf /etc/fdfs/
ADD conf/http.conf /etc/fdfs/
ADD conf/mime.types /etc/fdfs/
ADD conf/storage.conf /etc/fdfs/
ADD conf/tracker.conf /etc/fdfs/
ADD fastdfs.sh /home
ADD conf/nginx.conf /etc/fdfs/
ADD conf/mod_fastdfs.conf /etc/fdfs

# 添加源文件
# add source code
ADD source/libfastcommon.tar.gz /usr/local/src/
ADD source/fastdfs.tar.gz /usr/local/src/
ADD source/fastdfs-nginx-module.tar.gz /usr/local/src/
ADD source/nginx-1.15.4.tar.gz /usr/local/src/

多个文件需要添加到容器中不同的路径,每个文件使用一条 ADD 指令的话就会增加一层镜像,这样戏曲就多了 12 层镜像 。

其实大可不必,我们可以将这些文件全部打包为一个文件为 src.tar.gz 然后通过 ADD 的方式把文件添加到当中去,然后在 RUN 指令后使用 mv 命令把文件移动到指定的位置。这样仅仅一条 ADD 和 RUN 指令取代掉了 12 个 ADD 指令。

FROM alpine:3.10
COPY src.tar.gz /usr/local/src.tar.gz
RUN set -xe \
    && apk add --no-cache --virtual .build-deps gcc libc-dev make perl-dev openssl-dev pcre-dev zlib-dev tzdata \
    && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
    && tar -xvf /usr/local/src.tar.gz -C /usr/local \
    && mv /usr/local/src/conf/fastdfs.sh /home/fastdfs/ \
    && mv /usr/local/src/conf/* /etc/fdfs \
    && chmod +x /home/fastdfs/fastdfs.sh \
    && rm -rf /usr/local/src/* /var/cache/apk/* /tmp/* /var/tmp/* $HOME/.cache
VOLUME /var/fdfs

其他最小化层数无非就是把构建项目的整个步骤弄成一条 RUN 指令,不过多条命令合并可以使用 && 或者 ; 这两者都可以,不过据我在 docker hub 上的所见所闻,使用 ; 的居多,尤其是官方的 Dockerfile。

0

评论区