Dockerfile
1、说明
Dockerfile是一个组合镜像命令的文本文件,其中存放了一系列指令,Docker通过这些指令自动生成镜像。
写好Dockerfile之后,通过指令docker build -t repository:tag 路径 即可构建,要求路径下存在Dockerfile文件。
镜像是分层的,Dockerfile中每多一条指令,就多一层。用&&或\连接的多条指令仍视为一条指令。
2、规则
- 文件名是Dockerfile,如果目录下有不同名的dockerfile,可以用-f来指定编排的是哪个dockerfile;
- Dockerfile中用到的所有文件都和Dockerfile文件位于同级目录下;
- Dockerfile中的相对路径默认都是Dockerfile所在目录;
- Dockerfile是分层结构,一行就是一层,构建镜像时是分层构建的,因此某些指令能写到一行就写到一行,减少层次;
- Dockerfile对指令大小写不敏感,但是指令都用大写;
- 非注释的第一行一定是FROM;
- Dockerfile工作空间目录下支持隐藏文件(.dockerignore),类似git的.gitignore
- 一行写不下,可以用\来换行接着写
3、指令
1)FROM:基础镜像
语法:FROM <image>:<tag> [as new_name]
例子:
#使用私有仓库镜像 FROM 172.30.138.23/base/nginx_2024v1:latest FROM registry.company.com/internal/base-image-v1.0 #使用官方镜像 FROM ubuntu:22.04 FROM nginx:alpine #使用空镜像 FROM scratch
说明
- FROM是非注释首行代码;
- 为该镜像文件指定基础镜像,后续指令都基于该基础镜像环境运行;
- 基础镜像可以是任何一个已存在的镜像文件;
- as new_name是可选的,常用于长期工程、多阶段构建(有利于减少镜像大小)
- 通过--from other_name 使用,例如COPY --from oter_name。
- 如果无需任何镜像,可以用FROM scratch,构建“从零开始”的极简镜像
2)LABEL:镜像描述信息
语法
#① LABEL K1=V1 K2=V2 #② LABEL K1=V1 LABEL K2=V2 #③ LABEL K1=V1 \ K2=V2
例子
LABEL author="zp wang <test@qq.com>" LABEL describe="test image"
说明
- V一般情况下是带引号字符串;
- LABEL长用于以K-V对的形式给镜像添加一些META信息;
- 可以用于替代MAINTAINER指令;
- 会集成基础镜像中的LABEL,如果二者存在同名K,则新K覆盖老K。
3)MAINTAINER:作者信息
语法:MAINTAINER 姓名 <邮箱>
例子
MAINTAINER Shine Le <test@qq.com>
说明:其功能正逐渐被LABEL替代
4)COPY:从本机复制文件到镜像中
用途:
从本机复制文件到镜像中,每个镜像在运行起来之后,都有其独立的文件系统结构,就像一个虚拟机一样。
使用COPY指令可以把本机中的文件复制到镜像中的指定目录下。
语法:COPY src dst
例子
COPY startServer.sh /app/bin/startServer.sh
说明
关于src
- src是本机路径,且为以Dockerfile所在目录为起始的相对路径;
- src的路径必须是以Dockerfile所在的目录为起始,不能选到Dockerfile的父目录;
- 如果src是目录,那么其下的所有文件、子目录都会递归复制到dst,但src目录自身不会复制;
- 因此要把某个目录复制到镜像中的同名目录下,dst中还要把该目录名给加上;
- 如果指定了多个src,那么dst必须是目录,且以/结尾;
关于dst
- dst为容器中的路径,每个容器都像一个虚拟机,其中的文件有独立的文件结构和路径;
- 如果dst不存在,那么会自动递归创建;
- dst末尾加不加/在大多数时候没啥区别,比如/app/和/app。当/app为一个存在的目录时,两者效果相同;当/app不存在时,则会直接创建一个名为/app的文件,而非目录,这显然不符合预期,因此复制文件到目录时,目标路径末尾通常加/
5)ADD:从本机复制文件到镜像
用途:
同COPY,但是ADD支持将tar文件解压缩
语法:ADD src dst
说明
- 如果src是一个压缩文件,那么会被解压为一个目录;
- 如果src是一个URL,那么会通过URL下载一个文件;
- 如果src是多个,那么dst必须是以/结尾的目录,否则src会被视为一个普通文件。
6)WORKDIR:设置(容器)工作目录
用途:
类似cd命令,改变(容器中的)当前工作目录。
默认情况下,容器命令执行时的当前目录为/,可以通过WORKDIR改变当前目录。
之后的RUN、CMD、ENTRYPOINT、COPY、ADD都会将该目录作为当前工作目录。
语法:WORKDIR 绝对路径
例子
WORKDIR /opt
说明
- 如果路径中的某个目录不存在会自动创建,包括它的父目录;
- 一个Dockerfile中的WORKDIR可以出现多次,也可以为相对路径,这里的相对是指相对于前一个WORKDIR;
- 可以在WORKDIR中调用ENV指定的变量。
7)ENV:设置环境变量
语法
ENV K1 V1
ENV K1=V1 K2=V2 K3=V3 ……
环境变量的使用方式(下文的K是上文K-V定义时的K)
- $K
- ${K}
- ${K:-default V}
- ${K:+default V}
例子
#定义 ENV NGINX_VERSION 1.16.1 #使用 RUN cd /usr \ && tar -zxvf nginx_${NGINX_VERSION}.tar.gz
8)USER:设置启动容器的用户
语法:USER username
例子
USER dcos
说明
- 如果整个Dockerfile中没有USER指令,那么整个镜像中的指令都是以root权限构建、运行的
- USER指令一经设置,就会持续作用于后续所有指令,直到被另一个USER指令覆盖。这意味着从该USER开始,后续的RUN、CMD、ENTRYPOINT、COPY、ADD指令都会以该用户身份执行;
- 如果Dockerfile的最后一条USER指定了非root用户,如USER dcos,则容器启动后(通过CMD、ENTRYPOINT),默认也会以该用户身份运行进程,而非root;这是一种常见的容器实践
- 权限;有时从root切到低权限用户(如dcos、miduser),若后续指令需要操作高权限文件时,可能会因权限不足而失败:
USER nginx # 错误:nginx 用户无权限写入 /etc/nginx/conf.d/ COPY nginx.conf /etc/nginx/conf.d/
解决方案:
- 在root用户下,完成上述高权限操作,确保低权限用户的操作不涉及高权限文件;
- 提前用chown指令为低权限用户赋予必要权限(如RUN chown -R nginx:nginx /etc/nginx)
9)RUN:构建镜像时指定的命令
语法
- shell形式:RUN 指令1 && 指令2 && ……
- exec形式:RUN ["executable" , "参数1" , "参数2" , ……]
例子
#简单用法 #打印1和2 RUN echo 1 && echo 2 #常规用法 RUN chown -R dcos:docker /app/bin/startServer.sh /usr/nginx/conf #分行多条指令 RUN cd /usr \ && curl -O https://172.29.32.190:8081/nginx/${NGINX_VERSION}.tar.gz \ && tar -zxvf nginx/${NGINX_VERSIOIN}.tar.gz
说明
- RUN在下次构建期间,会优先查找本地缓存,如果不想用缓存可以用--no-cache解除:
docker build --no-cache
- shell形式:
- 默认用/bin/sh -c执行后续的命令;
- 可以用&&与\连接多个命令
- exec形式:
- exec形式会被解析为JSON序列,这意味着必须用双引号""
- 与shell不同,exec形式不会调用shell解析。但是exec形式可以运行在不包含shell命令的基础镜像中;
- 例如:
RUN ["echo","$HOME"]
;这样的指令$HOME
并不会被解析,必须RUN ["/bin/sh","-c","echo $HOME"]
10)EXPOSE:打开指定端口以实现与外部通信
语法:EXPOSE <port>/<protocol>
例子
EXPOSE 80 EXPOSE 80/http EXPOSE 2379/tcp
说明
- 不写protocol时,默认协议为tcp;
- 实际暴露时需要在docker run时用-P指定。
11)VOLUME:挂载,将宿主机目录挂载到容器中
语法:VOLUME 挂载路径 挂载点
例子:
VOLUME ["/data"] # [“/data”]可以是一个JsonArray ,也可以是多个值 VOLUME /var/log VOLUME /var/log /opt
说明
- 一般不用于Dockerfile,且在Kubernetes场景中几乎没用
12)CMD:为容器设置默认启动命令或参数
语法
#1 shell形式 CMD 指令 参1 参2 #2 exec形式 CMD ["executable","参1","参2"] #3 exec形式,但是只设置参数 CMD ["参1","参2"]
例子
CMD ["/app/bin/startServer.sh"]
说明
- CMD运行结束后容器将终止,CMD可以被docker run后边的命令覆盖;
- 一个Dockerfile只有顺序向下的最后一个CMD生效;
- 三种语法:
shell,默认/bin/sh -c
- 此时运行为shell的子进程,可以使用shell的操作符(if、环境变量、?*通配符)
- 位于容器中的进程的PID !=1 ,这意味着该进程并不能接受到外部传入的停止信号docker stop;
exec,CMD ["executable","参数1",'参数2"]
- 不会以/bin/sh -c运行(非shell子进程),因此不支持shell操作符;
- 若运行的命令依赖shell特性,可以手动启动 CMD ["/bin/sh","-c","executable","参1","参2",……]
exec,CMD ["参1","参2"]
-
- 一般结合ENTRYPOINT指令使用。
关于CMD的执行时机以及与其他指令之间的关系
镜像构建(docker build)与容器运行(docker run)是两个不同的阶段,CMD指令在build期间不会执行,只做记录,CMD的实际执行是在docker run期间。但是CMD之外的指令都是在docker build执行完毕(这些指令出问题会导致build失败,而CMD出问题不会使build失败,只会导致run失败)。
Dockerfile中除了CMD之外所有指令(如FROM、RUN、COPY),都是在build期间顺序执行。在此期间CMD只是把自己定义的内容,如["nginx","-g","daemon off;"]写入到镜像的元数据中,作为容器启动时的默认命令。
因此就算在CMD之后还有指令,例如:
# 3. 构建阶段:CMD 不执行,只记录“容器启动时要运行 nginx” CMD ["nginx", "-g", "daemon off;"] # 4. 构建阶段:CMD 之后的指令,依然会正常执行(比如清理缓存) RUN rm -rf /var/lib/apt/lists/*
看起来后边的RUN rm -rf是在CMD之后,但是却是在build期间就已经执行掉了这条语句,从而把缓存清理干净。
结论:普通指令(非CMD)在build期间就执行完毕且固化到镜像的文件系统中,成为镜像的一部分;CMD指令仅会被记录到镜像元数据中,不执行。当进入到run环节才会执行CMD。更极端的情况下,你甚至可以把CMD指令放在第一句,但是为了保证规范性、可读性,通常(或者说必须)把它放在最后一句。
13)ENTRYPOINT:为容器指定默认运行程序或命令
用途
与CMD类似,但存在区别,主要用于指定启动的父进程的PID=1。
用法
#1 shell形式 ENTRYPOINT command #2 execz形式 ENTRYPOINT ["/bin/bash","参1","参2"]
说明
- ENTRYPOINT设置默认命令不会被docker run命令行指定的参数覆盖,指定的参数会被传递给ENTRYPOINT指定的程序;
- docker run命令的--entrypoint选项可以覆盖ENTRYPOINT指令指定的程序;
- 一个Dockerfile中可以有多个ENTRYPOINT,但只有最后一个生效;
- ENTRYPOINT主要用于启动父进程,后边的参数会被当做子进程来启动。
4、补充
1)Dockerfile非注释首行是FROM,代表它的基础镜像。但是首个基础镜像的FROM是什么?
由于FROM是构建镜像上下文的开始,因此必须存在。
基础镜像的FROM指令是FROM scratch,表示使用一个空镜像。
2)CMD指令执行的脚本结束后,容器随之退出?
是的,如果CMD执行的.sh脚本运行完毕后没有持续运行的前台进程(如tail -f、服务程序等),那么容器会在脚本执行结束后立即退出。
根源在于:Docker容器生命周期与“主进程”(即CMD、ENTRYPOINT启动的进程)绑定。当主进程结束时,容器也会随之停止。
具体解释
Docker容器本质上是“围绕主进程运行的隔离环境”,当主进程执行完毕(退出),容器就失去了存在的意义,会自动终止。
因此如果.sh脚本中只有一些一次性命令(如文件复制、配置修改、短暂的任务处理等),没有启动任何持续运行的前台服务(如nginx -g "daemon off"、python -m http.server等),则:
- 脚本执行完最后一条指令,sh进程会退出;
- 容器检测到sh进程退出,会立即停止(状态变为Exited)。
如果需要容器在sh脚本执行完毕后继续执行,可以在脚本末尾添加一个“阻塞进程”,例如:
#script.sh #...其他命令... #脚本末尾的阻塞命令(保持主进程运行) tail -f /dev/null #持续读取空设备 #或 sleep infinity #无线休眠
一次性容器的用途
通常情况下见到的容器用途是“长期跑服务”,但实际运维中,大量场景需要用容器执行一次性脚本,用完即走,这些场景包括:
- 数据备份/迁移
- 代码编译/构建
- 定时任务(crontab、k8s CronJob):每日定时清理垃圾文件
这些任务一般伴随着存储卷、网络、输出到宿主机从而使外界感知到容器运行前后文件的变化,否则如果只是做类似echo abc这种任务,即使做完了我们也看不到,因为做完的一瞬间容器就销毁了。