镜像的定制实际上就是定制每一层所添加的配置、文件。我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本,这个脚本就叫做Dockerfile。
Dockerfile是一个文本文件,其内包含了一条条指令,每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。
创建Dockerfile
首先创建一个目录,然后在该目录里面创建Dockerfile文件。例如:
$ mkdir goweb
$ cd goweb
$ touch Dockerfile
保存Dockerfile
文件的目录,称之为构建环境,也叫构建上下文。Docker会在构建镜像时将构建上下文和该上下文中的文件和目录上传到Docker守护进程。这样Docker守护进程就能直接访问你想在镜像中存储的任何代码、文件或者其他数据。
以下面的文档为例来介绍如何使用:
FROM alpine
MAINTAINER benben <benben@csdn.com>
LABEL maintainer="benben <benben@csdn.com>"
# 原utc时间,修改成cst(中国标准东八区时间)
ENV TZ Asia/Shanghai
RUN apk --update add tzdata ca-certificates && ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime && \
echo ${TZ} > /etc/timezone
WORKDIR /opt/benben-project
COPY benbenProject benbenProject
RUN chmod 755 ./benbenProject
COPY conf/ ./conf
RUN mkdir logs
EXPOSE 8088
CMD ./benbenProject
FROM指定基础镜像
定制镜像,就是以一个镜像为基础,在其上进行定制。一个Dockerfile中FROM
是必不可少的指令,并且必须是第一条指令。用法如下:
FROM [AS ]
FROM [:] [AS ]
FROM [@] [AS ]
基础镜像可以是任何有效的镜像,通过从公共仓库中拉取镜像非常容易。本文的基础镜像是一个面向安全的轻型Linux发行版。
ARG
命令是Dockerfile中唯一可以在FROM
之前的指令,可以参阅随后的ARG和FROM如何交互。FROM
可以在单个Dockerfile中多次出现以创建多个映像,或者使用一个构建层作为另一个构建层的依赖。只需要在每个新的FROM
指令之前记下最后一个commit镜像的id,因为每个FROM
指令会清除事先所有指令创建的状态。tag
和digest
值是可选的,如果省略,则构建镜像时,会默认采用latest
标签。如果找不到,则构建器会返回错误。
除了选择现有镜像为基础镜像外,Docker还存在一个特殊的镜像,scratch
。这个镜像是虚拟的概念,实际并不存在,它表示一个空白的镜像。如果你以scratch
为基础镜像的话,意味着你不以任何镜像为基础。接下来的指令将作为镜像第一层开始。
ARG和FROM如何交互
FROM
指令支持在第一个FROM
之前出现任何ARG
指令声明的变量。例如:
ARG CODE_VERSION=latest
FROM base:${CODE_VERSION}
CMD /code/run-app
FROM extras:${CODE_VERSION}
CMD /code/run-extras
在FROM
指令之前的ARG
指令位于构建阶段之外,因此在FROM
命令之后,是不能使用那些环境变量的。上面指令的第二行可以使用变量CODE_VERSION
,,但第五行就不能使用。因此要想使用在第一个FROM
之前声明的ARG
的默认值,需要使用在构建阶段的ARG
。
ARG VERSION=latest
FROM busybox:$VERSION
ARG VERSION
RUN echo $VERSION > image_version
ARG构建参数
ARG
指令的效果和ENV
的效果一样,都是设置环境变量。不同的是,ARG
设置的构建环境的环境变量,在将来容器运行时是不会存在的。但是不要因此保存密码之类的信息,因为通过docker history
命令还是可以看到所有值的。
用法ARG <name>[=<default value>]
,该命令定义参数名称以及其默认值。默认值可以在构建命令docker build
中用--build-arg <参数名>=<值>
来覆盖。如果事先用ARG
设置了默认值,但却在构建时没有覆盖,那么构建就会使用默认值。
注意:ARG
变量在Dockerfile中定义的那行开始生效,而不是从命令行或其他地方使用处才开始。例如:
FROM busybox
USER ${user:-some_user}
ARG user
USER $user
用户在构建时在命令行输入$ docker build --build-arg user=what_user
,那么值是如何传递的呢?
首先第二行的USER
值为some_user
,接着第三行定义了用户变量,第四行的USER
在定义user
时,其值通过命令行传递,得到what_user
。在
FROM
指令支持在第一个FROM
之前出现任何ARG
指令声明的变量,ARG
指令定义之前,对变量的任何使用都会导致空字符串。
你可以使用ARG
或者ENV
指令来指定RUN
指令可用的变量,使用ENV
指令定义的环境变量始终会覆盖ARG
指令定义的同名变量。例如:
FROM ubuntu
ARG CONT_IMG_VER
ENV CONT_IMG_VER v1.0.0
RUN echo $CONT_IMG_VER
接着在命令行输入$ docker build --build-arg CONT_IMG_VER=v2.0.1
。在这种情况下,RUN
指令将会使用v1.0.0
而不是通过ARG
设置的v2.0.1
。
MAINTAINER(废弃)
MAINTAINER
命令用来指定该镜像的作者,用法MAINTAINER <name>
。LABEL
命令是一个更灵活的版本,同样可以指定镜像的作者,推荐使用这个,官方已弃用这个命令。因为这个命令可以设置你需要的任何元数据,而且可以轻松查看。如果想通过LABEL
命令来设置和MAINTAINER
字段对应的标签,你可以这样使用:LABEL maintainer="[email protected]
。通过命令$ docker inspect [OPTIONS] NAME|ID [NAME|ID...]
来查看其他标签,例如$ docker inspect b92f7d0bb745
。
LABEL
LABEL
命令会添加元数据到镜像,用法:LABEL <key>=<value> <key>=<value> <key>=<value> ...
。如果要在LABEL
值中包含空格,需要使用引号和反斜杠,就像在命令行解析中一样。实例如下:
LABEL "com.example.vendor"="ACME Incorporated"
LABEL com.example.label-with-value="foo"
LABEL version="1.0"
LABEL description="This text illustrates \
that label-values can span multiple lines."
镜像可以有多个标签,在Docker 1.10
之前,你可以在一行中指定多个标签。这减小了最终
镜像的大小,但现在不再是这种情况了。你仍然可以选择在单个指令中指定多个标签,方法有以下两种:
LABEL multi.label1="value1" multi.label2="value2" other="value3"
LABEL multi.label1="value1" \
multi.label2="value2" \
other="value3"
基础镜像或父镜像中的标签会被后添加的镜像继承,如果标签已存在但具有不同的值,那么后添加的值会覆盖原来设定的值。
"Labels":{
"maintainer": "benben <benben@csdn.com>"
"com.example.vendor": "ACME Incorporated",
"com.example.label-with-value": "foo",
"version": "1.0",
"description": "This text illustrates that label-values can span multiple lines.",
"multi.label1": "value1",
"multi.label2": "value2",
"other": "value3"
}
注释
接着那一行为注释,Dockerfile中以#
开头的行都会被认为是注释。
ENV设置环境变量
ENV
指令将环境变量key的值设置为value,用法ENV <key> <value>
或者ENV <key>=<value>
。设置的环境变量将在后续的所有镜像中,同时也可以被替换。如果值中存在空格,则需要使用反斜杠。例如:
ENV myName="John Doe" myDog=Rex\ The\ Dog \
myCat=fluffy
上面的命令和下面的。
ENV myName John Doe
ENV myDog Rex The Dog
ENV myCat fluffy
当从生成的镜像运行容器时,使用ENV
设置的环境变量将保持不变。同样,可以使用docker inspect
命令来查看环境变量的值。
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"TZ=Asia/Shanghai"
],
注意:永久环境变量也可能会导致副作用。例如,设置ENV DEBIAN_FRONTEND noninteractive
可能会使apt-get
获取Debian
基础镜像的用户迷惑。要为单个命令设置值,使用RUN <key>=<value> <command>
。
RUN
RUN
命令会在当前镜像中运行指定的命令。具有两种形式:
RUN <command>
,(shell形式,RUN
指令会在shell里使用/bin/sh -c
来执行);RUN ["executable","param1","param2"]
,(exec形式,不支持shell的平台上运行或者不希望在shell中运行,也可以使用此格式)。
注意:与shell形式不同,exec格式不会自动调用shell命令,也就是说一般的shell命令是不能正常执行的。例如RUN ["echo", "$HOME"]
将不会对变量进行替换,如果想实现这样的结果,就需要这样RUN ["sh","-c","echo $HOME"]
。
注意:使用exec格式时,如果有路径,需要使用反斜杠来转义。不然不能正确解析,例如RUN ["c:\windows\system32\tasklist.exe"]
应该写成RUN ["c:\\windows\\system32\\tasklist.exe"]
。
示例Dockerfile文件中的指令含义是:
- 第一句是更新
tzdata
软件包和ca-certificates
; - 第二句是为文件创建连接,这里的连接时符号连接(符号链接是一个新文件,而硬链接并没有建立新文件,当用
ls -l
命令列出文件时,可以看到符号链接名后有一个箭头指向源文件或目录。例如lrwxrwxrwx. 1 root root 35 Apr 20 15:45 localtime -> ../usr/share/zoneinfo/Asia/Shanghai
,最前面的l
表示软连接); - 第三句打印环境变量到
timezone
文件。
由于Dockerfile中每一个指令都会建立一层,
RUN
也不例外。这里我用&&
将三条命令串联起来,是为了减少镜像层的创建。Union FS
最大层数限制是不超过127层,太多的层会使镜像非常臃肿,不仅增加了构建部署的时间,也容易出错。
同时Dockerfile支持shell类的行尾添加\
的命令换行方式,以及行首#
进行注释的格式。
示例Dockerfile文件中的RUN chmod 755 ./benbenProject
命令用来变更benbenProject
文件的权限,755表示:文件所有者可读可写可执行,与文件所有者同属一个用户组的其他用户可读可执行,其他用户组可读可执行。
示例Dockerfile文件中的RUN mkdir logs
命令用来创建一个logs目录。
WORKDIR指定工作目录
用法WORKDIR <工作目录路径>
,该命令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如果该目录不存在,WORKDIR
会帮你建立目录。
WORKDIR
指令可以在Dockerfile中多次使用,如果提供了相对路径,则它将相对于先前WORKDIR
指令的路径。
WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd
pwd
命令的输出结果将会是/a/b/c
。
WORKDIR
可以解析通过ENV
设置的环境变量,例如下面结果将是/path/$DIRNAME
。
ENV DIRPATH /path
WORKDIR $DIRPATH/$DIRNAME
RUN pwd
示例Dockerfile文件中定义工作目录为/opt/benben-project
。
COPY复制文件
COPY
指令将从构建上下文目录中<源路径>的文件/目录复制到新的一层镜像内的<目标路径>位置。命令具有两种形式:
COPY [--chown=<user>:<group>] <src>...<dest>
COPY [--chown=<user>:<group>] ["<src>",..."<dest>"]
,这种形式允许路径中有空格
如果有多个源路径,那么文件和目录的路径将被解释为相对于构建上下文的源。源路径在设置时只要满足Go的路径匹配规则就可以,例如:
COPY hom* /mydir/ #会添加所有以hom开头的文件
COPY hom?.txt /mydir/ #匹配任何以hom开头,以.txt结尾的文件
源路径如果是个目录,那么目录的所有内容都会被copy,包括文件系统的元数据。目录本身没有被复制,只是复制其内容。
目标路径可以是绝对路径或者相对于WORKDIR
的路径,例如:
COPY test relativeDir/ #会添加test文件到`WORKDIR`/relativeDir/目录
COPY test /absoluteDir/ #会田间test文件到/absoulteDir/目录
示例Dockerfile文件中的COPY benbenProject benbenProject
命令将会拷贝上下文的benbenProject文件到/opt/benben-project/benbenProject
。
EXPOSE声明端口
指令声明容器运行时提供的服务端口,这只是一个声明,在运行时并不会因为这个声明,应用就会开启这个端口的服务(处于安全的原因)。Docker并不会自动打开该端口,而是需要你在使用docker run -P
运行容器时来指定需要打开哪些端口。
命令格式EXPOSE <port> [<port>/<protocol>...]
,可以指定端口的协议是TCP还是UDP,如果未指定,则默认为TCP。例如:
EXPOSE 8088/tcp
EXPOSE 8088/udp
如果同时暴露TCP和UDP,那么使用docker run -P
,端口将针对TCP公开一次,针对UDP公开一次。尽管通过EXPOSE
可以设置,但是你也可以在运行时重写它们。例如:
docker run -p 8088:8088/tcp -p 8088:8088/udp ...
注意:要将EXPOSE
和在运行时使用docker run -p <宿主端口>:<容器端口>
区分开来,-p
是映射宿主端口和容器端口。换句话说,就是将容器的对应端口服务公开给外界访问,而EXPOSE
仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。
CMD容器启动命令
容器就是进程,既然是进程,那么在启动容器的时候,需要指定所运行的程序及参数。CMD
指令就是用于指定默认的容器主进程的启动命令的。
命令格式有三种形式:
CMD ["executable","param1","param2"]
,(exec格式,官方给推荐这种形式)CMD ["param1","param2"]
,(作为ENTRYPOINT
的默认参数)CMD command param1 param2
,(shell格式)
在一个Dockerfile中只能有一个CMD
指令,如果你设置了多条CMD
指令,那么实际起作用的只是最后一条。
CMD
的主要目的是为执行容器提供默认值,这些默认值可以包含可执行文件,也可以省略可执行文件,在这种情况下,你必须指定ENTRYPOINT
指令。如果CMD
用于为ENTRYPOINT
指令提供默认参数,则应使用json数组格式指定CMD
和ENTRYPOINT
指令。
注意:同样如前面所述,exec格式不能激活命令行,因此你想运行常用的shell命令,需要提前运行shell命令工具。例如:CMD ["sh","-c","echo $HOME"]
。这个shell是执行变量扩展的,而不是docker。
如果你希望容器每次都运行相同的可执行文件,那么应该考虑将ENTRYPOINT
与CMD
结合使用。
不要讲
RUN
与CMD
混淆,RUN
实际上是运行一个命令并提交结果,CMD
则在构建时不进行任何操作,它只是指定了镜像的预期命令。
整体执行讲解
Dockerfile中的指令会按顺序从上到下执行,所以应该根据需要合理安排指令的顺序。每条指令都会创建一个新的镜像层并对镜像进行提交。Docker大体上按照如下流程执行Dockerfile中的指令。
- Docker从基础镜像运行一个容器
- 执行一条指令,对容器做出修改
- 执行类似
docker commit
的操作,提交一个新的镜像层 - Docker再基于刚提交的镜像运行一个新容器
- 执行Dockerfile中的下一条指令,直到所有指令都执行完毕