赞
踩
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 . 注意后面这个点 意思是构建当前目录。
所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。就像我们之前运行了一个 nginx
镜像的容器,再进行修改一样,基础镜像是必须指定的。而 FROM
就是指定 基础镜像,因此一个 Dockerfile
中 FROM
是必备的指令,并且必须是第一条指令。
RUN
指令是用来执行命令行命令的。由于命令行的强大能力,RUN
指令在定制镜像时是最常用的指令之一。其格式有两种:
RUN <命令>
,就像直接在命令行中输入的命令一样。刚才写的 Dockerfile 中的 RUN
指令就是这种格式。RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
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 制作出了很臃肿的镜像的原因之一,就是忘记了每一层构建的最后一定要清理掉无关文件。
在Dockerfile文件所在目录执行: 注意必须有Dockerfile文件才能 build 哪怕不叫这个名用 -f 参数指定dockerfile。
-t 参数意思是指定 镜像的名字及标签
从命令的输出结果中,我们可以清晰的看到镜像的构建过程。在step中,如同我们之前所说的那样,RUN指令启动了一个容器88baf8e5c516,执行了所要求的命令,并最后提交了这一层0475a6626276 ,随后删除了所用到的这个容器88baf8e5c516。
这里我们使用了 docker build
命令进行镜像构建。其格式为:
docker build [选项] <上下文路径/URL/->
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> .
;.
)下的所有文件打包成一个 tar 包,发送给 Docker 服务端;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 引擎会下载这个包,并解压缩,以其作为上下文来构建。
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/
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中写的文件地址)
对于容器而言,其启动程序就是容器应用进程,容器就是为了主进程而存在的,主进程退出,容器就失去了存在的意义,从而退出,其它辅助进程不是它需要关心的东西。
所以你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 就直接退出了。这个概念是很重要的,至少它蒙蔽了我很久。
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时传递的不同参数创建不同的容器。
Dockerfile的环境变量。这个指令很简单,就是设置环境变量而已,无论是后面的其它指令,如 RUN
,还是运行时的应用,都可以直接使用这里定义的环境变量。
ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>...
- ENV VERSION=1.0 DEBUG=on \
- NAME="Happy Feet"
- #或者如下
- ENV VERSION 1.0
下列指令可以支持环境变量展开: ADD
、COPY
、ENV
、EXPOSE
、LABEL
、USER
、WORKDIR
、VOLUME
、STOPSIGNAL
、ONBUILD
。
可以从这个指令列表里感觉到,环境变量可以使用的地方很多,很强大。通过环境变量,我们可以让一份 Dockerfile
制作更多的镜像,只需使用不同的环境变量即可。
对于ARG 指令没觉得很有用。请参考:ARG 构建参数 - Docker — 从入门到实践
EXPOSE 8080
EXPOSE
指令是声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务。在 Dockerfile 中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另一个用处则是在运行时使用随机端口映射时,也就是 docker run -P
时,会自动随机映射 EXPOSE
的端口。
要将 EXPOSE
和在运行时使用 -p <宿主端口>:<容器端口>
区分开来。-p
,是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问,而 EXPOSE
仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。除非你使用docker run --net=host指定才能保证EXPOSE的发挥。
格式为 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 这么搞是可以的。因为它操作在一层的容器中。 所以每条指令生成一个容器的概念很重要!
格式:USER <用户名>[:<用户组>]
USER
指令和 WORKDIR
相似,都是改变环境状态并影响以后的层。WORKDIR
是改变工作目录,USER
则是改变之后层的执行 RUN
, CMD
以及 ENTRYPOINT
这类命令的身份。
当然,和 WORKDIR
一样,USER
只是帮助你切换到指定用户而已,这个用户必须是事先建立好的,否则无法切换。
- RUN groupadd -r redis && useradd -r -g redis redis
- USER redis
- RUN [ "redis-server" ]
在这种root文件系统中使用权限小的用户看起来还是很有用的。但是我喜欢直接操作root 哈哈哈哈、
挂载数据卷。从宿主机映射文件/文件夹到容器里。
volume 宿主机文件(文件夹):/容器文件(文件夹)
dockerfile的方式挂载数据卷的时候使用volume 关键字 但是只能生成随机的目录,不能生成指定的目录。
Dockerfie
官方文档:Dockerfile reference | Docker Documentation
Dockerfile
最佳实践文档:Best practices for writing Dockerfiles | Docker Documentation
Docker
官方镜像 Dockerfile
:GitHub - docker-library/docs: Documentation for Docker Official Images in docker-library
本篇博客摘自:前言 - Docker — 从入门到实践
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。