Docker 学习笔记(二):Dockerfile 定制镜像
镜像的定制实际上就是定制每一层所添加的配置、文件。
如果我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像,那么之前提及的无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决。
这个脚本就是 Dockerfile。
-
Dockerfile 是一个文本文件,其内包含了一条条的 指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。
-
因为每一条指令构建一层,而且每一层构建好后,就不会再变化。为了使镜像尽可能地小而且层次清晰,每一层都应该围绕一个特定的目标进行构建,并且在构建结束前,要清理掉所有缓存和其他无关的东西!
-
Docker 现在最多只能支持 127 层,尽量让每一条命令都完成一个完整的目标,不要每条 shell 命令都对应一个 RUN,这是相当糟糕的做法。
在撰写 Dockerfile 的时候,要经常提醒自己,这并不是在写 Shell 脚本,而是在定义每一层该如何构建。
一、commit 命令
docker commit
用法类似 git commit
,用于将当前容器层的修改,固化成一个新的镜像层。
# 1. 首先启动了一个容器
# 2. 通过 exec 命令登入该容器做一些修改
# 3. 可以使用下列命令查看容器层的具体改动
docker diff <container id>
# 4. 使用 commit 命令提交容器层的改动
docker commit [选项] <容器ID或容器名> [<仓库名>[:<标签>]] # 模板
docker commit \
--author "Tao Wang <twang2218@gmail.com>" \
--message "修改了默认网页" \
webserver \
nginx:v2
# 5. 使用 history 命令查看镜像的构建历史
docker history nginx:v2
利用 Dockerfile 定制镜像,其实就是在基础镜像上启动一个临时容器,然后在该容器上一条条地运行 Dockerfile 内的指令。
每跑完一个指令,就将当前的修改固化层一个新的镜像层(这就类似在此时执行 docker commit
)。
指令跑完了,一个分层的镜像也就生成了,这时再清除掉构建用的临时容器。
二 、Dockerfile 指令详解
Dockerfile 常用的有十多个指令:
FROM
:指定基础镜像LABEL
:镜像的一些标签,如 maintainer/licenceUSER
:能用普通用户,就不要用 root 来做。建议使用普通用户来运行不需要 root 权限的服务。ENV
:设置环境变量,可用于设置 PATH 或者其他环境变量。- 运行容器时,可以通过
--env XXX=xxx
来设置或者修改环境变量。--env
可多次重复使用 - 密码/密钥等参数的默认值可设置在这里,在运行时自行修改。
- 运行容器时,可以通过
ARG
:构建时的参数,只在构建期有用。(而 ENV 就相当于运行期参数)- 可通过
docker build -build-arg <varname>=<value> xxx
来修改构建参数。 - 一般用于设置一些依赖的版本号、镜像源的地址等。构建时根据这些参数从镜像源下载对应的依赖。
- 可通过
WORKDIR
:用于制定下一个镜像层的工作目录(容器内部的),类比cd xxx
。- 可多次使用,这样每一个镜像层都可以用不同的工作目录。
- 如果路径不存在,会直接创建该路径
ADD/COPY
:都是添加文件的命令,更推荐使用 COPY,ADD 最好只用在tar.gz/tar.xz
等文件的添加上(会自动解压)。- 需要下载的文件,建议使用 curl/wget
- COPY 可以用于从别的镜像复制文件(常用于多阶段构建)
RUN
:最常用的构建指令,会创建新的镜像层,所以最好让每条 RUN 命令都完成一个目标的构建,减少层数。VOLUME
:指定数据层挂载点。- 常用:
VOLUME ["/data", "/var/log/"]
- 常用:
EXPOSE
:暴露端口。- 该指令只制定了容器需要暴露的端口。在 run 时还需要用
-p xx:xx
做端口映射,才能和本机的端口绑定!
- 该指令只制定了容器需要暴露的端口。在 run 时还需要用
ENTRYPOINT
:镜像的“入口”,也就是启动镜像时会执行的命令。- 格式:
ENTRYPOINT ["executable", "param1", "param2"]
docker run
命令的所有其他参数,都会被当作 "入口"命令的参数传入!
- 格式:
CMD
:在不使用ENTRYPOINT
的情况下,它就是镜像的默认命令。- 格式:
CMD ["executable","param1","param2"]
- 在使用
ENTRYPOINT
的情况下,CMD
建议设置为CMD ["--help"]
,并且紧跟在ENTRYPOINT
命令之后。
- 格式:
ONBUILD
:该指令适合用在基础镜像的构建中。- 如果
FROM
一个使用了ONBUILD
指令的镜像,会先执行该指令,然后才执行 Dockerfile 里面的指令。
- 如果
需要注意的是,现在只有 RUN/ADD/COPY
这三条指令,才会创建新的镜像层。其他的指令只会在构建过程中创建临时镜像层,它们不会出现在最终的镜像中。
1. FROM 指定基础镜像
所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。
在 Docker Hub 上有非常多的高质量的官方镜像,有可以直接拿来使用的服务类的镜像,如 nginx
、redis
、mongo
、mysql
、httpd
、php
、tomcat
等;也有一些方便开发、构建、运行各种语言应用的镜像,如 node
、openjdk
、python
、ruby
、golang
等。可以在其中寻找一个最符合我们最终目标的镜像为基础镜像进行定制。
如果没有找到对应服务的镜像,官方镜像中还提供了一些更为基础的操作系统镜像,如 ubuntu
、debian
、centos
、fedora
、alpine
等,这些操作系统的软件库为我们提供了更广阔的扩展空间。
除了选择现有镜像为基础镜像外,Docker 还存在一个特殊的镜像,名为 scratch
。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。
FROM scratch
...
如果你以 scratch
为基础镜像的话,意味着你不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。
不以任何系统为基础,直接将可执行文件复制进镜像的做法并不罕见,比如 swarm
、coreos/etcd
。对于 Linux 下静态编译的程序来说,并不需要有操作系统提供运行时支持,所需的一切库都已经在可执行文件里了,因此直接 FROM scratch
会让镜像体积更加小巧。使用 Go 语言 开发的应用很多会使用这种方式来制作镜像,这也是为什么有人认为 Go 是特别适合容器微服务架构的语言的原因之一。
镜像构建上下文
docker build --tag <image name>:tag .
中的 .
并不仅仅指 Dockerfile 的路径!
build 命令的最后一个参数,是镜像构建上下文的路径,这个路径可以是文件夹路径,可以是一个 tar 压缩包,也可以是一个 url,甚至 git 仓库地址也是支持的。
Docker 是 Client/Server 模式的程序,build 命令会将该 [文件夹/tar 压缩包/url] 的内容发送给 Server 端(Docker 引擎)用于构建,
因此后面构建中的 COPY/ADD
指令,只能使用上下文里面的内容,更不支持 ../xxx
这样的路径。
三、多阶段构建
多阶段构建中,不同的阶段使用不同的基础镜像(因此有多个 FROM),前面的阶段大都是为了生成一些需要的文件(前后端编译等)。
在最后一个阶段,使用 COPY
将需要的文件从前几个阶段生成的镜像中 COPY 过来,这样就得到了一个只包含运行时的镜像。
前端编译基于前端相关的镜像,后端用后端的编译镜像,最后放到只包含运行时的镜像里。
FROM golang:1.9-alpine as builder
RUN apk --no-cache add git
WORKDIR /go/src/github.com/go/helloworld/
RUN go get -d -v github.com/go-sql-driver/mysql
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
FROM alpine:latest as prod
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=0 /go/src/github.com/go/helloworld/app .
CMD ["./app"]
附
一些 Jenkins 构建,可供参考/使用的 Dockerfile:
- Slave: Jenkins Slave
- Python 3.7
- Miniconda3,Linux 通用,省心
- docker-alpine-python3,超小版本
- Android 构建环境:Docker Android Build Box
- 大合集:mritd/dockerfile