

帅哥(靓仔)、美女,点个关注后续不迷路!
本章目录

0x01 Dockerfile 镜像构建浅析与实践
1.基本结构
2.常用指令浅析
1)FROM - 基础镜像信息
2)LABEL - 标签信息
3)MAINTAINER - 维护者信息
4)RUN - 镜像操作命令:
5)CMD- 容器启动时执行指令
6)EXPOSE - 端口映射指令
7)ENV - 修改环境变量指令
8)ARG - 构建参数
9)ADD - 添加指定目录文件到镜像指令
10)COPY - 复制指定文件或者目录到容器中
11)ENTRYPOINT - 配置容器启动进入后的执行命令-应用运行前的准备工作
13)VOLUME- 创建本地主机或其他主机挂载点-定义匿名卷
13)USER- 指定容器运行时名用户名或者UID
14) WORKDIR - 配置工作目录
15)ONBUILD - 为他人做嫁衣裳
16)STOPSIGNAL - 指定所创建镜像启动的容器接收退出的信号值:
17)SHELL - 指定其他命令执行时默认使用shell的类型
18)HEALTHCHECK - 健康检查
3.补充知识
4.基础实战
5.Dockerfile 编写构建示例
示例0.自定义 Debian 镜像安装中文编码支持
示例1.Docker - Webp Server Go
示例2.进行dockerfile编写并生成自定义hexo博客环境的镜像
示例3.Dockerfile多阶段构建实践

描述:Dockerfile是一个文本格式的配置文件,其内包含了一条条的指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。 用户可以使用Dockerfile快速创建自定义的镜像;通过它所支持的内部指令,以及使用它创建镜像的基本过程,Docker拥有"一点修改代替大量更新"的灵活之处;
文本化的镜像生成操作让其方便版本管理和自动化部署;
每条命令对应镜像的一层,细化操作后保证其可增量更新,复用镜像块减小镜像体积(后面您会体验到);
总结为一点就是将每一层都写入到一个脚本之中。

温馨提示: 一个镜像构建时不能超过 127 层,我们需要保证了稳定的变化的命令至于上层保证了每层打包出来的 Layer 能够尽可能的复用,而不会徒增镜像的大小,影响后续拉取镜像的速度;
Dockerfile分为四个部分:
基础镜像信息:FROM \<image\> 或者 FROM \<image\>:\<tag\>
维护者信息: MAINTAINER (建议使用LABEL标签进行替代,先已丢弃)
镜像标签信息: LABEL
镜像操作指令: RUN
容器启动时执行指令: CMD
例如:在/opt/目录中利用dockerfile创建一个基于ubuntu的nginx容器与vnc服务;
# Usage: docker build -t hub.weiyigeek.top/first_tag -f /opt/dockerfile .
#1.第一行必须指定基于的基础镜像
FROM ubuntu
#2.前期操作处理
MAINTAINER weiyigeek weiyigeek@qq.com #会影响实际仓库的拉取
LABEL version=&quot;1.0&quot; #容器元信息,帮助信息,Metadata,类似于代码注释
LABEL maintainer=&quot;weiyigeek@163.com&quot;
WORKDIR /opt/demo #切换工作空间(建议使用绝对路径)
ADD /opt/package /data/ # 把本地文件目录添加到镜像中
ENV MYSQL_VERSION 5.6 # 设置一个mysql常量 环境变量,尽可能使用ENV增加可维护性
#3.镜像的操作命令
#RUN指令时对镜像执行跟随的命令,每运行一条RUN指令,镜像添加新的一层并提交;
RUN echo &quot;deb http://archive.ubuntu.com/ubuntu/ raring main uiverse&quot; &gt;&gt; /etc/apt/source.list
RUN apt-het update &amp;&amp; apt-get install -y nginx \
net-tools mysql-server=&quot;${MYSQL_VERSION}&quot;
RUN echo &quot;\ndaemon off;&quot; &gt;&gt; /etc/nginx/nginx.conf
#4.镜像启动时候执行的命令
CMD /usr/sbin/nginx

在编写完成Dockerfile之后可以,该命令读取指定路径下(包括子目录)的dockerfile(实际上是构建上下文Context),并将该路径下的内容发送给Docker服务端由它创建镜像;
因此一般建议放置Dockerfile的目录为空另外)会让Docker忽略路径下的目录和文件;
docker 镜像生成常用命令:
docker build [选项]
- t :指定标签信息
--build-arg &lt;参数名&gt;=&lt;值&gt;
# 构建镜像的几种方式:
#1) 指定的Dockfile所在路径为/tmp/docker_builder
$docker build -t [TAG/version] /tmp/docker_builder
#2) 支持从 URL 构建
$docker build https://github.com/twang2218/gitlab-ce-zh.git#:11.1
#3) 用给定的 tar 压缩包构建
$docker build http://server/context.tar.gz
#4) 从标准输入中读取 Dockerfile 进行构建
$docker build - &lt; Dockerfile
$cat Dockerfile | docker build -
#5) 从标准输入中读取上下文压缩包进行构建
$docker build - &lt; context.tar.gz #标准输入的文件格式还可以是 gzip、bzip2 以及 xz
Dockerfile指令参数(Instruction arguments)如下,可以参考Docker官方有关dockerfile指令介绍的详细文档。
https://docs.docker.com/engine/reference/builder
描述:尽可能使用官方镜像或者信任的镜像作为你构建镜像的基础设施,推荐使用镜像,因为它被严格控制并保持最小尺寸(目前小于 6 MB),但它仍然是一个完整的发行版。
# 基础语法
FROM &lt;image&gt; 或者 FROM &lt;image&gt;:&lt;tag&gt;
# 使用案例
FROM alpine
FROM golang:1.9-alpine as builder # 注意:多阶段构建使用 as 来为某一阶段命名
描述:可以给镜像添加标签来帮助组织镜像、记录许可信息、辅助自动化构建等
注意:如果你的字符串包含空格,那么它必须被引用或者空格必须被转义。如果您的字符串包含内部引号字符("),则也可以将其转义。
# 基础语法:每个标签一行,由 LABEL 开头加上一个或多个标签对。
LABEL key=&lt;value&gt;
# 使用案例
# Set one or more individual labels `#`开头的行是注释内容。
LABEL maintainer=&quot;WeiyiGeek&quot;
LABEL vendor=&quot;ACME Incorporated&quot;
LABEL version=1.1
LABEL com.example.version=&quot;0.0.1-beta&quot;
LABEL com.example.release-date=&quot;2015-02-12&quot; Tips(1): 在 1.10 之前,建议将所有标签合并为一条指令,以防止创建额外的层,但是现在这个不再是必须的了,以上内容也可以写成下面这样:
# Set multiple labels at once, using line-continuation characters to break long lines
LABEL vendor=ACME\ Incorporated \
version=1.1 \
com.example.version=&quot;0.0.1-beta&quot; \
com.example.release-date=&quot;2015-02-12&quot;
# 基础语法
MAINTAINER key=&lt;value&gt;
# 使用案例
MAINTAINER WeiyiGeek master@weiyigeek.top
描述:为了保持 Dockerfile 文件的可读性,以及可维护性,建议将长的或复杂的指令用反斜杠分割成多行。
RUN 指令最常见的用法是安装包用的,因为该指令会安装包,所以有几个问题需要注意。
不要使用 RUN apt-get upgrade 或 dist-upgrade , 如果基础镜像中的某个包过时了,你应该联系它的维护者。
如果你确定某个特定的包比如 foo 需要升级,使用 apt-get install -y foo 就行,该指令会自动升级 foo 包。
最好将 RUN 多条语句汇集成为一条 apt-get update 和 apt-get install 以及 rm -rf /var/lib/apt/lists* 组合成一条 RUN 声明
# 基础语法
RUN &lt;COMMAND&gt; 或者 RUN [&quot;executable&quot;,&quot;param1&quot;,&quot;param2&quot;]
# 使用案例
如:RUN [&quot;/bin/bash&quot;,&quot;-c&quot;,&quot;echo Hello&quot;],当命令较长时可以使用\来换行;
RUN apt-get update;\
apt-get install -y nginx ;\
rm -rf /var/lib/apt/lists/*
#展示了所有关于 apt-get 的建议
#其中 s3cmd 指令指定了一个版本号`1.1.*`。如果之前的镜像使用的是更旧的版本,指定新的版本会导致 apt-get udpate 缓存失效并确保安装的是新版本。
RUN apt-get update &amp;&amp; apt-get install -y \
aufs-tools \
automake \
build-essential \
curl \
dpkg-sig \
libcap-dev \
libsqlite3-dev \
mercurial \
reprepro \
ruby1.9.1 \
ruby1.9.1-dev \
s3cmd=1.1.* \
&amp;&amp; rm -rf /var/lib/apt/lists/*
Tips(1):为何建议将RUN写在一行目录之中?
1.比如假设你有一个 Dockerfile 文件:将 apt-get update 放在一条单独的 RUN 声明中会导致缓存问题以及后续的 apt-get install 失败。
FROM ubuntu:18.04
RUN apt-get update
RUN apt-get install -y curl
2.构建镜像后所有的层都在 Docker 的缓存中。假设你后来又修改了其中的 apt-get install 添加了一个包。
3.Docker 发现修改后的 RUN apt-get update 指令和之前的完全一样。所以apt-get update不会执行,而是使用之前的缓存镜像。因为 apt-get update 没有运行后面的 apt-get install 可能安装的是过时的 curl 和 nginx 版本。
解决办法:采用cache busting(缓存破坏)的方式进行:
#方式1.使用`RUN apt-get update &amp;&amp; apt-get install -y` 可以确保你的`Dockerfiles`每次安装的都是包的最新的版本,而且这个过程不需要进一步的编码或额外干预。
#方式2.固定版本会迫使构建过程检索特定的版本来达到 `cache-busting`目的,而不管缓存中有什么,该项技术也可以减少因所需包中未预料到的变化而导致的失败。
RUN apt-get update &amp;&amp; apt-get install -y \
package-bar \
package-baz \
package-foo=1.3.* Tips(2): 清理掉 apt 缓存 可以减小镜像大小,因为 RUN 指令的开头为 包缓存总是会在 之前刷新。
注意:官方的 Debian 和 Ubuntu 镜像会自动运行 apt-get clean,所以不需要显式的调用 apt-get clean。
描述:指令用于执行目标镜像中包含的软件和任何参数, 实际上为容器提供一个默认的执行命令。
在Dockerfile中CMD被用来为ENTRYPOINT指令提供参数,则CMD和ENTRYPOINT指令都应该使用exec格式
当基于镜像的容器运行时将会自动执行CMD指令, 并且如果在docker run命令中指定了参数,这些参数将会覆盖在CMD指令中设置的参数。
多数情况下CMD 都需要一个交互式的 shell (bash, Python, perl 等),例如 CMD ["perl", "-de0"],或者 CMD ["PHP", "-a"]。使用这种形式意味着,当你执行类似时,你会进入一个准备好的 shell 中。
#基础语法
#CMD指令有如下三种格式
#exec格式
CMD [&quot;executable&quot;,&quot;param1&quot;,&quot;param2&quot;]
#为ENTRYPOINT提供参数
CMD [&quot;param1&quot;,&quot;param2&quot;]
#shell格式,在/bin/bash中执行提供给需要交互的应用
CMD command param1 param2
# 基础示例
#(1)如果创建镜像的目的是为了部署某个服务(比如 Apache)
CMD [&quot;apache2&quot;, &quot;-DFOREGROUND&quot;]
#(2)如果使用 shell 格式的话,实际的命令会被包装为 sh -c 的参数的形式进行执行。比如:
CMD echo $HOME #在实际执行中,会将其变更为: CMD [ &quot;sh&quot;, &quot;-c&quot;, &quot;echo $HOME&quot; ] 注意事项:
(1)如果用户启动容器指定了运行命令则会覆盖掉CMD指定命令,注意每个Dockerfile只能有一条CMD命令,如果指定了多条命令只有最后一条执行;
(2)CMD 在极少的情况下才会以 CMD ["param", "param"] 的形式与ENTRYPOINT协同使用,除非你和你的镜像使用者都对 ENTRYPOINT 的工作方式十分熟悉。
描述:指令用于指定容器将要监听的端口即默认向外部的暴露的服务端口。因此你应该为你的应用程序使用常见的端口。 对于外部访问,用户可以在执行 docker run 时使用一个标志来指示如何将指定的端口映射到所选择的端口。
#基础语法
EXPOSE &lt;port&gt; [&lt;port&gt;...]
#基础示例
# (1) 告诉dokcer服务端容器暴露的端口号,供互联系统使用; 也就是 docker run -P 时会自动随机映射 EXPOSE 的端口。
EXPOSE 22 80 8443
# (2) 例如提供 Apache web 服务的镜像应该使用 80,而提供 MongoDB 服务的镜像使用 27017
EXPOSE 80 27017
# (3) 指定一个范围
EXPOSE 30000-40000
描述:为了方便新程序运行,你可以使用来为容器中安装的程序更新 PATH 环境变量。例如使用来确保能正确运行。类似于程序中的常量,该方法可以让你只需改变 ENV 指令来自动的改变容器中的软件版本。
# 基础语法
ENV &lt;key&gt; &lt;value&gt; #会被后续的RUN指令使用,并在容器运行时保持;
# 基础示例
#方式1: 如RUN还是运行时的应用,都可以直接使用这里定义的环境变量。
ENV PG_MAJOR 9.3
ENV PATH /usr/local/postgres-\$PG_MAJOR/bin:$PATH
RUN curl -SL http://example.com/postgre-$PG_MAJOR.tar.xz &amp;&amp; ENV PATH /usr/local/postgres-$PG_MAJOR/bin:$PATH
#方式2: 将所有的环境变量定义在一条ENV语句中
ENV VERSION=1.0 DEBUG=on \
NAME=&quot;Happy Feet&quot; 描述:构建参数和 ENV 的效果一样都是设置环境变量不同点;
ARG &lt;参数名&gt; [=&lt;默认值&gt;]
# 调用方式与Shell中一致
${IMG_PATH}
描述:该命令将复制指定的源文件到镜像内中的目标文件,其中可以是在Dockerfile所在的目录的一个相对路径(文件或者目录)/URL/tar文件();
# 基础语法
ADD &lt;src&gt; &lt;dest&gt;
# 基础示例
# (1)最佳用例是将本地tar文件自动提取到镜像中
ADD rootfs.tar.xz
# (2)下载后的文件权限自动设置为 600 这个自动解压缩的功能非常有用;
ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz
ADD --chown=55:mygroup files* /mydir/
ADD $PWD /opt/blog/ #复制到目录之中
注意事项:
(1)为了让镜像尽量小,最好不要使用 ADD 指令从远程 URL 获取包,而是使用 curl 和 wget。
#你可以在文件提取完之后删掉不再需要的文件来避免在镜像中额外添加一层
#比如尽量避免下面的用法:
ADD http://example.com/big.tar.xz /usr/src/things/
RUN tar -xJf /usr/src/things/big.tar.xz -C /usr/src/things
RUN make -C /usr/src/things all
#而是应该使用下面这种方法:由于使用的管道操作,所以没有中间文件需要删除。
RUN mkdir -p /usr/src/things \
&amp;&amp; curl -SL http://example.com/big.tar.xz \
| tar -xJC /usr/src/things \
&amp;&amp; make -C /usr/src/things all
描述:COPY只支持简单将本地文件拷贝到镜像中它比 ADD 更透明,所以和功能类似但一般优先使用 COPY ;
COPY 指令将从构建上下文目录中 <源路径> 的文件/目录复制到新的一层的镜像内的 <目标路径> 位置。
当目标路径不存在时候自动创建,当使用本地目录作为源目录时候推荐使用COPY:
# 基础语法
COPY &lt;src&gt; &lt;dest&gt;
# 选项
--from=多阶段构建的镜像名称 #FROM Alpine AS [名称]
# 常规方式
COPY package.json /usr/src/app/
#&lt;源路径&gt; 可以是多个,甚至可以是通配符
COPY hom* /mydir/
COPY hom?.txt /mydir/
# 选项来改变文件的所属用户及所属组。
COPY --chown=55:mygroup files* /mydir/
COPY --from=0 /go/src/github.com/go/helloworld/app . #多阶段构建,从上一阶段的镜像中复制文件
COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf #复制任意镜像中的文件(但需要指定镜像名称 Tips:
对于其他不需要 ADD 的自动提取功能的文件或目录,你应该使用 COPY。
采用CPOY --from 从上一个构建阶段拷贝文件时,使用的路径是相对于上一阶段的根目录的,此时建议复制成果时候采用绝对路径;
描述: 该指令是设置镜像的主命令,其作用是允许将镜像当成命令本身来运行(使用终端提供默认选项)
# 基础示例
#exec格式推荐的格式
ENTRYPOINT [&quot;executable&quot;,&quot;param1&quot;,&quot;param2&quot;]
#shell格式:使用ENTRYPONT指令并不可被docker run提供的参数覆盖(与CMD不同之处)
ENTRYPOINT command param1 param2 #shell中执行
# 基础示例
# 1.例如下面的示例镜像提供了命令行工具 s3cmd:
ENTRYPOINT [&quot;s3cmd&quot;]
CMD [&quot;--help&quot;]
#现在直接运行该镜像创建的容器会显示命令帮助: $ docker run s3cmd
#或者提供正确的参数来执行某个命令:$ docker run s3cmd ls s3://mybucket
# 2.使用ENTRYPOINT 的exec形式来设置相对稳定的默认命令和参数,然后使用任何形式的CMD指令来设置可能发生变化的参数。
FROM alpine
ENTRYPOINT [&quot;top&quot;, &quot;-b&quot;]
CMD [&quot;-c&quot;]
#当运行容器是,可以看到只有一个top进程在运行:
$ docker run -it --rm --name alpine:test top -H
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1 root 20 0 19744 2336 2080 R 0.0 0.1 0:00.04 top
$ docker exec -it alpine:test ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 2.6 0.1 19752 2352 ? Ss+ 08:24 0:00 top -b -H
root 7 0.0 0.1 15572 2164 ? R+ 08:25 0:00 ps aux
# 3.ENTRYPOINT指令的shell格式
#通过为ENTRYPOINT指定文本格式的参数,此参数将在`/bin /sh -c` 中进行执行
#该形式将使用shell处理而不是shell环境变量,并且将忽略任何的CMD或docker run运行命令行参数。
FROM alpine
ENTRYPOINT exec top -b 补充说明:
1.ENTRYPOINT 指令也可以结合一个辅助脚本使用,和前面命令行风格类似,即使启动工具需要不止一个步骤。
注意:该脚本使用了 Bash 的内置命令 exec,所以最后运行的进程就是容器的 PID 为 1 的进程。这样,进程就可以接收到任何发送给容器的 Unix 信号了。
#例如:Postgres 官方镜像使用下面的脚本作为 ENTRYPOINT;
#!/bin/bash
set -e
if [ &quot;$1&quot; = &#39;postgres&#39; ]; then
chown -R postgres &quot;$PGDATA&quot;
if [ -z &quot;$(ls -A &quot;$PGDATA&quot;)&quot; ]; then
gosu postgres initdb
fi
exec gosu postgres &quot;$@&quot;
fi
exec &quot;$@&quot;
该辅助脚本被拷贝到容器,并在容器启动时通过 ENTRYPOINT 执行:
COPY ./docker-entrypoint.sh /
ENTRYPOINT [&quot;/docker-entrypoint.sh&quot;]
该脚本可以让用户用几种不同的方式和 Postgres 交互
$ docker run postgres
$ docker run postgres postgres --help
$ docker run --rm -it postgres bash #启动另外一个完全不同的工具,比如 Bash:
2.通过ENTRYPOINT指令可以将容器设置作为可执行的文件
docker run <image>命令行参数将会被追加到exec格式的ENTRYPOINT所有元素之后,并将会覆盖使用CMD指定的所有元素,此时是允许将参数传递到入口点;
#下面是是启动一个nginx的例子端口为80:
docker run -i -t --rm -p 80:80 nginx
#例如,docker run &lt;Image&gt; -d 将通过-d 参数传递到入口点
#例如,docker run –entrypoint 字段覆盖&quot;ENTRYPOINT &quot;指令 下面的Dockerfile显示使用ENTRYPOINT在前台运行Apache:
FROM debian:stable
RUN apt-get update &amp;&amp; apt-get install -y --force-yes apache2
EXPOSE 80 443
VOLUME [&quot;/var/www&quot;, &quot;/var/log/apache2&quot;, &quot;/etc/apache2&quot;]
ENTRYPOINT [&quot;/usr/sbin/apache2ctl&quot;, &quot;-D&quot;, &quot;FOREGROUND&quot;]
注意:当指定多个ENTRYPOINT时候只有最后一个生效;
描述:指令用于暴露任何数据库存储文件,配置文件,或容器创建的文件和目录。强烈建议使用 VOLUME来管理镜像中的可变部分和用户可以改变的部分。
VOLUME指令只是起到了声明了容器中的目录作为匿名卷,但是并没有将匿名卷绑定到宿主机指定目录的功能;
镜像run了一个容器的时候,docker会在安装目录下的指定目录下面生成一个目录来绑定容器的匿名卷(这个指定目录不同版本的docker会有所不同)
# 基础示例:
VOLUME [&quot;&lt;路径1&gt;&quot;, &quot;&lt;路径2&gt;&quot;...]
# 实际示例:
#一般用来存放数据和需要保持的数据等,在运行的时候我们就可以利用 -v /Store/data(宿主机):/date(容器),数据就可以直接存在在宿主机上面;
VOLUME [&quot;/data&quot;] 描述:如果某个服务不需要特权执行,建议使用 USER 指令切换到非 root 用户。
注意事项:
1.在镜像中用户和用户组每次被分配的 UID/GID 都是不确定的,下次重新构建镜像时被分配到的 UID/GID 可能会不一样。 如果要依赖确定的 UID/GID 你应该显示的指定一个 UID/GID。
# 方式1
RUN groupadd -r postgres -g 1001 &amp;&amp; useradd -r -g postgres postgres -u 1001
# 方式2
RUN useradd -r -u 1001 -U postgres 2.应该避免使用 sudo 命令因为它不可预期的 TTY 和信号转发行为可能造成的问题比它能解决的问题还多。如果你真的需要和 sudo 类似的功能(例如以 root 权限初始化某个守护进程,以非 root 权限执行它)你可以使用 gosu 命令; 最后为了减少层数和复杂度,避免频繁地使用 USER 来回切换用户。
# 基础示例
#(1) 当服务不需要管理员权限时,可以通过该命令指定运行用户,并且可以在之前创建所需要的用户
USER daemon
#(2)要临时获取管理员权限可以使用gosu而不使用sudo;
#USER 只是帮助你切换到指定用户而已,这个用户必须是事先建立好的,否则无法切换。
RUN groupadd -r postgres &amp;&amp; useradd -r -g postgres postgres
USER postgres
描述:为了清晰性和可靠性,你应该总是在中使用绝对路径。另外你应该使用 WORKDIR 来替代类似于 RUN cd ... && do-something 的指令,后者难以阅读、排错和维护。
# 基础示例
#(1)为后续的RUN CMD ENTRYPOINT 指令配置工作目录
WORKDIR /path/to/workdir
#(2)使用多个WORKDIR指令,如果后续命令参数是相对路径,则会基于首个绝对路径进行拼接。
WORKDIR /a
WORKDIR b
WORKDIR C
RUN pwd #最终路径是/a/b/c
描述:是一个特殊的指令在当前镜像构建时并不会被执行。只有当以当前镜像为基础镜像去构建下一级镜像的时候才会被执行。它后面跟的是其它指令比如 等
Dockerfile 中的其它指令都是为了定制当前镜像而准备的,唯有 ONBUILD 是为了帮助别人定制自己而准备的。
为了方便我们理解先来看一个示例: 假设我们要制作 Node.js 所写的应用的镜像。我们都知道 Node.js 使用 npm 进行包管理,所有依赖、配置、启动信息等会放到 package.json 文件里。在拿到程序代码后,需要先进行 npm install 才可以获得所有需要的依赖然后再通过 npm start 来启动应用。
Dockerfile
FROM node:slim
RUN mkdir /app
WORKDIR /app
COPY ./package.json /app
RUN [ &quot;npm&quot;, &quot;install&quot; ]
COPY . /app/
CMD [ &quot;npm&quot;, &quot;start&quot; ]
把这个 Dockerfile 放到 Node.js 项目的根目录,构建好镜像后,就可以直接拿来启动容器运行。
但是如果我们还有第二个 Node.js 项目也差不多呢? 好吧那就再把这个 Dockerfile 复制到第二个项目里那如果有第三个项目呢?再复制么?文件的副本越多,版本控制就越困难,让我们继续看这样的场景维护的问题:如果第一个 Node.js 项目在开发过程中,发现这个 Dockerfile 里存在问题,比如敲错字了、或者需要安装额外的包,然后开发人员修复了这个 Dockerfile,再次构建,问题解决。
第一个项目没问题了,但是第二个项目呢? 虽然最初 Dockerfile 是复制、粘贴自第一个项目的,但是并不会因为第一个项目修复了他们的 Dockerfile,而第二个项目的 Dockerfile 就会被自动修复。
那么我们可不可以做一个基础镜像,然后各个项目使用这个基础镜像呢? 答案是显而易见这样基础镜像更新,各个项目不用同步 Dockerfile 的变化,重新构建后就继承了基础镜像的更新?好吧,可以让我们看看这样的结果。 那么上面的这个 Dockerfile 就会变为:
FROM my-node
RUN mkdir /app
WORKDIR /app
CMD [ &quot;npm&quot;, &quot;start&quot; ] 现在把相关构建的指令拿出来放在子项目里面,此时假如各个项目内的自己Dockerfile变成如下:
FROM node:slim
COPY ./package.json /app
RUN [ &quot;npm&quot;, &quot;install&quot; ]
COPY . /app/ 基础镜像变化后,各个项目都用这个 Dockerfile 重新构建镜像,会继承基础镜像的更新。
那么,问题解决了么? 没有准确说,只解决了一半。如果这个 Dockerfile 里面有些东西需要调整呢?比如 npm install 都需要加一些参数,那怎么办?这一行 RUN 是不可能放入基础镜像的,因为涉及到了当前项目的 ./package.json,难道又要一个个修改么?所以说这样制作基础镜像,只解决了原来的 Dockerfile 的前4条指令的变化问题,而后面三条指令的变化则完全没办法处理。
完整解决Dockerfile
#当我们需要在一个镜像中操作添加更新然后打包放在子项目中,在以后的日子里都以此镜像来更新创建容器,为了方便版本控制所以引入了ONBUILD指令
FROM node:slim
RUN mkdir /app
WORKDIR /app
ONBUILD COPY ./package.json /app
ONBUILD RUN [ &quot;npm&quot;, &quot;install&quot; ]
ONBUILD COPY . /app/
CMD [ &quot;npm&quot;, &quot;start&quot; ]
#在构建基础镜像的时候 -t my-node,这三行ONBUILD并不会被执行,然后各个项目的 Dockerfile 就变成了简单地:
# 例如,执行docker build 并不会执行ONBUILD ,只有在使用my-node-onbuild:latest 作为基础镜像时才触发。
Docker build -t weiyigeek/my-node-onbuild:latest .
FROM my-node
#当在各个项目目录中,用这个只有一行的 Dockerfile 构建镜像时,之前基础镜像的那三行 ONBUILD 就会开始执行,成功的将当前项目的代码复制进镜像、并且针对本项目执行 npm install,生成应用镜像。
描述: 该指令设置将发送到的系统调用信号容器退出,如果不定义信号名称默认是 SIGTERM。 此信号可以是格式中的 , 例如 SIGKILL,或与 例如内核的系统调用表 9.
# 实际示例
STOPSIGNAL signal
描述: 该指令允许使用 shell 形式覆盖命令, Linux中默认的Shell是,而在Windows中 默认的Shell是, 如果Linux中还存在备用的shell例如(),我们也可以采用此种方法指定。
# 实际示例
SHELL [&quot;/bin/sh&quot;,&quot;-c&quot;]
SHELL [&quot;powershell&quot;,&quot;-command&quot;]
# 实践使用示例
FROM microsoft/nanoserver
SHELL [&quot;powershell&quot;,&quot;-command&quot;]
RUN New-Item -ItemType Directory C:\Example
ADD Execute-MyCmdlet.ps1 c:\example\
RUN c:\example\Execute-MyCmdlet -sample &#39;hello world&#39;
描述:该命令设置检查容器健康状况的命令,它与 kubernetes 中的 Pod 探针类似
在没有 指令前Docker 引擎只可以通过出来判断容器是否状态异常。很多情况下这没问题,,但是该容器已经无法提供服务了。
在 1.12 以前,Docker 不会检测到容器的这种状态,从而不会重新调度,导致可能会有部分容器已经无法提供服务了却还在接受用户请求。
从 Docker 1.12 引入该指令HEALTHCHECK 指令是告诉 Docker 应该如何进行判断容器的状态是否正常,从而比较真实的反应容器实际状态。
当在一个镜像指定了 HEALTHCHECK 指令后,用其启动容器,初始状态会为 starting,,如果连续一定次数失败,则会变为 unhealthy。
# 基础语法
#命令的返回值决定了该次健康检查的成功与否:0:成功;1:失败;2:保留,不要使用这个值。
HEALTHCHECK [选项] CMD &lt;命令&gt;
# 选项:
--interval=&lt;间隔&gt;:两次健康检查的间隔,默认为 30 秒;
--timeout=&lt;时长&gt;:健康检查命令运行超时时间,如果超过这个时间本次健康检查就被视为失败默认 30 秒;
--retries=&lt;次数&gt;:当连续失败指定次数后,则将容器状态视为 unhealthy,默认 3 次。
# 如果基础镜像有健康检查指令,使用这行可以屏蔽掉其健康检查指令
HEALTHCHECK NONE 基础示例:
# Dockerfile 示例
# (1) 设置了每 5 秒检查一次(这里为了试验所以间隔非常短,实际应该相对较长),如果健康检查命令超过 3 秒没响应就视为失败
# 并且使用 curl -fs http://localhost/ || exit 1 作为健康检查命令。
FROM nginx
RUN apt-get update &amp;&amp; apt-get install -y curl &amp;&amp; rm -rf /var/lib/apt/lists/*
HEALTHCHECK --interval=5s --timeout=3s --retries=5 \
CMD curl -fs http://localhost/ || exit 1
# (2) 使用 docker build 来构建这个镜像并启动;
docker build -t myweb:v1 .
$ docker run -d --name web -p 80:80 myweb:v1
# (3) 最初的状态为 (health: starting)在等待几秒钟后,再次 docker container ls 就会看到健康状态变化为了 (healthy);
# 如果健康检查连续失败超过了重试次数,状态就会变为 (unhealthy)。
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
03e28eb00bd0 myweb:v1 &quot;nginx -g &#39;daemon off&quot; 18 seconds ago Up 16 seconds (healthy) 80/tcp, 443/tcp web
# (4) 健康检查命令的输出(包括 stdout 以及 stderr)都会被存储于健康状态里,可以用 docker inspect 来查看
$ docker inspect --format &#39;{{json .State.Health}}&#39; web | python -m json.tool
{
&quot;FailingStreak&quot;: 0,
&quot;Log&quot;: [
{
&quot;End&quot;: &quot;2019-12-30T14:35:37.940957051Z&quot;,
&quot;ExitCode&quot;: 0,
&quot;Output&quot;: &quot;&lt;!DOCTYPE html&gt;\n&lt;html&gt;\n&lt;head&gt;\n&lt;title&gt;Welcome to nginx!&lt;/title&gt;....&quot;,
&quot;Start&quot;: &quot;2019-12-30T14:35:37.780192565Z&quot;
}
],
&quot;Status&quot;: &quot;healthy&quot;
} Q:ADD与COPY之间的比较? 答:优先使用COPY命令,而ADD除了COPY功能还有解压功能(在多数情况下显得多余) 注意:ADD 指令会令镜像构建缓存失效从而可能会令镜像构建变得比较缓慢。
Q:RUN and CMD and ENTRYPOINT之间的差别?
区别在于 RUN 是在镜像构建过程中执行的,而 CMD/ENTRYPOINT 是在镜像生成实例的时候执行的 RUN:执行命令并创建新的Image Layer CMD:设置容器启动后默认执行的命令和参数(如果定义多个CMD,只有最后一个执行) ENTRYPOINT:设置容器启动时运行的命令,让容器以应用程序或服务形式运行
补充重点:前面我们说过CMD和ENTRYPOINT指令都可以定义容器运行时所执行的命令,下面是它们之间协调的一些规则:
在Dockerfile至少需要设置一条CMD或者ENTRYPOINT指令;
当将容器作为可执行文件使用时,建议定义ENTRYPOINT指令;
CMD作为为ENTRYPOINT命令定义默认参数的一种方式;
当使用带有参数的命令运行容器时 CMD将会被覆盖。
下表是显示了不同的指令组合的命令执行情况:

Q:命令指令的()不同格式异同?; 描述:通过shell格式去运行命令会读取$name变量,而exec格式是仅仅的执行一个命令而不是shell指令;
RUN/CMD/ENTRYPOINT yum install -y vim #Shell格式 如$PWD $LANG 宿主机
RUN/CMD/ENTRYPOINT [&quot;apt-get&quot;,&quot;install&quot;,&quot;-y&quot;,&quot;vim&quot;] #EXEC格式
Q:ARG与ENV之间的区别? 答:ARG 所设置的构建环境的环境变量,在将来容器运行时是不会存在这些环境变量的,但是不要因此就使用 ARG 保存密码之类的信息,因为 docker history 还是可以看到所有值的。 而ENV会将变量通过镜像的entrypoint指令与容器中的应用传值;
Q:Docker前后台执行浅析问题? 描述:提到 CMD 就不得不提容器中应用在前台执行和后台执行的问题()
Docker不是虚拟机在容器中的应用都应该以前台执行,而不是像虚拟机、物理机里面那样,。 对于容器而言,启动程序就是容器应用进程,容器就是为了主进程而存在的,主进程退出容器就失去了存在的意义从而退出,其它辅助进程不是它需要关心的东西。
#错误的形式
CMD service nginx start #发现容器执行后就立即退出了
#会被理解为
CMD [ &quot;sh&quot;, &quot;-c&quot;, &quot;service nginx start&quot;]
#因此主进程实际上是 sh。那么当 service nginx start 命令结束后,sh 也就结束了,sh 作为主进程退出了,自然就会令容器退出。
#正确的做法是直接执行 nginx 可执行文件,并且要求以前台形式运行。比如:
CMD [&quot;nginx&quot;, &quot;-g&quot;, &quot;daemon off;&quot;]
那么什么是上下文路径呢? 首先我们要理解 docker build 的工作原理,由于Docker是C/S设计架构,Dockerclient通过这组 API 与 Docker 引擎交互,从而完成各种功能; 因此,虽然表面上我们好像是在本机执行各种 docker 功能,但实际上一切都是使用的远程调用形式在服务端(Docker 引擎)完成。 当我们进行镜像构建的时候,并非所有定制都会通过 RUN 指令完成,经常会需要将一些本地文件复制进镜像,比如通过 COPY 指令、ADD 指令等。
而 docker build 命令构建镜像,其实并非在本地构建,而是在服务端也就是 Docker 引擎中构建的;用户会指定构建镜像上下文的路径,docker build 命令得知这个路径后,会将路径下的所有内容打包,然后上传给 Docker 引擎。这样 Docker 引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。
一般来说,应该会将 Dockerfile 置于一个空目录下,或者项目根目录下。如果该目录下没有所需文件,那么应该把所需文件复制一份过来。如果目录下有些东西确实不希望构建时传给 Docker 引擎,那么可以用 .gitignore 一样的语法写一个 .dockerignore,该文件是用于剔除不需要作为上下文传递给 Docker 引擎的。
COPY /opt/package.json /app/ #上下文路径错误或导致构造失败
COPY ./package.json /app/ #COPY 这类指令中的源文件的路径都是相对路
#这并不是要复制执行 docker build 命令所在的目录下的 package.json,也不是复制 Dockerfile 所在目录下的 package.json,而是复制 上下文(context) 目录下的 package.json。 下列指令可以支持环境变量展开:
ADD、COPY、ENV、EXPOSE、LABEL、USER、WORKDIR、VOLUME、STOPSIGNAL、ONBUILD。
注意:对于Windows系统建议Dockerfile开头添加#escape=
描述: 确保app.py和dockerfile在同一个目录 1.准备好app.py的flask程序
[root@localhost ~]# cat app.py
from flask import Flask
app=Flask(__name__)
@app.route(&#39;/&#39;)
def hello():
return &quot;hello docker&quot;
if __name__==&quot;__main__&quot;:
app.run(host=&#39;0.0.0.0&#39;,port=8080)
[root@master home]# ls
app.py Dockerfile

2.编写dockerfile
FROM python:2.7
LABEL version=&quot;1.0&quot;
LABEL maintainer=&quot;weiyigeek&quot;
RUN pip install flask
COPY app.py /app/
WORKDIR /app
EXPOSE 8080
CMD [&quot;python&quot;,&quot;app.py&quot;] 使用.dockerignore文件(每一行添加一条匹配模式)来让Docker忽略匹配模式路径下的目录和文件;
#comment
*/temp*
*/*/temp*
tmp?
~*
3.构建镜像image找到当前目录的dockerfile开始构建
docker build -t weiyigeek/flask-hello-docker .
docker image ls #.查看创建好的images
4.启动此flask-hello-docker容器,映射一个端口供外部访问(检查运行的容器)
docker run -d -p 8080:8080 weiyigeek/flask-hello-docker
docker container ls
5.提交修改并推送到我们的私有仓库中
docker tag weiyigeek/flask-hello-docker 192.168.11.37:5000/peng-flaskweb
docker push 192.168.11.37:5000/peng-flaskweb
# Tomcat]# docker build -t weiyigeek/tomcat:7.0.85-jre8-gbk18030 .
FROM weiyigeek/tomcat:7.0.85-jre8
MAINTAINER WeiyiGeek &lt;master@WeiyiGeek.top&gt;
RUN wget -O /etc/apt/sources.list http://192.168.10.206:8080/sources.list \
&amp;&amp; apt clean &amp;&amp; apt update &amp;&amp; apt install -y locales \
&amp;&amp; sed -i &#39;s/# zh_CN.GB18030/zh_CN.GB18030/g&#39; /etc/locale.gen \
&amp;&amp; locale-gen \
&amp;&amp; locale -a \
&amp;&amp; rm -rf /var/cache/apt/*
ENV LANG=zh_CN.gb18030
CMD [&quot;catalina.sh&quot; &quot;run&quot;] FROM golang:alpine as builder
ARG IMG_PATH=/opt/pics
ARG EXHAUST_PATH=/opt/exhaust
RUN apk update ;\
apk add alpine-sdk ;\
git clone https://github.com/webp-sh/webp_server_go /build ;\
cd /build ;\
sed -i &quot;s|.\/pics|${IMG_PATH}|g&quot; config.json ;\
sed -i &quot;s|\&quot;\&quot;|\&quot;${EXHAUST_PATH}\&quot;|g&quot; config.json ;\
sed -i &#39;s/127.0.0.1/0.0.0.0/g&#39; config.json
WORKDIR /build
RUN go build -o webp-server .
FROM alpine
COPY --from=builder /build/webp-server /usr/bin/webp-server
COPY --from=builder /build/config.json /etc/config.json
WORKDIR /opt
VOLUME /opt/exhaust
CMD [&quot;/usr/bin/webp-server&quot;, &quot;--config&quot;, &quot;/etc/config.json&quot;]
注意事项:在 RUN 指令的每行结尾我使用的是 ;\ 来接下一行 shell 而不是 && 其中缘由相信读者也猜到一二了吧(提高容错性),两则本质区别是 运行失败时会继续运行而 运行成功则继续执行; 笔者也逛了一圈 docker hub 官方镜像中用较多一些,个人觉得是因为比要美观一些
#基础镜像
FROM mhart/alpine-node:latest
#作者描述
LABEL maintainer=&quot;weiyigeek &lt;weiygeek@qq.com&gt;&quot;
LABEL description=&quot;weiyigeek blog&quot;
# 切换工作目录
WORKDIR /opt/web/
#更新alpine软件源和下载git
RUN apk update \
&amp;&amp; apk add git \
&amp;&amp; git clone https://gitee.com/WeiyiGeek/blog.git
ENV LANG=en_US.UTF-8 \
LANGUAGE=en_US:en \
LC_ALL=en_US.UTF-8
# 重要不能采用cd进行切换
WORKDIR /opt/web/blog/
#下载博客hexo框架与依赖
RUN npm install -g hexo-cli --save \
&amp;&amp; npm install --production --registry=https://registry.npm.taobao.org --save
#对外暴露的端口
EXPOSE 4000 30000-40000
#容器建立好运行的命令
ENTRYPOINT [&quot;hexo&quot;, &quot;server&quot;]

执行完成后将会在docker iamges 中显示我们设置仓库名称:
$docker run -d -p 80:4000 --name blog weiyigeek/blog
c6059fc19891792ce03304bcf943dccd708e5b8c7565fe86159d3407e23e7530
$docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c6059fc19891 weiyigeek/blog &quot;hexo server&quot; 7 seconds ago Up 5 seconds 0.0.0.0:80-&gt;4000/tcp blog
构建docker容器的多种方式: 方式1:将所有的构建过程编包含在一个 Dockerfile 中,包括项目及其依赖库的编译、测试、打包等流程 产生的问题:
镜像层次多,镜像体积较大,部署时间变长
源代码存在泄露的风险
优点:管理方便简单
方式2:将项目及其依赖库编译测试打包好后,再将编译好的可执行文件拷贝到运行环境中 产生的问题:部署过程较复杂 优点:生成的镜像大小相比较于方式1较小
比如,参照下面的多阶段构建
#!/bin/sh
#该方式构建需要shell文件来进行打包融合
echo Building gcc/helloworld:build
#进行编译环境搭建并编译环境
docker build -t gcc/helloworld:build . -f Dockerfile.build
#创建编译的容器
docker create --name extract go/helloworld:builder
docker cp extract:/gcc/demo/test ./test
docker rm -f extract
echo Building gcc/helloworld:runner
#构建运行时环境
docker build --no-cache -t gcc/helloworld:2 . -f Dockerfile.copy
rm ./test
方式3:Docker v17.05 开始支持多阶段构建 (multistage builds),此种方法综合和方式1和2并且解决了他们响应的问题;
比如,我们要编译.c文件分为两步进行,编译c再运行
步骤01.test.c 文件内容如下
#include &lt;stdio.h&gt;
int main(void)
{
printf(&quot;Hello World! Dockerfile!\n&quot;);
return 0;
}
步骤02.编写Dockerfile,此处使用多阶段构建
# Step1 . 编译阶段-Dockerfile.build
FROM alpine as builder
MAINTAINER weiyigeek &lt;weiyigeek@q.com&gt;
RUN apk update &amp;&amp; apk --no-cache add gcc g++
WORKDIR /gcc/demo/
COPY test.c .
RUN gcc test.c -o test
# Step2 . 运行阶段-Dockerfile.run
FROM alpine-c as runer
WORKDIR /gcc/demo/
#复制 builder 阶段成果二进制文件test到 当前镜像之中
COPY --from=0 /gcc/demo/test .
# 或者 COPY --from=builder /gcc/demo/test .
CMD [&quot;./test&quot;]
步骤03.进行镜像构建操作
# 项目结构
$ls
mutilDockerfile test.c
# 构建镜像
$docker build -t weiyigeek/hello:1 -f mutilDockerfile . #注意上下文
Sending build context to Docker daemon 3.072kB
Step 1/10 : FROM alpine as builder
---&gt; 055936d39205
Step 2/10 : MAINTAINER weiyigeek &lt;weiyigeek@q.com&gt;
---&gt; Running in 59097e3a8120
Removing intermediate container 59097e3a8120
---&gt; 8e796c96a921
Step 3/10 : RUN apk update &amp;&amp; apk --no-cache add gcc g++
---&gt; Running in 89fca19b761d
步骤04.最后运行构建的镜像
$docker run --name hello weiyigeek/hello:1
Hello World! Dockerfile! 至此本节完毕,敬请期待下一小节内容。
Docker容器技术入门实践系列历史已发布文章(点击即可进入):
4.我在B站学云原生之Docker容器数据持久化介绍与实践
6.我在B站学云原生之Docker容器Registry私有镜像仓库搭建实践

欢迎各位志同道合的朋友一起学习交流,如文章有误请在下方留下您宝贵的经验知识,个人邮箱地址【master#weiyigeek.top】或者 个人公众号【WeiyiGeek】联系我。

更多文章来源于【WeiyiGeek Blog - 为了能到远方,脚下的每一步都不能少】 个人博客。
博客地址: https://weiyigeek.top

人永远在追求快乐,永远在逃避痛苦!
专栏书写不易,如果您觉得这个专栏还不错的,请给这篇专栏【点个赞、投个币、收个藏、关个注,转个发】,这将对我的肯定,谢谢!。
echo "【点个赞】,动动你那粗壮的拇指或者芊芊玉手,亲!"
printf("%s", "【投个币】,万水千山总是情,投个硬币行不行,亲!")
fmt.Printf("【收个藏】,阅后即焚不吃灰,亲!")
System.out.println("【关个注】,后续浏览查看不迷路哟,亲!")
console.info("【转个发】,让更多的志同道合的朋友一起学习交流,亲!")