本篇记录一下DockerFile怎么写。

1. 关于dockerFile

Dockerfile 是一个文本文件,其内包含了一条条的 指令(Instruction)每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。  因此不要写太多指令,可以将多个指令写在一条。

首先创建一个文件夹然后进入这个文件夹创建一个名字叫  Dockerfile 的文件。如下我创建一个docker 文件夹。

vim  Dockerfile   添加如下命令

FROM nginx
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html

docker  build  -t  nginx:v2  .   注意后面这个点   意思是构建当前目录。

2.FROM 指定基础镜像

所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。就像我们之前运行了一个 nginx 镜像的容器,再进行修改一样,基础镜像是必须指定的。而 FROM 就是指定 基础镜像,因此一个 Dockerfile 中 FROM 是必备的指令,并且必须是第一条指令。

3.RUN 执行命令

RUN 指令是用来执行命令行命令的。由于命令行的强大能力,RUN 指令在定制镜像时是最常用的指令之一。其格式有两种:

  • shell 格式:RUN <命令>,就像直接在命令行中输入的命令一样。刚才写的 Dockerfile 中的 RUN 指令就是这种格式。
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
  • exec 格式:RUN ["可执行文件", "参数1", "参数2"],这更像是函数调用中的格式。

对于下面这种指令,实际上用了两次RUN 那么就build就会创建了两层镜像。

RUN apt-get update
RUN apt-get install -y gcc libc6-dev make wget

Dockerfile 中每一个指令都会建立一层,RUN 也不例外。每一个 RUN 的行为,就和commit手工建立镜像的过程一样:新建立一层,在其上执行这些命令,执行结束后,commit 这一层的修改,构成新的镜像。

Union FS 是有最大层数限制的,比如 AUFS,曾经是最大不得超过 42 层,现在是不得超过 127 层。

应该做出如下 && 修改    下面就是编译、安装 redis 可执行文件

FROM debian:stretch

RUN buildDeps='gcc libc6-dev make wget' \
    && apt-get update \
    && apt-get install -y $buildDeps \
    && wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" \
    && mkdir -p /usr/src/redis \
    && tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \
    && make -C /usr/src/redis \
    && make -C /usr/src/redis install \
    && rm -rf /var/lib/apt/lists/* \
    && rm redis.tar.gz \
    && rm -r /usr/src/redis \
    && apt-get purge -y --auto-remove $buildDeps

仅仅使用一个 RUN 指令,并使用 && 将各个所需命令串联起来。将多层简化为了 1 层。在撰写 Dockerfile 的时候,要经常提醒自己,这并不是在写 Shell 脚本,而是在定义每一层该如何构建。

并且,这里为了格式化还进行了换行。Dockerfile 支持 Shell 类的行尾添加 \ 的命令换行方式,以及行首 # 进行注释的格式。良好的格式,比如换行、缩进、注释等,会让维护、排障更为容易,这是一个比较好的习惯。

此外,还可以看到这一组命令的最后添加了清理工作的命令,删除了为了编译构建所需要的软件,清理了所有下载、展开的文件,并且还清理了 apt 缓存文件。这是很重要的一步,我们之前说过,镜像是多层存储,每一层的东西并不会在下一层被删除,会一直跟随着镜像。因此镜像构建时,一定要确保每一层只添加真正需要添加的东西,任何无关的东西都应该清理掉。

很多人初学 Docker 制作出了很臃肿的镜像的原因之一,就是忘记了每一层构建的最后一定要清理掉无关文件。

4.构建

在Dockerfile文件所在目录执行: 注意必须有Dockerfile文件才能 build  哪怕不叫这个名用 -f 参数指定dockerfile。

-t 参数意思是指定  镜像的名字及标签

从命令的输出结果中,我们可以清晰的看到镜像的构建过程。在step中,如同我们之前所说的那样,RUN指令启动了一个容器88baf8e5c516,执行了所要求的命令,并最后提交了这一层0475a6626276 ,随后删除了所用到的这个容器88baf8e5c516。

这里我们使用了 docker build 命令进行镜像构建。其格式为:

docker build [选项] <上下文路径/URL/->

5.构建镜像上下文

docker build 命令最后有一个 .      . 表示当前目录,而 Dockerfile 就在当前目录,这是在指定 上下文路径

(即在当前pwd的目录下找dockerfile文件,并将当前目录作为上下文路径)

首先我们要理解 docker build 的工作原理。Docker 在运行时分为 Docker 引擎(也就是服务端守护进程)和客户端工具。Docker 的引擎提供了一组 REST API,被称为 Docker Remote API,而如 docker 命令这样的客户端工具,则是通过这组 API 与 Docker 引擎交互,从而完成各种功能。因此,虽然表面上我们好像是在本机执行各种 docker 功能,但实际上,一切都是使用的远程调用形式在服务端(Docker 引擎)完成。也因为这种 C/S 设计,让我们操作远程服务器的 Docker 引擎变得轻而易举。

当我们进行镜像构建的时候,并非所有定制都会通过 RUN 指令完成,经常会需要将一些本地文件复制进镜像,比如通过 COPY 指令、ADD 指令等。而 docker build 命令构建镜像,其实并非在本地构建,而是在服务端,也就是 Docker 引擎中构建的。那么在这种客户端/服务端的架构中,如何才能让服务端获得本地文件呢?

这就引入了上下文的概念。当构建的时候,用户会指定构建镜像上下文的路径,docker build 命令得知这个路径后,会将路径下的所有内容打包,然后上传给 Docker 引擎。这样 Docker 引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。

如果在 Dockerfile 中这么写:

COPY ./package.json /app/

这并不是要复制执行 docker build 命令所在的目录下的 package.json,也不是复制 Dockerfile 所在目录下的 package.json,而是复制 上下文(context) 目录下的 package.json(执行docker build 时已经指定好上下文路径了 .   将上下文路径下面的package.json文件复制到 docker 服务端中的 /app/目录下 。后面会详细记录COPY目录以及工作目录的概念 )

因此,COPY 这类指令中的源文件的路径都是相对路径。 COPY ../package.json /app 或者 COPY /opt/xxxx /app 无法工作的原因是这些路径已经超出了上下文的范围,Docker 引擎无法获得这些位置的文件。如果真的需要那些文件,应该将它们复制到上下文目录中去。

现在就可以理解刚才的命令 docker build -t nginx:v5 . 中的这个 .,实际上是在指定上下文的目录,docker build 命令会将该目录下的内容打包交给 Docker 引擎以帮助构建镜像。

如果观察 docker build 输出,我们其实已经看到了这个发送上下文的过程:

docker build -t nginx:v5 .
Sending build context to Docker daemon  2.048kB

一般来说,应该会将 Dockerfile 置于一个空目录下,或者项目根目录下。如果该目录下没有所需文件,那么应该把所需文件复制一份过来。如果目录下有些东西确实不希望构建时传给 Docker 引擎,那么可以用 .gitignore 一样的语法写一个 .dockerignore,该文件是用于剔除不需要作为上下文传递给 Docker 引擎的。

那么为什么会有人误以为 . 是指定 Dockerfile 所在目录呢?这是因为在默认情况下,如果不额外指定 Dockerfile 的话,会将上下文目录下的名为 Dockerfile 的文件作为 Dockerfile。

这只是默认行为,实际上 Dockerfile 的文件名并不要求必须为 Dockerfile,而且并不要求必须位于上下文目录中,比如可以用 -f ../Dockerfile.php 参数指定某个文件作为 Dockerfile

当然,一般大家习惯性的会使用默认的文件名 Dockerfile,以及会将其置于镜像构建上下文目录中。

docker build -f /path/to/a/Dockerfile .

构建的整体流程大概如下。

  • 执行 docker build -t <imageName:imageTag> . ;
  • Docker 客户端会将构建命令后面指定的路径(.)下的所有文件打包成一个 tar 包,发送给 Docker 服务端;
  • Docker 服务端收到客户端发送的 tar 包,然后解压,根据 Dockerfile 里面的指令进行镜像的分层构建;
  • 此时你的dockerFile有操作上下文目录的可以正常操纵。

6.其它 docker build 的用法

直接用 Git repo 进行构建

或许你已经注意到了,docker build 还支持从 URL 构建,比如可以直接从 Git repo 中构建:

$ docker build https://github.com/twang2218/gitlab-ce-zh.git#:11.1

这行命令指定了构建所需的 Git repo,并且指定默认的 master 分支,构建目录为 /11.1/,然后 Docker 就会自己去 git clone 这个项目、切换到指定分支、并进入到指定目录后开始构建。

用给定的 tar 压缩包构建   docker build http://server/context.tar.gz

如果所给出的 URL 不是个 Git repo,而是个 tar 压缩包 Docker 引擎会下载这个包,并解压缩,以其作为上下文来构建。

7. COPY复制文件

COPY 指令将从构建上下文目录中 <源路径> 的文件/目录复制到新的一层的镜像内的 <目标路径> 位置。

  • COPY [--chown=<user>:<group>] <源路径>... <目标路径>
  • COPY [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]
COPY package.json /usr/src/app/

<源路径> 可以是多个,甚至可以是通配符,其通配符规则要满足 Go 的 filepath.Match 规则,如:

COPY hom* /mydir/
COPY hom?.txt /mydir/

<目标路径> 可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 WORKDIR 指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。

此外,还需要注意一点,使用 COPY 指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用。特别是构建相关文件都在使用 Git 进行管理的时候。

在使用该指令的时候还可以加上 --chown=<user>:<group> 选项来改变文件的所属用户及所属组。

COPY --chown=55:mygroup files* /mydir/
COPY --chown=bin files* /mydir/
COPY --chown=1 files* /mydir/
COPY --chown=10:11 files* /mydir/

8.ADD 更高级的复制文件

ADD 指令和 COPY 的格式和性质基本一致。但是在 COPY 基础上增加了一些功能。

ADD指令增加了 源路径可以为URL    并且对于

ADD   http://asd.com/a.tar.gz    /app

需要注意的是 / 这个是很敏感的。我第一次写的  /app 然后进容器发现App创建成了文件而不是目录。当写成  /app/  就会创建成目录了。  通过add 远程下载的文件root文件系统文件权限会是 600  你可以chown像COPY一样。

ADD http://mirrors.tuna.tsinghua.edu.cn/apache/tomcat/tomcat-8/v8.5.45/bin/apache-tomcat-8.5.45.tar.gz  /app/

ADD 指令会令镜像构建缓存失效,从而可能会令镜像构建变得比较缓慢。

个人不成熟的感觉使用 COPY就好了,除非你有需求一定要通过URL下载。或者解压。(todo而且我如上这个ADD命令也没有成功解压,我去Tomcat官网取的html中写的文件地址)

9.CMD

对于容器而言,其启动程序就是容器应用进程,容器就是为了主进程而存在的,主进程退出,容器就失去了存在的意义,从而退出,其它辅助进程不是它需要关心的东西。

所以你CMD的命令如果是一瞬间结束的话,docker  ps  是不会查询到运行的容器的、

CMD 指令的格式和 RUN 相似,也是两种格式:

  • shell 格式:CMD <命令>
  • exec 格式:CMD ["可执行文件", "参数1", "参数2"...]
  • 参数列表格式:CMD ["参数1", "参数2"...]。在指定了 ENTRYPOINT 指令后,用 CMD 指定具体的参数。

FROM  nginx

CMD   echo  $HOME 

可以看到有输出但是直接就退出了,这是因为CMD只能运行一个命令,而这个命令作为主进程,主进程结束宣告退出。

在运行时可以指定新的命令来替代镜像设置中的这个默认命令,比如,ubuntu 镜像默认的 CMD 是 /bin/bash,如果我们直接 docker run -it ubuntu 的话,会直接进入 bash。我们也可以在运行时指定运行别的命令,如 docker run -it ubuntu cat /etc/os-release。这就是用 cat /etc/os-release 命令替换了默认的 /bin/bash 命令了,输出了系统版本信息。

在指令格式上,一般推荐使用 exec 格式,这类格式在解析时会被解析为 JSON 数组,因此一定要使用双引号 ",而不要使用单引号。

如果使用 shell 格式的话,实际的命令会被包装为 sh -c 的参数的形式进行执行。比如:

CMD echo $HOME

在实际执行中,会将其变更为:

CMD [ "sh", "-c", "echo $HOME" ]

提到 CMD 就不得不提容器中应用在前台执行和后台执行的问题。这是初学者常出现的一个混淆。

Docker 不是虚拟机,容器中的应用都应该以前台执行,而不是像虚拟机、物理机里面那样,用 systemd 去启动后台服务,容器内没有后台服务的概念。

Docker 不是虚拟机,容器就是进程。既然是进程,那么在启动容器的时候,需要指定所运行的程序及参数。CMD 指令就是用于指定默认的容器主进程的启动命令的。

之前我写的nginx例子 实际上也是  CMD ["nginx", "-g", "daemon off;"]   来运行的。这是nginx镜像的默认,我试着重写CMD 输出一句echo 就直接退出了。这个概念是很重要的,至少它蒙蔽了我很久。

10.ENTRYPOINT 入口点

ENTRYPOINT 的格式和 RUN 指令格式一样,分为 exec 格式和 shell 格式。

ENTRYPOINT 的目的和 CMD 一样,都是在指定容器启动程序及参数。ENTRYPOINT 在运行时也可以替代,不过比 CMD 要略显繁琐,需要通过 docker run 的参数 --entrypoint 来指定。

当指定了 ENTRYPOINT 后,CMD 的含义就发生了改变,不再是直接的运行其命令,而是将 CMD 的内容作为参数传给 ENTRYPOINT 指令,换句话说实际执行时,将变为:

<ENTRYPOINT> "<CMD>"

可能直接理解起来费点劲。实际上就是 ENREYPOINT这个指令将CMD 搞成动态的了。

FROM ubuntu:18.04
RUN apt-get update \
    && apt-get install -y curl \
    && rm -rf /var/lib/apt/lists/*
CMD [ "curl", "-s", "https://ip.cn" ]

这个加 -i  参数是没用的。

上述这个命令是使用的CMD 没有使用 ENTRYPONT  所以docker run  ubuntu:18.04的时候只能是输出-s 参数的结果,不能再添加参数,因为CMD已经将这个command写死了。但是如果你想加个参数  -i  多查一些信息,你就可以使用ENTRYPOINT。下面展示一下ENTRYPOINT的执行结果。首先我就将CMD改成 ENTRYPOINT 其他没变。

跟在镜像名后面的是 command,运行时会替换 CMD 的默认值。因此这里的 -i 替换了原来的 CMD,而不是添加在原来的 curl -s https://ip.cn 后面。而 -i 根本不是命令,所以自然找不到。

下面是运行的结果。

因为当存在 ENTRYPOINT 后,CMD 的内容将会作为参数传给 ENTRYPOINT,而这里 -i 就是新的 CMD,因此会作为参数传给 curl,从而达到了我们预期的效果。

ENTRYPOINT ["test.sh"]  这样就是在运行这个镜像时,预先执行了一个脚本test.sh  然后默认参数是AAA  run最后可以指定传入的参数 比如改成BBB 那么传入到test.sh的参数就是BBB了,此时run成容器就会有不同的效果了。

CMD ['"AAA"]

因此ENTRYPOINT 可以将镜像做成动态的,这样可以用一个镜像,通过run时传递的不同参数创建不同的容器。

11. ENV

Dockerfile的环境变量。这个指令很简单,就是设置环境变量而已,无论是后面的其它指令,如 RUN,还是运行时的应用,都可以直接使用这里定义的环境变量。

  • ENV <key> <value>
  • ENV <key1>=<value1> <key2>=<value2>...
ENV VERSION=1.0 DEBUG=on \
    NAME="Happy Feet"
#或者如下
ENV VERSION 1.0

下列指令可以支持环境变量展开: ADDCOPYENVEXPOSELABELUSERWORKDIRVOLUMESTOPSIGNALONBUILD

可以从这个指令列表里感觉到,环境变量可以使用的地方很多,很强大。通过环境变量,我们可以让一份 Dockerfile 制作更多的镜像,只需使用不同的环境变量即可。

对于ARG 指令没觉得很有用。请参考:ARG 构建参数 - Docker — 从入门到实践

12.EXPOSE

EXPOSE  8080

EXPOSE 指令是声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务。在 Dockerfile 中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另一个用处则是在运行时使用随机端口映射时,也就是 docker run -P时,会自动随机映射 EXPOSE 的端口。

要将 EXPOSE 和在运行时使用 -p <宿主端口>:<容器端口> 区分开来。-p,是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问,而 EXPOSE 仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。除非你使用docker run --net=host指定才能保证EXPOSE的发挥。

13.WORKDIR 指定工作目录

格式为 WORKDIR <工作目录路径>

使用 WORKDIR 指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR 会帮你建立目录。

这个工作目录的作用是帮你操作一个文件时多个指令可以结合着用,否则每一层指令就是一层。每一层指令执行都是一个新的容器,然后执行你的指令,然后commit。这样当两条指令操作同一个文件时,你以为能成功,实际上却是在两个容器中操作的。

写一个Dockerfile 来验证这个WORKDIR 指令的作用。

FROM ubuntu
WORKDIR /app
RUN echo hello > /app/a.txt
RUN echo zhangyong >> /app/a.txt
如上基于ubuntu镜像,两条RUN指令,第一条写入hello  第二条追加zhangyong到 a.txt  如果不加WORKDIR,执行的时候会报

/bin/sh: 1: cannot create /app/a.txt: Directory nonexistent
当然  dockerfile中你这么写  RUN echo  hello > /tmp/a.txt  RUN echo zang >> /tmp/a.txt 也是ok的,虽然没加WORKDIR。但是镜像它是一层一层打的,后面那一层会基于上一层的RUN 。这么搞的前提是你在ubuntu中存在这个目录。

但是你要是RUN  cd /tmp    RUN echo  hello >a.txt  这么搞就不行了。因为第二次RUN指令时,相当于重新进入了容器,cd命令已经不复存在了。会将a.txt创建在  / 根目录。

RUN  cd  /tmp  \

   &&  echo  hello >a.txt  这么搞是可以的。因为它操作在一层的容器中。  所以每条指令生成一个容器的概念很重要!

14.USER 指定当前用户

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

USER 指令和 WORKDIR 相似,都是改变环境状态并影响以后的层。WORKDIR 是改变工作目录,USER 则是改变之后层的执行 RUNCMD 以及 ENTRYPOINT 这类命令的身份。

当然,和 WORKDIR 一样,USER 只是帮助你切换到指定用户而已,这个用户必须是事先建立好的,否则无法切换。

RUN groupadd -r redis && useradd -r -g redis redis
USER redis
RUN [ "redis-server" ]

在这种root文件系统中使用权限小的用户看起来还是很有用的。但是我喜欢直接操作root  哈哈哈哈、

15.Volume

挂载数据卷。从宿主机映射文件/文件夹到容器里。

volume  宿主机文件(文件夹):/容器文件(文件夹)

dockerfile的方式挂载数据卷的时候使用volume 关键字 但是只能生成随机的目录,不能生成指定的目录。

本篇博客摘自:前言 - Docker — 从入门到实践

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐