Docker从入门到精通<6>-使用Dockerfile构建镜像
Dockerfile自动构建docker镜像
Dockerfile 语法格式
ENV
格式:ENV <key> <value>或ENV <key>=<value> ...
ENV指令可以为镜像创建出来的容器声明环境变量。并且在Dockerfile中,ENV指令声明的环境变量会被后面的特定指令(即ENV、ADD、COPY、WORKDIR、EXPOSE、VOLUME、USER)解释使用。其他指令使用环境变量时,使用格式为$variable_name或者${variable_name}。在变量前面添加斜杠\可以转义,如\$foo或者\${foo},将会被分别转换为$foo和${foo},而不是环境变量所保存的值。另外,ONBUILD指令不支持环境替换。
FROM [--platform=<platform>] <image>[@<digest>] [AS <name>] # 构建新镜像所依赖的基础镜像
RUN <command> | ["executable", "param1", "param2"]
执行命令,这里有两种方式,第一种默认直接调用系统shell,第二种会转换为json,所以一定要用双引号。如果命令比较长可以使用反斜杠进行换行处理。
与shell形式不同,exec形式不调用命令 shell。这意味着不会发生正常的 shell 处理。例如,
RUN [ "echo", "$HOME" ]
不会对 进行变量替换$HOME
。如果你想要 shell 处理,那么要么使用shell形式,要么直接执行 shell,例如:RUN [ "sh", "-c", "echo $HOME" ]
. 当使用 exec 形式并直接执行 shell 时,就像 shell 形式一样,是 shell 进行环境变量扩展,而不是 docker。如果命令中需要转义,需要用反斜杠进行转义。
ADD
ADD [--chown=<user>:<group>] <src>... <dest>
ADD [--chown=<user>:<group>] ["<src>",... "<dest>"]
把文件、目录、或者远程的文件的url,从src 拷贝到 镜像 的文件系统中的dest位置。
注意:
1. --chown 仅适用于linux系统。
2. src源文件等可以进行模糊匹配,指定go语言中的filepath.Match,支持 .*?
3. dest 的位置,是相对于WORKDIR的位置,比如WORKDIR 为/data/, dest为app/,那么实际上的dest路径为/data/app/
4. dest必须以斜杠结尾,否则将dest将被视为一个文件
5. 如果src为可识别的压缩格式(identity、gzip、bzip2 或 xz)的压缩文件,复制到dest将被自动解压缩
COPY
COPY [--chown=<user>:<group>] <src>... <dest> COPY [--chown=<user>:<group>] ["<src>",... "<dest>"]
功能和ADD几乎相同,但是少了两种功能:
1. 不支持src为url
2. 不支持src为压缩归档文件的解压缩
不过一般推荐使用COPY,COPY更加透明
CMD
CMD指令有3种格式:
- CMD <command>(shell格式)
- CMD ["executable", "param1", "param2"](exec格式,推荐格式)
- CMD ["param1", "param2"](为ENTRYPOINT指令提供参数)
CMD指令提供容器运行时的默认值,这些默认值可以是一条指令,也可以是一些参数。一个Dockerfile中可以有多条CMD指令,但只有最后一条CMD指令有效。CMD ["param1", "param2"]格式是在CMD指令和ENTRYPOINT指令配合时使用的,CMD指令中的参数会添加到ENTRYPOINT指令中。使用shell和exec格式时,命令在容器中的运行方式与RUN指令相同。不同在于,RUN指令在构建镜像时执行命令,并生成新的镜像;CMD指令在构建镜像时并不执行任何命令,而是在容器启动时默认将CMD指令作为第一条执行的命令。如果用户在命令行界面运行docker run命令时指定了命令参数,则会覆盖CMD指令中的命令。
LABEL
<key>=<value> <key>=<value> <key>=<value> ... 构建完镜像后,我们可以通过
docker image inspect --format='' myimage 查看标签信息
MAINTAINER 作者信息,已经弃用,可以用label取代
EXPOSE
<port> [<port>/<protocol>...]
指定docker容器在运行时侦听的端口,还可以指定协议是TCP,还是UDP,默认为TCP,例如:EXPOSE 80/TCP, 无论这个参数如何设置docker -p 都将覆盖这个参数,-P表示可以设置随机端口。
ENTRYPOINT
ENTRYPOINT ["executable", "param1", "param2"] # 推荐使用格式
ENTRYPOINT command param1 param2
ENTRYPOINT指令和CMD指令类似,都可以让容器在每次启动时执行相同的命令,但它们之间又有不同。一个Dockerfile中可以有多条ENTRYPOINT指令,但只有最后一条ENTRYPOINT指令有效。当使用shell格式时,ENTRYPOINT指令会忽略任何CMD指令和docker run命令的参数,并且会运行在/bin/sh -c中。这意味着ENTRYPOINT指令进程为/bin/sh -c的子进程,进程在容器中的PID将不是1,且不能接受Unix信号。即当使用docker stop <container>命令时,命令进程接收不到SIGTERM信号。我们推荐使用exec格式,使用此格式时,docker run传入的命令参数会覆盖CMD指令的内容并且附加到ENTRYPOINT指令的参数中。从ENTRYPOINT的使用中可以看出,CMD可以是参数,也可以是指令,而ENTRYPOINT只能是命令;另外,docker run命令提供的运行命令参数可以覆盖CMD,但不能覆盖ENTRYPOINT.
VOLUME
VOLUME ["/data"]
该
VOLUME
指令创建一个具有指定名称的挂载点,并将其标记为保存来自本机主机或其他容器的外部挂载卷点。该值可以是 JSON 数组、VOLUME ["/var/log/"]
或带有多个参数的纯字符串,例如VOLUME /var/log
或VOLUME /var/log /var/db, 就是创建了一个目录,其实跟mkdir 目录差不多,用的并不多
USER
USER <user>[:<group>]
或者USER <UID>[:<GID>]
所述
USER
指令集运行的image和用于任何时要使用的用户名(或UID)和任选的所述用户组(或GID)RUN
,CMD
和ENTRYPOINT
它后面的指令Dockerfile
注意:当用户没有主要组时,映像(或下一个说明)将与该
root
组一起运行
WORKDIR
WORKDIR 指令为 Dockerfile 中跟随它的任何 RUN、CMD、ENTRYPOINT、COPY 和 ADD 指令设置工作目录。 如果 WORKDIR 不存在,即使它没有在任何后续 Dockerfile 指令中使用,它也会被创建。
WORKDIR 指令可以在 Dockerfile 中多次使用。 如果提供了相对路径,它将相对于前一个 WORKDIR 指令的路径。 例如:
WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd
最终
pwd
命令的输出Dockerfile
将是/a/b/c
.
ONBUILD
(when combined with one of the supported instructions above)
用的比较少
Dockerfile编写经验总结:
-
在构建Dockefile镜像时最好加上标语识别的标签,docker buit -t python3.8-flask:v1 .
-
在选择基础镜像的时候,尽量选择小的镜像,便于快速构建,快速发布;前期或者测试环境为了方便排错,可以选择大一点的镜像作为基础镜像。
-
充分利用缓存。Docker daemon会顺序执行Dockerfile中的指令,而且一旦缓存失效,后续命令将不能使用缓存。为了有效地利用缓存,需要保证指令的连续性,尽量将所有Dockerfile文件中相同的部分都放在前面,而将不同的部分放在后面
-
不要在Dockerfile中做端口映射,否则一台机器上只能部署一个容器实例。因为在Dockerfile中做完端口映射,相当用Hostport被占用了。
-
CMD和ENTRYPOINT指令 CMD和ENTRYPOINT指令指定了容器运行的默认命令,推荐二者结合使用。使用exec格式的ENTRYPOINT指令设置固定的默认命令和参数,然后使用CMD指令设置可变的参数。
-
为了使Dockerfile易读、易理解和可维护,在使用比较长的RUN指令时可以使用反斜杠\分隔多行。大部分使用RUN指令的场景是运行apt-get命令,在该场景下请注意如下几点。❏ 不要在一行中单独使用指令RUN apt-get update。当软件源更新后,这样做会引起缓存问题,导致RUN apt-get install指令运行失败。所以,RUNapt-get update和RUN apt-get install应该写在同一行,如RUN apt-getupdate && apt-get install -y package-bar package-foo package-baz。❏ 避免使用指令RUN apt-get upgrade和RUN apt-get dist-upgrade。因为在一个无特权的容器中,一些必要的包会更新失败。如果需要更新一个包(如foo),直接使用指令RUN apt-get install -y foo。在Docker的核心概念中,提交镜像是廉价的,镜像之间有层级关系,像一颗树。不要害怕镜像的层数过多,我们可以在任一层创建一个容器。因此,不要将所有的命令写在一个RUN指令中。RUN指令分层符合Docker的核心概念,这很像源码控制。
-
正确使用ADD与COPY指令 尽管ADD和COPY用法和作用很相近,但COPY仍是首选。COPY相对于ADD而言,功能简单够用。COPY仅提供本地文件向容器的基本复制功能。ADD有额外的一些功能,比如支持复制本地压缩包(复制到容器中会自动解压)和URL远程资源。因此,ADD比较符合逻辑的使用方式是ADD roots.tar.gz /。当在Dockerfile中的不同部分需要用到不同的文件时,不要一次性地将这些文件都添加到镜像中去,而是在需要时逐个添加,这样也有利于充分利用缓存。另外,考虑到镜像大小的问题,使用ADD指令去获取远程URL中的压缩包不是推荐的做法。应该使用RUN wget或RUN curl代替。这样可以删除解压后不再需要的文件,并且不需要在镜像中再添加一层。
示例1:2048小游戏
我们首先把2048这个小游戏的项目,下载到本地, 并编写Dockerfile
[root@vm1 ~]# mkdir /root/docker/2048 -p [root@vm1 ~]# [root@vm1 ~]# cd /root/docker/2048/ [root@vm1 2048]# ls [root@vm1 2048]# [root@vm1 2048]# wget https://github.com/gabrielecirulli/2048/archive/refs/heads/master.zip ... 2021-07-15 16:20:54 (1.03 MB/s) - “master.zip” 已保存 [332945] [root@vm1 2048]# ls master.zip [root@vm1 2048]# unzip master.zip ... [root@vm1 2048]# mv 2048-master 2048 [root@vm1 2048]# cat Dockerfile FROM nginx:1.20 RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime RUN echo 'Asia/Shanghai' >/etc/timezone COPY 2048 /usr/share/nginx/html/ EXPOSE 80
注意这里的Dockerfile
FROM nginx:1.20 # 基础镜像 RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime # 设置容器时区信息 RUN echo 'Asia/Shanghai' >/etc/timezone COPY 2048 /usr/share/nginx/html/ # 把项目代码部署到nginx server EXPOSE 80 # 监听80端口
因为nginx官方本身已经设置好了,入口启动命令,所以我们无需再进行配置。
开始构建镜像
[root@vm1 2048]# docker build -t nginx-2048 . [+] Building 1.0s (9/9) FINISHED => [internal] load build definition from Dockerfile 0.2s => => transferring dockerfile: 258B 0.0s => [internal] load .dockerignore 0.3s => => transferring context: 2B 0.0s => [internal] load metadata for docker.io/library/nginx:1.20 0.0s => [1/4] FROM docker.io/library/nginx:1.20 0.0s => [internal] load build context 0.2s => => transferring context: 4.17kB 0.0s => CACHED [2/4] RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime 0.0s => CACHED [3/4] RUN echo 'Asia/Shanghai' >/etc/timezone 0.0s => CACHED [4/4] COPY 2048 /usr/share/nginx/html/ 0.0s => exporting to image 0.3s => => exporting layers 0.0s => => writing image sha256:747803a902f250a62eb3996ca5c0478835ff4884df4ac2163f2e7fff1c4287cc 0.0s => => naming to docker.io/library/nginx-2048 0.0s [root@vm1 2048]# docker images |egrep nginx-2048 nginx-2048 latest 747803a902f2 15 minutes ago 134MB [root@vm1 2048]# docker run -d -p 80:80 --name=nginx-2048-server nginx-2048 83d6be13b4b1f581a7afd057e4988125a1df812771f0a86a26ae30c130ffbdcf [root@vm1 2048]# [root@vm1 2048]# docker ps |egrep 'nginx-2048-server' 83d6be13b4b1 nginx-2048 "/docker-entrypoint.…" 21 seconds ago Up 19 seconds 0.0.0.0:80->80/tcp, :::80->80/tcp nginx-2048-server
开始测试
示例2:定制化编译nginx镜像
FROM ubuntu:20.04 LABEL "author"="linuxyn linuxyn@163.com" COPY source.list /etc/apt/source.list RUN apt update && apt install -y iproute2 ntpdate tcpdump telnet traceroute nfs-kernel-server nfs-common lrzsz tree openssl libssl-dev libpcre3 libpcre3-dev zlib1g-dev gcc openssh-server iotop unzip zip make vim && mkdir /data/nginx -p ADD nginx-1.20.2.tar.gz /usr/local/src RUN cd /usr/local/src/nginx-1.20.2 && ./configure --prefix=/apps/nginx --user=nginx --group=nginx --with-file-aio --with-http_stub_status_module --with-http_ssl_module --with-http_flv_module --with-http_gzip_static_module && make && make install && ln -sv /apps/nginx/sbin/nginx /usr/bin && rm -rf /usr/local/src/nginx-1.20.2 && rm -rf /usr/local/src/nginx-1.20.2.tar.gz ADD nginx.conf /apps/nginx/conf/nginx.conf ADD localtime /etc/localtime RUN ln -sv /dev/stdout /apps/nginx/logs/access.log RUN ln -sv /dev/stderr /apps/nginx/logs/error.log
# 大公司生产环境中一般都有一个统一的用户和组,这样容器中服务的用户在调用的时候才不会纠结权限的问题 RUN groupadd -g 2022 nginx && useradd -g nginx -s /usr/sbin/nologin -u 2022 nginx && chown -R nginx:nginx /apps/nginx /data/nginx EXPOSE 80 443 # CMD ["/apps/nginx/sbin/nginx", "-g", "daemon off;"] # 上面的CMD等价于下面的两行写法,把CMD中值当成参数传递给了ENTRYPOINT CMD ["-g", "daemon off;"] ENTRYPOINT ["/apps/nginx/sbin/nginx"]
在mysql的官方镜像中,我们也发现了这么一段写法
ENTRYPOINT ["docker-entrypoint.sh"] EXPOSE 3306 33060 CMD ["mysqld"] # 即把CMD中的mysqld当成参数传递给了ENTRYPOINT