Docker构建镜像
# 一、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>",... "<目标路径>"]
2
注意 [--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 命令。
2
Exec 格式:
RUN ["可执行文件", "参数1", "参数2"]
# 例如:
# RUN ["./test.php", "dev", "offline"] 等价于 RUN ./test.php dev offline
2
3
注意在构建过程中 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
2
3
4
5
如上,以 && 符号连接命令,这样执行后,只会创建 1 层镜像
# 5、CMD
类似于 RUN 指令,用于运行程序,但二者运行的时间点不同:
CMD 在 docker run 时运行。
RUN 是在 docker build 过程中执行。
作用:为启动的容器指定默认要运行的程序,程序运行结束,容器也就结束。CMD 指令指定的程序可被 docker run 命令行参数中指定要运行的程序所覆盖。
注意
如果 Dockerfile 中如果存在多个 CMD 指令,仅最后一个生效。
格式:
CMD <shell 命令>
CMD ["<可执行文件或命令>","<param1>","<param2>",...]
CMD ["<param1>","<param2>",...] # 该写法是为 ENTRYPOINT 指令指定的程序提供默认参数
2
3
推荐使用第二种格式,执行过程比较明确。第一种格式实际上在运行的过程中也会自动转换成第二种格式运行,并且默认可执行文件是 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"] # 变参
2
3
不传参运行
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
2
3
4
5
6
7
8
9
10
11
12
该镜像的 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 "$@"
2
3
4
5
6
# 7、ENV
设置环境变量,定义了环境变量,那么在后续的指令中,就可以使用这个环境变量。当然在您使用该镜像运行容器时,ENV 所定义的变量仍然对容器有效,例如 MySQL 镜像的 Dockerfile 中就包含 root 用户的初始密码,使用 ENV 来指定。
格式:
ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>...
2
以下示例设置 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"
2
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
2
3
# 8、ARG
构建参数,与 ENV 作用一至。不过作用域不一样。ARG 设置的环境变量仅对 Dockerfile 内有效,也就是说只有 docker build 的过程中有效,构建好的镜像内不存在此环境变量。
构建命令 docker build 中可以用 --build-arg <参数名>=< 值 > 来覆盖。
格式
ARG <参数名>[=<默认值>]
# 9、VOLUME
定义匿名数据卷。在启动容器时忘记挂载数据卷,会自动挂载到匿名卷。
作用:
避免重要的数据,因容器重启而丢失,这是非常致命的。
避免容器不断变大。
格式:
VOLUME ["<路径1>", "<路径2>"...]
VOLUME <路径
2
在启动容器 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 次。
2
3
4
5
6
7
和 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 \
CMD curl -fs http://localhost/ || exit 1
2
3
4
当然您可以在 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
2
3
4
5
6
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" ]
2
3
4
5
6
7
# 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"
2
3
4
5
6
7
8
# 16、.dockerignore 文件
执行 docker build
命令时,当前的工作目录被称为构建上下文。默认情况下,Dockerfile 就位于该路径下。也可以通过 -f
参数来指定 dockerfile ,但 docker 客户端会将当前工作目录下的所有文件发送到 docker 守护进程进行构建。
所以来说,当执行 docker build 进行构建镜像时,当前目录一定要 干净
,切记不要在家里录下创建一个 Dockerfile 紧接着 docker build
一把梭。
正确做法是为项目建立一个文件夹,把构建镜像时所需要的资源放在这个文件夹下。比如这样:
mkdir project
cd !$
vi Dockerfile
# 编写 Dockerfile
2
3
4
也可以通过 .dockerignore
文件来忽略不需要的文件发送到 docker 守护进程
在 docker CLI 将上下文发送到 docker 守护程序之前,它会在上下文的根目录中查找一个名为.dockerignore 的文件。如果此文件存在,CLI 将修改上下文以排除与其中模式匹配的文件和目录。这有助于避免不必要地将大型或敏感文件和目录发送到守护程序,并可能使用或将它们添加到镜像中。
例如:
# comment
*/temp*
*/*/temp*
temp?
*.md
README-secret.md
2
3
4
5
6
# 三、镜像优化
# 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
2
3
不过需要注意的是,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/* && \
2
3
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
2
对于 CentOS ???
你确定要用 230MB 大小的基础镜像?
REPOSITORY TAG IMAGE ID CREATED SIZE
centos latest 5d0da3dc9764 16 months ago 231MB
2
建议这些命令就放在 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;"]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
如上所示,FROM定义了基于centos7镜像来构建,这里使用了yum方式来安装依赖,并通过源码编译的方式进行安装。然后使用WORKDIR定义了进程的工作目录,使用EXPOSE声明了应用要使用的端口,最后使用CMD指定了容器启动时默认命令。
# docker pull centos:7
那么此时我们可以创建一个空目录例如nginx,然后将Dockerfile文件放置于该目录中,然后执行构建命令
# docker build -t web:centos .
注意
这里的-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
2
# 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 /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;\"" ]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
在第一个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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 七、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
3
# 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 "$@"
2
3
4
5
6
7
8
9
10
11
12
13
14
# 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
3
4
5
6
# 2、基于 Centos7 镜像
FROM centos:7
#定义时区参数
ENV TZ=Asia/Shanghai
#设置时区
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo '$TZ' > /etc/timezone
2
3
4
5
# 3、基于 Debian 镜像
# 由于 Debian 镜像中已经包含了tzdata,所以只需添加环境变量TZ即可。
FROM debian:latest
ENV TZ=Asia/Shanghai
2
3
4
# 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/*
2
3
4
5
6
7
8
# 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
3
4
5
6
7
8
9
10
11
12
13
14
# 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
2
3
4
5
6
7
# 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;
2
3
4
5
6
# 4、基于 Ubuntu 镜像
FROM ubuntu:bionic
ENV LANG C.UTF-8
2
3
# 6、使用Label标注作者、软件版本等元信息
FROM alpine:3.11.5
LABEL Author=Curiouser \
Mail=****@163.com \
PHP=7.3 \
Tools=“git、vim、curl” \
Update="添加用户组"
2
3
4
5
6
# 7、指定工作区
WORKDIR /var/wwww
# 8、RUN指令显示优化
RUN set -eux ; \
ls -al
2
# 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/
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
多个文件需要添加到容器中不同的路径,每个文件使用一条 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
2
3
4
5
6
7
8
9
10
11
其他最小化层数无非就是把构建项目的整个步骤弄成一条 RUN 指令,不过多条命令合并可以使用 &&
或者 ;
这两者都可以,不过据我在 docker hub 上的所见所闻,使用 ;
的居多,尤其是官方的 Dockerfile。