构建镜像时把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像,那么之前提及的无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决。这个脚本就是 Dockerfile。
Dockerfile 是一个文本文件,其内包含了一条条的 指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。
官方技术文档: https://docs.docker.com/engine/reference/builder/
语法
FROM
第一个指令必须是FROM了,用于指定一个构建镜像的基础源镜像,镜像必须是已经存在的,如果本地没有就会从公共库中拉取,没有指定镜像的标签会使用默认的latest标签。
FROM <image> [AS <name>]
FROM <image>[:<tag>] [AS <name>]
FROM <image>[@<digest>] [AS <name>]
Docker 还存在一个特殊的镜像,名为 scratch
。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。
FROM scratch
...
如果你以 scratch
为基础镜像的话,意味着你不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。
LABEL
设置一组键值对,指定镜像的作者信息,版本信息
LABEL <key>=<value> <key>=<value> <key>=<value> ...
使用\ 来实现换行
LABEL description="This text illustrates \
that label-values can span multiple lines."
LABEL maintainer="NGINX Docker Maintainers <docker-maint@nginx.com>"
注意:之前的maintainer已经被docker所废弃 建议使用LABEL
RUN
指定当前镜像中运行的命令,一句RUN就是一层,也相当于一个版本。这就是之前说的缓存的原理。我们知道docker是镜像层是只读的,所以你如果第一句安装了软件,用完在后面一句删除是不可能的。所以这种情况要在一句RUN命令中完成,可以通过&符号连接多个RUN语句。
对于复杂的RUN请用反斜线换行,避免无用分分层,合并多条命令成一层。
RUN <command>#shell 模式
RUN ["executable","param1","param2"] #exec模式
FROM debian:stretch
RUN apt-get update
RUN apt-get install -y gcc libc6-dev make wget
RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz"
RUN mkdir -p /usr/src/redis
RUN tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1
RUN make -C /usr/src/redis
RUN make -C /usr/src/redis install
Dockerfile 中每一个指令都会建立一层,RUN
也不例外。每一个 RUN
的行为,就和刚才我们手工建立镜像的过程一样:新建立一层,在其上执行这些命令,执行结束后,commit
这一层的修改,构成新的镜像。
而上面的这种写法,创建了 7 层镜像。这是完全没有意义的,而且很多运行时不需要的东西,都被装进了镜像里,比如编译环境、更新的软件包等等。结果就是产生非常臃肿、非常多层的镜像,不仅仅增加了构建部署的时间,也很容易出错。 这是很多初学 Docker 的人常犯的一个错误。
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
就是编译、安装 redis 可执行文件。因此没有必要建立很多层,这只是一层的事情。因此,这里没有使用很多个 RUN
对一一对应不同的命令,而是仅仅使用一个 RUN
指令,并使用 &&
将各个所需命令串联起来。将之前的 7 层,简化为了 1 层。在撰写 Dockerfile 的时候,要经常提醒自己,这并不是在写 Shell 脚本,而是在定义每一层该如何构建。
这一组命令的最后添加了清理工作的命令,删除了为了编译构建所需要的软件,清理了所有下载、展开的文件,并且还清理了 apt
缓存文件。这是很重要的一步,我们之前说过,镜像是多层存储,每一层的东西并不会在下一层被删除,会一直跟随着镜像。因此镜像构建时,一定要确保每一层只添加真正需要添加的东西,任何无关的东西都应该清理掉。
设置时区
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
RUN echo 'Asia/Shanghai' >/etc/timezone
ENTRYPOINT
ENTRYPOINT的指令不会被docker run 运行的命令项所覆盖。如果需要覆盖ENTRYPOINT的指令,需要使用docker run —entrypoint
ENTRYPOINT <command> param1 param2 #shell 模式
ENTRYPOINT ["executable","param1","param2"] #exec模式
CMD
CMD会在启动容器run的时候提供默认的命令和参数,构建镜像build时不会不执行
如果用户启动容器run时指定了运行的命令,则会覆盖掉CMD指定的命令
CMD在Dockerfile中只能出现一次,有多个,只有最后一个会有效。
CMD <command> param1 param2 #shell 模式
CMD ["executable","param1","param2"] #exec模式
CMD ["param1","param2"] # 作为ENTRYPOINT指令的默认参数
当 ENTRYPOINT 与 CMD 同时给出时,CMD 中的内容会作为 ENTRYPOINT 定义命令的参数,最终执行容器启动的还是 ENTRYPOINT 中给出的命令。
ENV
设置容器的环境变量
EVN <key> <value> #只能设置一个
EVN <key>=<value>#允许一次设置多个
ARG
ARG指令用于设置构建参数,类似于ENV。和ARG不同的是,ARG设置的是构建时的环境变量,在容器运行时是不会存在这些变量的。创建一个Dockerfile文件 内容如下
FROM alpine
ARG runmode=PRO
ENV env=$runmode
CMD echo $env
执行dockerfile创建镜像baxiang/alpine
$ docker build -t baxiang/alpine --build-arg runmode=dev ./
创建容器,获取当前的环境变量参数
$ docker run -it baxiang/alpine sh
/ # echo $env
dev
在 1.13 之前的版本,要求 --build-arg
中的参数名,必须在 Dockerfile
中用 ARG
定义过了,换句话说,就是 --build-arg
指定的参数,必须在 Dockerfile
中使用了。如果对应参数没有被使用,则会报错退出构建。从 1.13 开始,这种严格的限制被放开,不再报错退出,而是显示警告信息,并继续构建。
$ docker build -t baxiang/alpine --build-arg ENV=dev ./
...
[Warning] One or more build-args [ENV] were not consumed
WORKDIR
为RUN、CMD、ENTRYPOINT、COPY和ADD设置工作目录,如果当前目录不存在会自动创建
ADD
复制本机文件或目录或远程文件,添加到指定的容器目录,支持GO的正则模糊匹配。路径是绝对路径,不存在会自动创建。如果源是一个目录,只会复制目录下的内容,目录本身不会复制。如果是URL或压缩包会自动下载或自动解压。
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 指令是个不错的选择。
COPY <src> <dest>
Expose
EXPOSE <端口1> [<端口2>...]。
EXPOSE
指令是声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务。在 Dockerfile 中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另一个用处则是在运行时使用随机端口映射时,也就是 docker run -P
时,会自动随机映射 EXPOSE
的端口。
要将 EXPOSE
和在运行时使用 -p <宿主端口>:<容器端口>
区分开来。-p
,是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问,而 EXPOSE
仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。
```
expose:
- "3000"
- "8000"
<a name="ySaCR"></a>
### VOLUME
容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中。为了防止运行时用户忘记将动态文件所保存目录挂载为卷,在 `Dockerfile` 中,我们可以事先指定某些目录挂载为匿名数据卷,这样在运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据。
```dockerfile
VOLUME ["<路径1>", "<路径2>"...]
VOLUME <路径>
下面的的 /data
目录就会在运行时自动挂载为匿名数据卷,任何向 /data
中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。 创建如下一个docker file 文件
FROM ubuntu:18.04
RUN mkdir /data
RUN echo "hello world" > /data/hi.txt
VOLUME /data
执行docker file 文件 并创建容器
$ docker build ./ -t baxiang/ubuntu
$docker run --name ubuntu1 baxiang/ubuntu
查看挂载的匿名数据圈本地位置docker inspect ubuntu1
"Mounts": [
{
"Type": "volume",
"Name": "f5c3d9f68d2de7bc5d269f106ae71688c35f650c2ce0dd030a9954eb8095f717",
"Source": "/var/lib/docker/volumes/f5c3d9f68d2de7bc5d269f106ae71688c35f650c2ce0dd030a9954eb8095f717/_data",
"Destination": "/data",
"Driver": "local",
"Mode": "",
"RW": true,
"Propagation": ""
}
],
执行
sudo cat /var/lib/docker/volumes/f5c3d9f68d2de7bc5d269f106ae71688c35f650c2ce0dd030a9954eb8095f717/_data/hi.txt
hello world
当然,运行时可以覆盖这个挂载设置。执行:-v mydata:/data,就使用了本地 mydata
这个命名卷挂载到了 /data
这个位置,替代了 Dockerfile
中定义的匿名卷的挂载配置。
docker run --name ubuntu2 -v ~/mydata:/data baxiang/ubuntu
查看挂载情况,这个时候数据卷/data 目录映射的是本地宿主机的。mydata 目录 注意原先在容器中的hi.txt 被隐藏掉无法在查看到了
"Mounts": [
{
"Type": "bind",
"Source": "/home/baxiang/mydata",
"Destination": "/data",
"Mode": "",
"RW": true,
"Propagation": "rprivate"
}
],
USER
USER
指定运行容器时的用户名或UID,后续的RUN、CMD、ENTRYPOINT也会使用指定用户
build 命令
docker build * 命令用于使用 Dockerfile 创建镜像;Dockerfile可以是本地的、也可以是在线的、自定义的;
语法
docker build [OPTIONS] PATH | URL | -
OPTIONS参数说明:
--build-arg=[] :设置镜像创建时的变量;
--cpu-shares :设置 cpu 使用权重;
--cpu-period :限制 CPU CFS周期;
--cpu-quota :限制 CPU CFS配额;
--cpuset-cpus :指定使用的CPU id;
--cpuset-mems :指定使用的内存 id;
--disable-content-trust :忽略校验,默认开启;
-f :指定要使用的Dockerfile路径;
--force-rm :设置镜像过程中删除中间容器;
--isolation :使用容器隔离技术;
--label=[] :设置镜像使用的元数据;
-m :设置内存最大值;
--memory-swap :设置Swap的最大值为内存+swap,"-1"表示不限swap;
--no-cache :创建镜像的过程不使用缓存;
--pull :尝试去更新镜像的新版本;
--quiet, -q :安静模式,成功后只输出镜像 ID;
--rm :设置镜像成功后删除中间容器;
--shm-size :设置/dev/shm的大小,默认值是64M;
--ulimit :Ulimit配置。
--tag, -t: 镜像的名字及标签,通常 name:tag 或者 name 格式;可以在一次构建中为一个镜像设置多个标签。
--network: 默认 default。在构建期间设置RUN指令的网络模式
创建文件名是Dockerfile的文本
FROM ubuntu:16.04
MAINTAINER baxiang "yangyucug@gmail.com"
RUN apt-get update
RUN apt-get install -y nginx
COPY index.html /var/www/html/
EXPOSE 80
ENTRYPOINT ["/usr/sbin/nginx","-g","daemon off;"]
修改当前nginx首页,copy到nginx 首页目录下面
<html>
<head><title>hello docker</title></head>
<body>
<h1>welcome to baxiang webside</h1>
</body>
</html>
当前目录下执行docker build
docker build -t='baxiang/nginx' .
运行当前镜像
docker run -d --name nginx_web -p 80 baxiang/nginx
查看当前镜像
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
97df2b9ff4a1 baxiang/nginx "nginx '-gdaemon off…" 7 seconds ago Up 6 seconds 0.0.0.0:32768->80/tcp nginx_web
执行curl
curl 127.0.0.1:32768
多阶段构建(multi-stage build)
在Docker17.05版本之后现增加的功能,主要用于解决docker镜像构建的中间冗余文件的处理,
https://docs.docker.com/develop/develop-images/multistage-build/
创建main.c 文件
#include <stdio.h>
int main(){
printf("hello world\n");
return 0;
}
创建Dockerfile
FROM gcc:latest
WORKDIR /usr/src/helloworld
COPY . .
RUN gcc -o helloworld main.c
CMD ["./helloword"]
$ docker build -t baxiang/helloworld ./
Sending build context to Docker daemon 4.608kB
...
Successfully tagged baxiang/helloworld:latest
查看最终编译的镜像文件,一段代码
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
baxiang/helloworld latest 550b1161f582 About a minute ago 1.14GB
gcc latest d757f913db32 36 hours ago 1.14GB
采用多阶段编译
FROM gcc:latest as build
WORKDIR /usr/src/helloworld
COPY . .
RUN gcc -static -o helloworld main.c
FROM scratch
WORKDIR /
COPY --from=build /usr/src/helloworld/helloworld ./
CMD ["./helloworld"]
查看镜像大小
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
baxiang/helloworld latest efdb9fd40b2e 40 minutes ago 938kB
golang
FROM golang:latest as build
ENV GO111MODULE=off
ENV GOBUILDPATH=github.com/baxiang/hello-go
RUN mkdir -p /go/src/${GOBUILDPATH}
COPY ./ /go/src/${GOBUILDPATH}
RUN cd /go/src/${GOBUILDPATH} && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go install -v
FROM scratch
WORKDIR /baxiang
COPY --from=build /go/bin/hello-go ./hello-go
EXPOSE 8000
CMD ["./hello-go"]
docker build ./ -t baxiang/hello-go:0.1
Sending build context to Docker daemon 7.385MB
Step 1/4 : FROM scratch
--->
Step 2/4 : ADD main /main
---> 7450d15e31f1
Step 3/4 : EXPOSE 8000
---> Running in 4b7651956bc7
Removing intermediate container 4b7651956bc7
---> dcc5411ed957
Step 4/4 : CMD ["/main"]
---> Running in b69234b915cc
Removing intermediate container b69234b915cc
---> 639b373dd7b6
Successfully built 639b373dd7b6
✗ docker images |grep hello-go
baxiang/hello-go 0.1 639b373dd7b6 About a minute ago 7.38MB
只构建某一阶段的镜像
我们可以使用 as
来为某一阶段命名,例如
FROM golang:1.9-alpine as builder
例如当我们只想构建 builder
阶段的镜像时,增加 --target=builder
参数即可
$ docker build --target builder -t username/imagename:tag .
构建时从其他镜像复制文件
上面例子中我们使用 COPY --from=0 /go/src/github.com/go/helloworld/app .
从上一阶段的镜像中复制文件,我们也可以复制任意镜像中的文件。
$ 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