您的位置:首页 > 运维架构 > Docker

使用Dockerfile定制镜像

2018-09-07 19:02 411 查看
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/benben_2015/article/details/82501305

镜像的定制实际上就是定制每一层所添加的配置、文件。我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本,这个脚本就叫做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="benben@csdn.com
。通过命令
$ 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文件中的指令含义是:

  1. 第一句是更新
    tzdata
    软件包和
    ca-certificates
  2. 第二句是为文件创建连接,这里的连接时符号连接(符号链接是一个新文件,而硬链接并没有建立新文件,当用
    ls -l
    命令列出文件时,可以看到符号链接名后有一个箭头指向源文件或目录。例如
    lrwxrwxrwx.  1 root root     35 Apr 20 15:45 localtime -> ../usr/share/zoneinfo/Asia/Shanghai
    ,最前面的
    l
    表示软连接);
  3. 第三句打印环境变量到
    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中的下一条指令,直到所有指令都执行完毕

参考文章:

  1. Dockerfile reference
  2. ln命令
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: