构建镜像时把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像,那么之前提及的无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决。这个脚本就是 Dockerfile。
Dockerfile 是一个文本文件,其内包含了一条条的 指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。
官方技术文档: https://docs.docker.com/engine/reference/builder/

语法

FROM

第一个指令必须是FROM了,用于指定一个构建镜像的基础源镜像,镜像必须是已经存在的,如果本地没有就会从公共库中拉取,没有指定镜像的标签会使用默认的latest标签。

  1. FROM <image> [AS <name>]
  2. FROM <image>[:<tag>] [AS <name>]
  3. FROM <image>[@<digest>] [AS <name>]

Docker 还存在一个特殊的镜像,名为 scratch。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。

  1. FROM scratch
  2. ...

如果你以 scratch 为基础镜像的话,意味着你不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。

LABEL

设置一组键值对,指定镜像的作者信息,版本信息

  1. LABEL <key>=<value> <key>=<value> <key>=<value> ...

使用\ 来实现换行

  1. LABEL description="This text illustrates \
  2. that label-values can span multiple lines."
  1. LABEL maintainer="NGINX Docker Maintainers <docker-maint@nginx.com>"

注意:之前的maintainer已经被docker所废弃 建议使用LABEL

RUN

指定当前镜像中运行的命令,一句RUN就是一层,也相当于一个版本。这就是之前说的缓存的原理。我们知道docker是镜像层是只读的,所以你如果第一句安装了软件,用完在后面一句删除是不可能的。所以这种情况要在一句RUN命令中完成,可以通过&符号连接多个RUN语句。
对于复杂的RUN请用反斜线换行,避免无用分分层,合并多条命令成一层。

  1. RUN <command>#shell 模式
  2. RUN ["executable","param1","param2"] #exec模式
  1. FROM debian:stretch
  2. RUN apt-get update
  3. RUN apt-get install -y gcc libc6-dev make wget
  4. RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz"
  5. RUN mkdir -p /usr/src/redis
  6. RUN tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1
  7. RUN make -C /usr/src/redis
  8. RUN make -C /usr/src/redis install

Dockerfile 中每一个指令都会建立一层,RUN 也不例外。每一个 RUN 的行为,就和刚才我们手工建立镜像的过程一样:新建立一层,在其上执行这些命令,执行结束后,commit 这一层的修改,构成新的镜像。
而上面的这种写法,创建了 7 层镜像。这是完全没有意义的,而且很多运行时不需要的东西,都被装进了镜像里,比如编译环境、更新的软件包等等。结果就是产生非常臃肿、非常多层的镜像,不仅仅增加了构建部署的时间,也很容易出错。 这是很多初学 Docker 的人常犯的一个错误。

  1. FROM debian:stretch
  2. RUN buildDeps='gcc libc6-dev make wget' \
  3. && apt-get update \
  4. && apt-get install -y $buildDeps \
  5. && wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" \
  6. && mkdir -p /usr/src/redis \
  7. && tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \
  8. && make -C /usr/src/redis \
  9. && make -C /usr/src/redis install \
  10. && rm -rf /var/lib/apt/lists/* \
  11. && rm redis.tar.gz \
  12. && rm -r /usr/src/redis \
  13. && apt-get purge -y --auto-remove $buildDeps

就是编译、安装 redis 可执行文件。因此没有必要建立很多层,这只是一层的事情。因此,这里没有使用很多个 RUN 对一一对应不同的命令,而是仅仅使用一个 RUN 指令,并使用 && 将各个所需命令串联起来。将之前的 7 层,简化为了 1 层。在撰写 Dockerfile 的时候,要经常提醒自己,这并不是在写 Shell 脚本,而是在定义每一层该如何构建。

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

  1. RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
  2. RUN echo 'Asia/Shanghai' >/etc/timezone

ENTRYPOINT

ENTRYPOINT的指令不会被docker run 运行的命令项所覆盖。如果需要覆盖ENTRYPOINT的指令,需要使用docker run —entrypoint

  1. ENTRYPOINT <command> param1 param2 #shell 模式
  2. ENTRYPOINT ["executable","param1","param2"] #exec模式

CMD

CMD会在启动容器run的时候提供默认的命令和参数,构建镜像build时不会不执行
如果用户启动容器run时指定了运行的命令,则会覆盖掉CMD指定的命令
CMD在Dockerfile中只能出现一次,有多个,只有最后一个会有效。

  1. CMD <command> param1 param2 #shell 模式
  2. CMD ["executable","param1","param2"] #exec模式
  3. CMD ["param1","param2"] # 作为ENTRYPOINT指令的默认参数

当 ENTRYPOINT 与 CMD 同时给出时,CMD 中的内容会作为 ENTRYPOINT 定义命令的参数,最终执行容器启动的还是 ENTRYPOINT 中给出的命令。

ENV

设置容器的环境变量

  1. EVN <key> <value> #只能设置一个
  2. EVN <key>=<value>#允许一次设置多个

ARG

ARG指令用于设置构建参数,类似于ENV。和ARG不同的是,ARG设置的是构建时的环境变量,在容器运行时是不会存在这些变量的。创建一个Dockerfile文件 内容如下

  1. FROM alpine
  2. ARG runmode=PRO
  3. ENV env=$runmode
  4. CMD echo $env

执行dockerfile创建镜像baxiang/alpine

  1. $ docker build -t baxiang/alpine --build-arg runmode=dev ./

创建容器,获取当前的环境变量参数

  1. $ docker run -it baxiang/alpine sh
  2. / # echo $env
  3. dev

在 1.13 之前的版本,要求 --build-arg 中的参数名,必须在 Dockerfile 中用 ARG 定义过了,换句话说,就是 --build-arg 指定的参数,必须在 Dockerfile 中使用了。如果对应参数没有被使用,则会报错退出构建。从 1.13 开始,这种严格的限制被放开,不再报错退出,而是显示警告信息,并继续构建。

  1. $ docker build -t baxiang/alpine --build-arg ENV=dev ./
  2. ...
  3. [Warning] One or more build-args [ENV] were not consumed

WORKDIR

为RUN、CMD、ENTRYPOINT、COPY和ADD设置工作目录,如果当前目录不存在会自动创建

ADD

复制本机文件或目录或远程文件,添加到指定的容器目录,支持GO的正则模糊匹配。路径是绝对路径,不存在会自动创建。如果源是一个目录,只会复制目录下的内容,目录本身不会复制。如果是URL或压缩包会自动下载或自动解压。

  1. ADD <src> <dest>

注意:
① src必须在构建的上下文内,不能使用例如:ADD ../somethine /something 这样的命令,因为docker build 命令首先会将上下文路径和其子目录发送到docker daemon。
② 如果src是一个URL,同时dest不以斜杠结尾,dest将会被视为文件,src对应内容文件将会被下载到dest。
③ 如果src是一个URL,同时dest以斜杠结尾,dest将被视为目录,src对应内容将会被下载到dest目录。
④ 如果src是一个目录,那么整个目录下的内容将会被拷贝,包括文件系统元数据。
⑤ 如果文件是可识别的压缩包格式,则docker会自动解压。

COPY

COPY除了不能自动解压,也不能复制网络文件。其它功能和ADD相同。但对于那些不希望源文件被解压或没有网络请求的场景,COPY 指令是个不错的选择。

  1. COPY <src> <dest>

Expose

  1. EXPOSE <端口1> [<端口2>...]。

EXPOSE 指令是声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务。在 Dockerfile 中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另一个用处则是在运行时使用随机端口映射时,也就是 docker run -P 时,会自动随机映射 EXPOSE 的端口。
要将 EXPOSE 和在运行时使用 -p <宿主端口>:<容器端口> 区分开来。-p,是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问,而 EXPOSE 仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。

  1. ```
  2. expose:
  3. - "3000"
  4. - "8000"
  1. <a name="ySaCR"></a>
  2. ### VOLUME
  3. 容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中。为了防止运行时用户忘记将动态文件所保存目录挂载为卷,在 `Dockerfile` 中,我们可以事先指定某些目录挂载为匿名数据卷,这样在运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据。
  4. ```dockerfile
  5. VOLUME ["<路径1>", "<路径2>"...]
  6. VOLUME <路径>

下面的的 /data 目录就会在运行时自动挂载为匿名数据卷,任何向 /data 中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。 创建如下一个docker file 文件

  1. FROM ubuntu:18.04
  2. RUN mkdir /data
  3. RUN echo "hello world" > /data/hi.txt
  4. VOLUME /data

执行docker file 文件 并创建容器

  1. $ docker build ./ -t baxiang/ubuntu
  2. $docker run --name ubuntu1 baxiang/ubuntu

查看挂载的匿名数据圈本地位置docker inspect ubuntu1

  1. "Mounts": [
  2. {
  3. "Type": "volume",
  4. "Name": "f5c3d9f68d2de7bc5d269f106ae71688c35f650c2ce0dd030a9954eb8095f717",
  5. "Source": "/var/lib/docker/volumes/f5c3d9f68d2de7bc5d269f106ae71688c35f650c2ce0dd030a9954eb8095f717/_data",
  6. "Destination": "/data",
  7. "Driver": "local",
  8. "Mode": "",
  9. "RW": true,
  10. "Propagation": ""
  11. }
  12. ],

执行

  1. sudo cat /var/lib/docker/volumes/f5c3d9f68d2de7bc5d269f106ae71688c35f650c2ce0dd030a9954eb8095f717/_data/hi.txt
  2. hello world

当然,运行时可以覆盖这个挂载设置。执行:-v mydata:/data,就使用了本地 mydata 这个命名卷挂载到了 /data 这个位置,替代了 Dockerfile 中定义的匿名卷的挂载配置。

  1. docker run --name ubuntu2 -v ~/mydata:/data baxiang/ubuntu

查看挂载情况,这个时候数据卷/data 目录映射的是本地宿主机的。mydata 目录 注意原先在容器中的hi.txt 被隐藏掉无法在查看到了

  1. "Mounts": [
  2. {
  3. "Type": "bind",
  4. "Source": "/home/baxiang/mydata",
  5. "Destination": "/data",
  6. "Mode": "",
  7. "RW": true,
  8. "Propagation": "rprivate"
  9. }
  10. ],

USER

USER
指定运行容器时的用户名或UID,后续的RUN、CMD、ENTRYPOINT也会使用指定用户

build 命令

docker build * 命令用于使用 Dockerfile 创建镜像;Dockerfile可以是本地的、也可以是在线的、自定义的;

语法

  1. docker build [OPTIONS] PATH | URL | -

OPTIONS参数说明:

  1. --build-arg=[] :设置镜像创建时的变量;
  2. --cpu-shares :设置 cpu 使用权重;
  3. --cpu-period :限制 CPU CFS周期;
  4. --cpu-quota :限制 CPU CFS配额;
  5. --cpuset-cpus :指定使用的CPU id
  6. --cpuset-mems :指定使用的内存 id
  7. --disable-content-trust :忽略校验,默认开启;
  8. -f :指定要使用的Dockerfile路径;
  9. --force-rm :设置镜像过程中删除中间容器;
  10. --isolation :使用容器隔离技术;
  11. --label=[] :设置镜像使用的元数据;
  12. -m :设置内存最大值;
  13. --memory-swap :设置Swap的最大值为内存+swap"-1"表示不限swap
  14. --no-cache :创建镜像的过程不使用缓存;
  15. --pull :尝试去更新镜像的新版本;
  16. --quiet, -q :安静模式,成功后只输出镜像 ID
  17. --rm :设置镜像成功后删除中间容器;
  18. --shm-size :设置/dev/shm的大小,默认值是64M
  19. --ulimit :Ulimit配置。
  20. --tag, -t: 镜像的名字及标签,通常 name:tag 或者 name 格式;可以在一次构建中为一个镜像设置多个标签。
  21. --network: 默认 default。在构建期间设置RUN指令的网络模式

创建文件名是Dockerfile的文本

  1. FROM ubuntu:16.04
  2. MAINTAINER baxiang "yangyucug@gmail.com"
  3. RUN apt-get update
  4. RUN apt-get install -y nginx
  5. COPY index.html /var/www/html/
  6. EXPOSE 80
  7. ENTRYPOINT ["/usr/sbin/nginx","-g","daemon off;"]

修改当前nginx首页,copy到nginx 首页目录下面

  1. <html>
  2. <head><title>hello docker</title></head>
  3. <body>
  4. <h1>welcome to baxiang webside</h1>
  5. </body>
  6. </html>

当前目录下执行docker build

  1. docker build -t='baxiang/nginx' .

运行当前镜像

  1. docker run -d --name nginx_web -p 80 baxiang/nginx

查看当前镜像

  1. docker ps
  2. CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
  3. 97df2b9ff4a1 baxiang/nginx "nginx '-gdaemon off…" 7 seconds ago Up 6 seconds 0.0.0.0:32768->80/tcp nginx_web

执行curl

  1. curl 127.0.0.1:32768

多阶段构建(multi-stage build)

在Docker17.05版本之后现增加的功能,主要用于解决docker镜像构建的中间冗余文件的处理,
https://docs.docker.com/develop/develop-images/multistage-build/

创建main.c 文件

  1. #include <stdio.h>
  2. int main(){
  3. printf("hello world\n");
  4. return 0;
  5. }

创建Dockerfile

  1. FROM gcc:latest
  2. WORKDIR /usr/src/helloworld
  3. COPY . .
  4. RUN gcc -o helloworld main.c
  5. CMD ["./helloword"]
  1. $ docker build -t baxiang/helloworld ./
  2. Sending build context to Docker daemon 4.608kB
  3. ...
  4. Successfully tagged baxiang/helloworld:latest

查看最终编译的镜像文件,一段代码

  1. $ docker images
  2. REPOSITORY TAG IMAGE ID CREATED SIZE
  3. baxiang/helloworld latest 550b1161f582 About a minute ago 1.14GB
  4. gcc latest d757f913db32 36 hours ago 1.14GB

采用多阶段编译

  1. FROM gcc:latest as build
  2. WORKDIR /usr/src/helloworld
  3. COPY . .
  4. RUN gcc -static -o helloworld main.c
  5. FROM scratch
  6. WORKDIR /
  7. COPY --from=build /usr/src/helloworld/helloworld ./
  8. CMD ["./helloworld"]

查看镜像大小

  1. $ docker images
  2. REPOSITORY TAG IMAGE ID CREATED SIZE
  3. baxiang/helloworld latest efdb9fd40b2e 40 minutes ago 938kB

golang

  1. FROM golang:latest as build
  2. ENV GO111MODULE=off
  3. ENV GOBUILDPATH=github.com/baxiang/hello-go
  4. RUN mkdir -p /go/src/${GOBUILDPATH}
  5. COPY ./ /go/src/${GOBUILDPATH}
  6. RUN cd /go/src/${GOBUILDPATH} && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go install -v
  7. FROM scratch
  8. WORKDIR /baxiang
  9. COPY --from=build /go/bin/hello-go ./hello-go
  10. EXPOSE 8000
  11. CMD ["./hello-go"]
  1. docker build ./ -t baxiang/hello-go:0.1
  2. Sending build context to Docker daemon 7.385MB
  3. Step 1/4 : FROM scratch
  4. --->
  5. Step 2/4 : ADD main /main
  6. ---> 7450d15e31f1
  7. Step 3/4 : EXPOSE 8000
  8. ---> Running in 4b7651956bc7
  9. Removing intermediate container 4b7651956bc7
  10. ---> dcc5411ed957
  11. Step 4/4 : CMD ["/main"]
  12. ---> Running in b69234b915cc
  13. Removing intermediate container b69234b915cc
  14. ---> 639b373dd7b6
  15. Successfully built 639b373dd7b6
  1. docker images |grep hello-go
  2. baxiang/hello-go 0.1 639b373dd7b6 About a minute ago 7.38MB

只构建某一阶段的镜像

我们可以使用 as 来为某一阶段命名,例如

  1. FROM golang:1.9-alpine as builder

例如当我们只想构建 builder 阶段的镜像时,增加 --target=builder 参数即可

  1. $ docker build --target builder -t username/imagename:tag .

构建时从其他镜像复制文件

上面例子中我们使用 COPY --from=0 /go/src/github.com/go/helloworld/app . 从上一阶段的镜像中复制文件,我们也可以复制任意镜像中的文件。

  1. $ COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf

时间修改

https://cloud.tencent.com/developer/article/1626811

参考

https://docs.docker.com/engine/reference/builder/
https://docs.docker.com/engine/reference/commandline/build/
https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
https://docker_practice.gitee.io/zh-cn/appendix/best_practices.html
https://github.com/docker-library/docs