Dockerfile入门了解

镜像存在一个分层的概念,镜像的定制就是在一层层构建,Dockerfile是一个文本文件,每一条指令构建一层,描述每一层应该如何构建,每层构建的镜像可以被其他共享。

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

除了选择现有镜像为基础镜像外,Docker 还存在一个特殊的镜像,名为 scratch。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像

1
2
FROM scratch
...

想自己diy一个镜像,找个目录新建个空文件夹,空文件夹下新建Dockerfile,名字就得是这个,可以是小写,到时候就直接

1
docker build -t image_name:tag .

别忘了后面有个点,代表当前目录,也可在目录下新建.dockerignore配置忽略的文件,这样Docker CLI就不会把这些文件和目录给Docker服务

Dockerfile主要分为四个部分
1.基础镜像信息

1
FROM nginx //FROM mysql FROM nginx等均可

2.维护者信息

1
MAINTAINER docker_user docker_user@email.com

3.镜像操作指令

1
RUN echo '<h1>Hello Docker</h1>' > /usr/share/nginx/html/index.html

4.容器启动指令

1
CMD /usr/sbin/nginx

以上命令就完成了一个Dockerfile的编写,使用如下代码编译

1
docker build -t image_name:tag . //也可使用-f制定目录位置 docker build -t image_name:tag -f /path/to/a/Dockerfile .

如果你想打包一个你diy之后的容器就用下面这个打包成镜像

1
docker commit -a "" -m "" container_id image_name:tag

常用Dockfile指令列表

1.FROM

FROM用于指定基础镜像,Dockerfile所谓定制镜像,那也得一个镜像作为基础,FROM是必备指令而且必须是第一条指令,在 Docker Store 上有非常多的高质量的官方镜像,有可以直接拿来使用的服务类的镜像,如 nginxredismongomysqlhttpdphptomcat 等;也有一些方便开发、构建、运行各种语言应用的镜像,如 nodeopenjdkpythonrubygolang 等。可以在其中寻找一个最符合我们最终目标的镜像为基础镜像进行定制。

2.USER指定当前用户

USER切换到指定用户,用户必须是之前就建立好的,否则无法切换

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

3.WORKDIR指定当前工作目录

格式为 WORKDIR <工作目录路径>,如果目录不存在会帮你建立目录

常见错误像把Dockerfile当做Shell脚本来写

1
2
RUN cd /app
RUN echo "hello" > world.txt

镜像构建之后会发现根本找不到/app/world.txt,或者内容不对,原因很简单,在Shell中连续两行是一个进程执行环境,而在 Dockerfile 中,这两行 RUN 命令的执行环境根本不同,是两个完全不同的容器。这就是对 Dockerfile 构建分层存储的概念不了解所导致的错误 ,因此如果需要改变以后各层的工作目录的位置,那么应该使用 WORKDIR 指令。

4.COPY

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

1
2
3
4
COPY [--chown=<user>:<group>] <源路径>... <目标路径>
COPY package.json /usr/src/app/
COPY hom* /mydir/
COPY hom?.txt /mydir/

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

使用 COPY 指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等

5.ADD

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

如果原路径是个URL,它会通过URL下载下来。如果source是个压缩文件,它会自动解压缩到目标路径下

1
2
3
FROM scratch
ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz /
...

在 Docker 官方的 Dockerfile 最佳实践文档 中要求,尽可能的使用 COPY,因为 COPY 的语义很明确,就是复制文件而已,而 ADD 则包含了更复杂的功能,其行为也不一定很清晰。最适合使用 ADD 的场合,就是所提及的需要自动解压缩的场合。

6.ENV

格式有两种:

  • ENV <key> <value>
  • ENV <key1>=<value1> <key2>=<value2>...

比如

1
2
ENV VERSION=1.0 DEBUG=on \
NAME="Happy Feet"

定义环境变量和使用

1
2
3
4
5
6
7
8
9
ENV NODE_VERSION 7.2.0

RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \
&& curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \
&& gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \
&& grep " node-v$NODE_VERSION-linux-x64.tar.xz\$" SHASUMS256.txt | sha256sum -c - \
&& tar -xJf "node-v$NODE_VERSION-linux-x64.tar.xz" -C /usr/local --strip-components=1 \
&& rm "node-v$NODE_VERSION-linux-x64.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt \
&& ln -s /usr/local/bin/node /usr/local/bin/nodejs

7.ARG

构建参数和 ENV 的效果一样,都是设置环境变量。所不同的是,ARG 所设置的构建环境的环境变量,在将来容器运行时是不会存在这些环境变量的。但是不要因此就使用 ARG 保存密码之类的信息,因为 docker history 还是可以看到所有值的 .

Dockerfile 中的 ARG 指令是定义参数名称,以及定义其默认值。该默认值可以在构建命令 docker build 中用 --build-arg <参数名>=<值> 来覆盖。

8.EXPOSE

格式为 EXPOSE <端口1> [<端口2>...]

EXPOSE 指令是声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务。

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

9.VOLUME 定义匿名卷

格式为:

  • VOLUME ["<路径1>", "<路径2>"...]
  • VOLUME <路径>

容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中 .

Dockerfile 中,我们可以事先指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据。

1
VOLUME /data

default

这里的 /data 目录就会在运行时自动挂载为匿名卷,任何向 /data 中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。当然,运行时可以覆盖这个挂载设置

1
docker run -d -v mydata:/data xxxx

在这行命令中,就使用了 mydata 这个命名卷挂载到了 /data 这个位置,替代了 Dockerfile 中定义的匿名卷的挂载配置

10.CMD容器启动命令

CMD 指令的格式和 RUN 相似,多条CMD最后一条生效,也是两种格式:

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

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

1
2
3
CMD echo $HOME
## 实际执行上面这个指令会被替换成
CMD [ "sh", "-c", "echo $HOME" ]

??

10.ENTRYPOINT 入口点

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

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

1
<ENTRYPOINT> "<CMD>"

为什么有了CMD还有ENTRIPOINT?
CMD可以作为参数传递给ENTRYPOINT,就像我们jar包启动

1
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom", "-jar","/app.jar"]

docker run的时候结尾加上 –spring.profiles.active=test就可以指定环境

还有其他场景如下
创建一个获取当前公网IP的镜像

1
2
3
4
5
FROM ubuntu:16.04
RUN apt-get update \
&& apt-get install -y curl \
&& rm -rf /var/lib/apt/lists/*
CMD [ "curl", "-s", "http://ip.cn" ]

假如我们使用 docker build -t myip . 来构建镜像的话,如果我们需要查询当前公网 IP,只需要执行:

假如我们使用 docker build -t myip . 来构建镜像的话,如果我们需要查询当前公网 IP,只需要执行:

1
docker run myip

嗯,这么看起来好像可以直接把镜像当做命令使用了,不过命令总有参数,如果我们希望加参数呢?比如从上面的 CMD 中可以看到实质的命令是 curl,那么如果我们希望显示 HTTP 头信息,就需要加上 -i 参数。那么我们可以直接加 -i 参数给 docker run myip 么

1
2
docker run myip -i
docker: Error response from daemon: invalid header field value "oci runtime error: container_linux.go:247: starting container process caused \"exec: \\\"-i\\\": executable file not found in $PATH\"\n".

我们可以看到可执行文件找不到的报错,executable file not found。之前我们说过,跟在镜像名后面的是 command,运行时会替换 CMD 的默认值。因此这里的 -i 替换了原来的 CMD,而不是添加在原来的 curl -s http://ip.cn 后面。而 -i 根本不是命令,所以自然找不到。

使用ENTRYPOINT

1
2
3
4
5
FROM ubuntu:16.04
RUN apt-get update \
&& apt-get install -y curl \
&& rm -rf /var/lib/apt/lists/*
ENTRYPOINT [ "curl", "-s", "http://ip.cn" ]

重新构建镜像

1
2
3
4
5
6
7
8
9
10
11
12
13
docker run myip -i
HTTP/1.1 200 OK
Server: nginx/1.8.0
Date: Tue, 22 Nov 2016 05:12:40 GMT
Content-Type: text/html; charset=UTF-8
Vary: Accept-Encoding
X-Powered-By: PHP/5.6.24-1~dotdeb+7.1
X-Cache: MISS from cache-2
X-Cache-Lookup: MISS from cache-2:80
X-Cache: MISS from proxy-2_6
Transfer-Encoding: chunked
Via: 1.1 cache-2:80, 1.1 proxy-2_6:8006
Connection: keep-alive

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