参考文献

Dockerfile指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# 注释
# FROM <image>或者FROM<image>:<tag>
# 第一条指令必须为FROM指令.并且,如果在同一个Dockerfile中创建多个镜像时,可以使用多个FROM指令(每个镜像一次)
# FROM [--platform=<platform>] <image> [AS <name>]
# FROM [--platform=<platform>] <image>[:<tag>] [AS <name>]
# FROM [--platform=<platform>] <image>[@<digest>] [AS <name>]
FROM <image>

# 维护者信息
MAINTAINER <name>

# 镜像的操作指令
# RUN <command>或者RUN ["executable","param1","param2"]
# 前者将在shell终端中运行命令,即/bin/sh -c;
# 后者则使用exec执行.指定使用其他终端可以通过第二种方式实现,列如RUN ["/bin/bash","-c","echo hello"]
RUN <command>

# 容器启动是执行指令
# 支持三种格式:
# * CMD ["executable","param1","param2"]使用exec执行
# * CMD commad param1 param2在/bin/sh执行,提供给需要交互的应用
# * CMD ["param1","param2"]提供给ENTRYPOINT的默认参数.
# 指定启动容器时执行的命令,每个Dockerfile只能一条CMD命令.如果指定了多条命令,只有最后一条会被执行,如果用户启动容器时指定了运行的命令,则会覆盖掉CMD指定的命令
CMD <command>

# ----非必须----
# 告诉Docker服务端容器暴露的端口号,供互联系统使用.在启动容器时需要通过-P,Docker主机会自动分配一个端口转发到指定的端口;使用-p,则可以具体指定哪个本地端口映射过来
EXPOSE <port>[<port>....]

# 指定一个环境变量,会被后续RUN指令使用,并在容器运行时保持
# ENV KEY1=value1 KEY2=value2 KEY3=value3
ENV <key> <value>

# 拷贝本机文件或远程文件到镜像内
# 将复制指定的<src>到容器中的<dest>.
# 其中<src>可以是Dockerfile所在目录的一个相对路径(文件或目录);
# 也可以是一个URL;
# 还可以是一个tar文件(自动解压为目录)
# ADD <==> COPY+解压缩
ADD <src> <dest>

# 复制本地主机<src>(为Dockerfile所在目录的相对路径,文件或目录)为容器中的<dest>.若目标路径不存在是,会自动创建.当使用本地目录为源目录时,推荐使用COPY
COPY <src> <dest>

# 配置容器启动后执行的命令,并且不可被docker run 提供的参数覆盖.每个Dockerfile中只能有一个ENTRYPOINT,当指定多个ENTRYPOINT时,只有最后一个生效
# 两种格式:
# * ENTRYPOINT command param1 param2(shell中执行)
# * ENTRYPOINT ["executable","param1","param2"]
# 和CMD配合使用,CMD则作为完整命令的参数部分,ENTRYPOINT以JSON格式指定执行的命令部分。CMD可以为ENTRYPOINT提供可变参数,不需要变动的参数可以写在ENTRYPOINT里面。
# ENTRYPOINT ["/usr/bin/ls","-a"]
# CMD ["-l"]
ENTRYPOINT ["executable","param1","param2"]

# 创建一个可以从本地主机或者其他容器挂在的挂载点,一般用于存放数据库和需要保持的数据等
VOLUME ["/data"]

# 指定运行容器是的用户名或UID,后续RUN也会使用指定用户
# 当服务不需要管理员权限时,可以通过改名了指定改名了指定运行用户,并且可以在之前创建所需要的用户.列如 RUN groupadd -r postgress && useradd -r -g postgress postgress 要临时获取管理员权限可以使用gosu,而不推荐使用sudo
# USER user
# USER user:group
# USER uid
# USER uid:gid
# USER user:gid
# USER uid:group
USER daemon

# 为后续的RUN,CMD,ENTRYPOINT,COPY,ADD指定配置工作目录
# 可以使用多个WORKDIR指令,后续命令如果参数是相对路径,则会之前命令指定的路径
# WORKDIR /a
# WORKDIR b
# WORKDIR c
# RUN pwd
# 最终路径为/a/b/c
WORKDIR /path/to/workdir

# 配置当所创建的镜像为其他新创建的基础镜像时,所执行的操作指令,列如,Dockerfile使用如下的内容创建了镜像image-A
# ONBUILD ADD . /app/src
# ONBUILD RUN /usr/local/bin/python-build --dir /app/src
# 如果基于image-A创建新的镜像时,新的Dockerfile中使用FROM image-A指定基础镜像时,会自动执行ONBUILD指令内容,等价于在后面添加了两条指令
ONBUILD [INSTRUCTION]

RUN,CMD,ENTRYPOINT命令区别

  • RUN命令执行命令并创建新的镜像层,通常用于安装软件包
  • CMD命令设置容器启动后默认执行的命令及其参数,但CMD设置的命令能够被docker run命令后面的命令行参数替换
  • ENTRYPOINT配置容器启动时的执行命令(不会被忽略,一定会被执行,即使运行 docker run时指定了其他命令)

Dockerfile书写原则

  • 单一职责: 不同功能的应用应该尽量拆分为不同的容器,每个容器只负责单一业务进程

  • 提供注释信息: 晦涩难懂的代码尽量添加注释

  • 保持容器最小化: 应该避免安装无用的软件包

  • 合理选择基础镜像: 容器的核心是应用,只要基础镜像能够满足应用的运行环境即可

  • 使用.dockerignore文件忽略一些不需要做版本管理的文件

  • 尽量使用构建缓存

    • 从当前构建层开始,比较所有的子镜像,检查所有的构建指令是否与当前完全一致,如果不一致,则不使用缓存;
    • 一般情况下,只需要比较构建指令即可判断是否需要使用缓存,但是有些指令除外(例如ADDCOPY);
    • 对于ADD和COPY指令不仅要校验命令是否一致,还要为即将拷贝到容器的文件计算校验和(根据文件内容计算出的一个数值,如果两个文件计算的数值一致,表示两个文件内容一致 ),命令和校验和完全一致,才认为命中缓存.
    • 基于 Docker 构建时的缓存特性,我们可以把不轻易改变的指令放到 Dockerfile 前面(例如安装软件包),而可能经常发生改变的指令放在Dockerfile 末尾(例如编译应用程序)
  • 正确设置时区

    • Docker Hub 拉取的官方操作系统镜像大多数都是 UTC 时间(世界标准时间).如果想要在容器中使用中国区标准时间(东八区),请根据使用的操作系统修改相应的时区信息,常用操作系统的修改方式:

      1
      2
      3
      4
      5
      6
      Ubuntu 和Debian 系统可以向 Dockerfile 中添加以下指令:
      RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
      RUN echo "Asia/Shanghai" >> /etc/timezone

      CentOS 系统则向 Dockerfile 中添加以下指令:
      RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
  • 使用国内软件源加快镜像构建速度

  • 最小化镜像层数

    1
    2
    3
    4
    5
    6
    要在 CentOS 系统中安装make和net-tools两个软件包,应该在 Dockerfile 中使用以下指令:
    RUN yum install -y make net-tools

    而不应该写成这样:
    RUN yum install -y make
    RUN yum install -y net-tools

Dockerfile 指令书写建议

RUN
  • RUN指令在构建时将会生成一个新的镜像层并且执行RUN指令后面的内容.使用RUN指令时应该尽量遵循以下原则:

    • 当RUN指令后面跟的内容比较复杂时,建议使用反斜杠() 结尾并且换行;

    • RUN指令后面的内容尽量按照字母顺序排序,提高可读性.

    • 例如,在官方的 CentOS 镜像下安装一些软件,一个建议的 Dockerfile 指令如下:

    1
    2
    3
    4
    5
    FROM centos:7
    RUN yum install -y automake \
    curl \
    python \
    vim
CMD和ENTRYPOINT
  • CMDENTRYPOINT指令都是容器运行的命令入口,这两个指令使用中有很多相似的地方,但是也有一些区别.这两个指令的相同之处,CMDENTRYPOINT的基本使用格式分为两种.

    • 第一种为CMD/ENTRYPOINT["command","param"].这种格式是使用 Linux 的exec实现的, 一般称为exec模式,这种书写格式为CMD/ENTRYPOINT后面跟 json 数组,也是Docker 推荐的使用格式.
    • 另外一种格式为CMD/ENTRYPOINT command param,这种格式是基于 shell 实现的, 通常称为shell模式.当使用shell模式时,Docker 会以 /bin/sh -c command 的方式执行命令.
  • 使用 exec 模式启动容器时,容器的 1 号进程就是 CMD/ENTRYPOINT 中指定的命令,而使用 shell 模式启动容器时相当于我们把启动命令放在了 shell 进程中执行,等效于执行 /bin/sh -c "task command" 命令.因此 shell 模式启动的进程在容器中实际上并不是 1 号进程.

  • 这两个指令的区别:

    • Dockerfile 中如果使用了ENTRYPOINT指令,启动 Docker 容器时需要使用--entrypoint 参数才能覆盖 Dockerfile 中的ENTRYPOINT指令 ,而使用CMD设置的命令则可以被docker run后面的参数直接覆盖.
    • ENTRYPOINT指令可以结合CMD指令使用,也可以单独使用,而CMD指令只能单独使用.
    • 如果你希望你的镜像足够灵活,推荐使用CMD指令.如果你的镜像只执行单一的具体程序,并且不希望用户在执行docker run时覆盖默认程序,建议使用ENTRYPOINT.
    • 最后再强调一下,无论使用CMD还是ENTRYPOINT,都尽量使用exec模式.
      • exec 可以保证我们的业务进程就是 1 号进程,这对于需要处理 SIGTERM 信号量实现优雅终止十分重要
ADD和COPY
  • ADDCOPY指令功能类似,都是从外部往容器内添加文件.但是COPY指令只支持基本的文件和文件夹拷贝功能,ADD则支持更多文件来源类型,比如自动提取 tar 包,并且可以支持源文件为 URL 格式.

  • 推荐你使用COPY指令,因为COPY指令更加透明,仅支持本地文件向容器拷贝,而且使用COPY指令可以更好地利用构建缓存,有效减小镜像体积.

  • 当你想要使用ADD向容器中添加 URL 文件时,请尽量考虑使用其他方式替代.例如你想要在容器中安装 memtester(一种内存压测工具),你应该避免使用以下格式:

    1
    2
    3
    ADD http://pyropus.ca/software/memtester/old-versions/memtester-4.3.0.tar.gz /tmp/
    RUN tar -xvf /tmp/memtester-4.3.0.tar.gz -C /tmp
    RUN make -C /tmp/memtester-4.3.0 && make -C /tmp/memtester-4.3.0 install
    • 下面是推荐写法:
    1
    2
    3
    RUN wget -O /tmp/memtester-4.3.0.tar.gz http://pyropus.ca/software/memtester/old-versions/memtester-4.3.0.tar.gz \
    && tar -xvf /tmp/memtester-4.3.0.tar.gz -C /tmp \
    && make -C /tmp/memtester-4.3.0 && make -C /tmp/memtester-4.3.0 install
WORKDIR
  • 为了使构建过程更加清晰明了,推荐使用WORKDIR来指定容器的工作路径,应该尽量避免使用 RUN cd /work/path && do some work 这样的指令.

实例

1
2
3
4
5
6
7
8
9
10
FROM node:20.6.0 AS builder
RUN mkdir /usr/local/official
COPY . /usr/local/official
WORKDIR /usr/local/official
RUN npm install --unsafe-perm \
&& npm run build
FROM xxx.xxx:9091/test/demo:latest
RUN mkdir -p /usr/local/test/demo
COPY --from=builder /usr/local/official/dist /usr/local/test/demo/
COPY ./nginx.conf /etc/nginx/conf.d/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 将编译以及运行环境区分 减少最终应用程序docker镜像的体积
FROM maven:3.8.6-openjdk-11 AS builder
COPY ./pom.xml pom.xml
COPY ./src src/
COPY maven_config/settings.xml /usr/share/maven/conf/settings.xml
RUN mvn de.qaware.maven:go-offline-maven-plugin:resolve-dependencies \
&& mvn clean package -DskipTests

# 基础镜像只依赖jre
FROM eclipse-temurin:11-jre-alpine
RUN mkdir /usr/local/demo
COPY --from=builder /target/demmo-0.0.1-SNAPSHOT.jar /usr/local/demo/
# 设置环境变量
ENV JVM_OPTIONS="-Xms4096m -Xmx4096m -XX:+UseG1GC -Xlog:gc*,gc+ref=debug,gc+heap=debug,gc+age=trace:file=gc-%p-%t.log:tags,uptime,time,level:filecount=10,filesize=50m:/var/log/ct/gc/gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/ct"
WORKDIR /usr/local/demo
CMD java ${JVM_OPTIONS} -jar demo-0.0.1-SNAPSHOT.jar
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
FROM  python:3.9.16-slim
WORKDIR /src
COPY . /src/
RUN echo 'deb https://mirrors.aliyun.com/debian/ buster main non-free contrib' > /etc/apt/sources.list \
&& echo 'deb-src https://mirrors.aliyun.com/debian/ buster main non-free contrib' >> /etc/apt/sources.list \
&& echo 'deb https://mirrors.aliyun.com/debian-security buster/updates main' >> /etc/apt/sources.list \
&& echo 'deb-src https://mirrors.aliyun.com/debian-security buster/updates main' >> /etc/apt/sources.list \
&& echo 'deb https://mirrors.aliyun.com/debian/ buster-updates main non-free contrib' >> /etc/apt/sources.list \
&& echo 'deb-src https://mirrors.aliyun.com/debian/ buster-updates main non-free contrib' >> /etc/apt/sources.list \
&& echo 'deb https://mirrors.aliyun.com/debian/ buster-backports main non-free contrib' >> /etc/apt/sources.list \
&& echo 'deb-src https://mirrors.aliyun.com/debian/ buster-backports main non-free contrib ' >> /etc/apt/sources.list \
&& apt-get update && apt-get install -y libxext-dev libgl1-mesa-glx libxrender1 \
&& pip config set global.index-url https://mirrors.aliyun.com/pypi/simple \
&& pip config set global.trusted-host mirrors.aliyun.com \
&& python -m pip install --upgrade pip \
&& pip install --root-user-action=ignore -r requirements.txt
VOLUME ["/src/share-dicom-data"]
EXPOSE 10010
CMD ["python", "app.py"]