如何编写正确高效的Dockerfile
Dockerfile是什么
Dockerfile
非常普通,它就是一个纯文本,里面记录了一系列的构建指令,比如选择基础镜像、拷贝文件、运行脚本等等,RUN
, COPY
, ADD
指令都会生成一个 Layer
,而 Docker
顺序执行这个文件里的所有步骤,最后就会创建出一个新的镜像出来。
一个简单的 Dockerfile
实例:
# Dockerfile.busybox
FROM busybox # 选择构建使用的基础镜像
CMD echo "hello world" # 启动镜像默认运行的命令
CMD
CMD
,它指定 docker run
启动容器时默认运行的命令,上面我们使用了 echo
命令,输出“hello world”字符串。
FROM
构建镜像的第一条指令必须是 FROM
,所以基础镜像的选择非常关键。如果关注的是镜像的安全和大小,那么一般会选择 Alpine;如果关注的是应用的运行稳定性,那么可能会选择 Ubuntu、Debian、CentOS。
FROM alpine:3.15 # 选择Alpine镜像
FROM ubuntu:bionic # 选择Ubuntu镜像
我们在本机上开发测试时会产生一些源码、配置等文件,需要打包进镜像里,这时可以使用 COPY
命令,它的用法和 Linux 的 cp
差不多,不过拷贝的源文件必须是“构建上下文”路径里的,不能随意指定文件。也就是说,如果要从本机向镜像拷贝文件,就必须把这些文件放到一个专门的目录,然后在 docker build
里指定“构建上下文”到这个目录才行。
这里有两个 COPY
命令示例,你可以看一下:
COPY ./a.txt /tmp/a.txt # 把构建上下文里的a.txt拷贝到镜像的/tmp目录
COPY /etc/hosts /tmp # 错误!不能使用构建上下文之外的文件
RUN
Dockerfile
里最重要的一个指令 RUN
,它可以执行任意的 Shell 命令,比如更新系统、安装应用、下载文件、创建目录、编译程序等等,实现任意的镜像构建步骤,非常灵活。
RUN
通常会是 Dockerfile
里最复杂的指令,会包含很多的 Shell 命令,但 Dockerfile
里一条指令只能是一行,所以有的 RUN
指令会在每行的末尾使用续行符 \
,命令之间也会用 &&
来连接,这样保证在逻辑上是一行,就像下面这样:
RUN apt-get update \
&& apt-get install -y \
build-essential \
curl \
make \
unzip \
&& cd /tmp \
&& curl -fSL xxx.tar.gz -o xxx.tar.gz\
&& tar xzf xxx.tar.gz \
&& cd xxx \
&& ./config \
&& make \
&& make clean
有的时候在 Dockerfile
里写这种超长的 RUN
指令很不美观,而且一旦写错了,每次调试都要重新构建也很麻烦,所以你可以采用一种变通的技巧:把这些 Shell 命令集中到一个脚本文件里,用 COPY
命令拷贝进去再用 RUN
来执行:
COPY setup.sh /tmp/ # 拷贝脚本到/tmp目录
RUN cd /tmp && chmod +x setup.sh \ # 添加执行权限
&& ./setup.sh && rm setup.sh # 运行脚本然后再删除
ARG、ENV、EXPOSE
ARG
定义了基础镜像的名字(可以在FROM指令中使用),ENV
定义了两个环境变量
ARG
:镜像层的环境变量
EVN
:镜像层和容器层参数
ARG IMAGE_BASE="node"
ARG IMAGE_TAG="alpine"
ENV PATH=$PATH:/tmp
ENV DEBUG=OFF
EXPOSE
声明容器对外服务的端口号
EXPOSE
: 暴露容器内部端口给外部使用
EXPOSE 443 # 默认是tcp协议
EXPOSE 53/udp # 可以指定udp协议
RUN, COPY, ADD 会生成新的镜像层,其它指令只会产生临时层,不影响构建大小。所以 Dockerfile 里最好不要滥用这些指令,尽量精简合并,否则太多的层会导致镜像臃肿不堪。
docker build
Dockerfile
必须要经过 docker build
才能生效,命令行“docker”是一个简单的客户端,真正的镜像构建工作是由服务器端的“Docker daemon”来完成的,所以“docker”客户端就只能把“构建上下文”目录打包上传(显示信息 Sending build context to Docker daemon ),这样服务器才能够获取本地的这些文件。
但这个机制也会导致一些麻烦,如果目录里有的文件(例如 readme/.git/.svn 等)不需要拷贝进镜像,docker 也会一股脑地打包上传,效率很低。为了避免这种问题,你可以在“构建上下文”目录里再建立一个 .dockerignore
文件,语法与 .gitignore
类似,排除那些不需要的文件。
gitignore
下面是一个简单的示例,表示不打包上传后缀是“swp”“sh”的文件:
# docker ignore
*.swp
*.sh
另外关于 Dockerfile
,一般应该在命令行里使用 -f
来显式指定。但如果省略这个参数,docker build
就会在当前目录下找名字是 Dockerfile
的文件。所以,如果只有一个构建目标的话,文件直接叫“Dockerfile”是最省事的。
此时构建出来的镜像只有“IMAGE ID”没有名字,不是很方便。为此你可以加上一个 -t
参数,也就是指定镜像的标签(tag),这样 Docker 就会在构建完成后自动给镜像添加名字。当然,名字必须要符合一定的命名规范,用 : 分隔名字和标签,如果不提供标签默认就是“latest”。
docker inspect
命令 docker inspect
来查看镜像的分层信息,Docker 会检查是否有重复的层,如果本地已经存在就不会重复下载,如果层被其他镜像共享就不会删除,这样就可以节约磁盘和网络成本。
容器镜像是由多个只读的 Layer
构成的,同一个 Layer
可以被不同的镜像共享,减少了存储和传输的成本。
镜像分层的好处:可以重复使用未被改动的 layer
,每次修改打包镜像,只需重新构建被改动的部分。
完整的 Dockerfile 示例
# Dockerfile
# docker build -t ngx-app .
# docker build -t ngx-app:1.0 .
ARG IMAGE_BASE="nginx"
ARG IMAGE_TAG="1.21-alpine"
FROM ${IMAGE_BASE}:${IMAGE_TAG}
COPY ./default.conf /etc/nginx/conf.d/
RUN cd /usr/share/nginx/html \
&& echo "hello nginx" > a.txt
EXPOSE 8081 8082 8083
总结
- 创建镜像需要编写
Dockerfile
,写清楚创建镜像的步骤,每个指令都会生成一个Layer
。 Dockerfile
里,第一个指令必须是FROM
,用来选择基础镜像,常用的有 Alpine、Ubuntu 等。其他常用的指令有:COPY
、RUN
、EXPOSE
,分别是拷贝文件,运行 Shell 命令,声明服务端口号。docker build
需要用-f
来指定Dockerfile
,如果不指定就使用当前目录下名字是“Dockerfile”的文件。docker build
需要指定“构建上下文”,其中的文件会打包上传到Docker daemon
,所以尽量不要在“构建上下文”中存放多余的文件。- 创建镜像的时候应当尽量使用
-t
参数,为镜像起一个有意义的名字,方便管理。 Dockerfile
里的构建命令不区分大小写,例如“FROM”也可以写成“from”,但习惯上都使用大写形式。