一、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指令来定义构建过程中所要执行的操作,这里列举几个常用的指令:
常用指令 | 说明 |
---|---|
FROM | FROM指令是定义本阶段构建所要使用的基础镜像。 |
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镜像时,常遇到以下问题:
- RUN命令会让镜像新增layer,导致镜像变大,虽然通过&&连接多个命令能缓解此问题,但如果命令之间用到docker指令例如COPY、WORKDIR等,依然会导致多个layer;
- 有些工具在构建过程中会用到,但是最终的镜像是不需要的(例如用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/*
- 使用Alipine镜像中APK命令安装包时记得加上
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。
评论区