Dockerfile

学习自:Dockerfile详解(超详细) - 简书

1、说明

Dockerfile是一个组合镜像命令的文本文件,其中存放了一系列指令,Docker通过这些指令自动生成镜像。

写好Dockerfile之后,通过指令docker build -t repository:tag 路径 即可构建,要求路径下存在Dockerfile文件

镜像是分层的,Dockerfile中每多一条指令,就多一层。用&&或\连接的多条指令仍视为一条指令。

2、规则

  1. 文件名是Dockerfile,如果目录下有不同名的dockerfile,可以用-f来指定编排的是哪个dockerfile;
  2. Dockerfile中用到的所有文件都和Dockerfile文件位于同级目录下
  3. Dockerfile中的相对路径默认都是Dockerfile所在目录;
  4. Dockerfile是分层结构,一行就是一层,构建镜像时是分层构建的,因此某些指令能写到一行就写到一行,减少层次;
  5. Dockerfile对指令大小写不敏感,但是指令都用大写;
  6. 非注释第一行一定是FROM;
  7. Dockerfile工作空间目录下支持隐藏文件(.dockerignore),类似git的.gitignore
  8. 一行写不下,可以用\来换行接着写

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

execCMD ["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这种任务,即使做完了我们也看不到,因为做完的一瞬间容器就销毁了。

 

posted @ 2024-04-16 13:47  ShineLe  阅读(77)  评论(0)    收藏  举报