五分钟学K8S系列<四>-深入浅出Dockerfile

2024-05-01 21:48:16 浏览数 (1)

Dockerfile 是 Docker 镜像构建的核心,它通过一系列指令自动化地定义了镜像的构建过程。下面我们将详细介绍 Dockerfile 的制作流程,并通过案例展示其应用。

自动构建镜像的优势

    在讨论 Dockerfile 的制作流程之前,我们先来探讨为什么要使用 Dockerfile 进行自动构建。

  1. 高效利用存储空间:Dockerfile 利用缓存机制,避免重复下载和处理相同的文件,减少了镜像的体积。
  2. 节省带宽:由于镜像分层存储,只有变化的层会被传输,这大大减少了传输数据的大小。
  3. 简化修改过程:Dockerfile 中的指令清晰记录了构建步骤,修改和更新更加直观和方便。
  4. 提高构建效率:Dockerfile 允许重用已有的镜像层,避免重复构建,加快了构建速度。

Dockerfile 简介

    Dockerfile 是构建 Docker 镜像的核心脚本,它包含了一系列的指令,这些指令定义了镜像的构建过程。每个指令都会创建一个新的层,这些层最终组合成一个完整的镜像。Dockerfile 的设计哲学与 Makefile 类似,都用于自动化构建过程,但 Dockerfile 更专注于容器镜像的构建。

图片图片

Dockerfile 的基本结构

一个 Dockerfile 通常包含以下四个部分:

  1. 基础镜像信息:指定从哪个现有的镜像开始构建。这是通过 FROM 指令实现的,它是 Dockerfile 中的第一条指令。
  2. 维护者信息:虽然这不是强制性的,但 MAINTAINER 或 LABEL 指令可以用来指定镜像的作者或维护者信息,这有助于镜像的管理和归属。
  3. 镜像操作指令:这些指令定义了镜像的内容和结构,包括安装软件包、复制文件、设置环境变量、创建用户和工作目录等。常见的指令有 RUN、COPY、ADD、ENV、WORKDIR 等。
  4. 容器启动时执行指令:定义容器启动时应该运行的命令。这通常由 CMD 或 ENTRYPOINT 指令指定,两者可以一起使用以提供默认行为和可覆盖的入口点。

制作 Dockerfile 的流程

  手动构建镜像就像是直接烹饪一道菜,而 Dockerfile 则像是这道菜的食谱。使用 Dockerfile,你只需按照食谱上的步骤操作,就可以复现相同的菜式。

  1. 手动制作 Docker 镜像:首先手动创建一个 Docker 镜像,并记录下所有使用的命令。
  2. 编写 Dockerfile 文件:根据记录的命令,编写 Dockerfile,将命令转换为 Docker 可以理解的指令。
  3. 使用 docker build 构建镜像:运行 docker build 命令,Docker 会根据 Dockerfile 中的指令构建镜像。
  4. 测试镜像功能:构建完成后,运行并测试镜像以确保其按预期工作。

Dockerfile 常用指令解析

▌FROM

FROM 指令用于指定基础镜像,是 Dockerfile 中的第一条指令。

有两种格式

代码语言:javascript复制
FROM<image>  指定基础image为该image的最后修改的版本。

或者:

代码语言:javascript复制
FROM<image>:<tag>  指定基础image为该image的一个tag版本

示例:

代码语言:javascript复制
FROM ubuntu:18.04

▌RUN

构建指令,RUN可以运行任何被基础image支持的命令

RUN 指令的两种格式

1. Shell 格式:

代码语言:javascript复制
RUN <command> (the command is run in a shell - `/bin/sh -c`)

   这种格式在 shell 环境中执行命令,允许使用 shell 的特性,如变量替换、管道、通配符等。

2. Exec 格式:

代码语言:javascript复制
RUN ["executable", "param1", "param2", ... ] (exec form)

   这种格式直接执行命令,不通过 shell。这有助于避免 shell 带来的潜在安全风险,并提供更清晰的执行环境。

跨行命令

RUN 指令中的命令较长,为了提高可读性和便于维护,可以使用反斜杠 进行命令换行:

代码语言:javascript复制
RUN apt-get update     && apt-get install -y         curl         vim

▌COPY&ADD

在 Dockerfile 中,ADDCOPY 是两个常用的指令,用于将文件从构建上下文(通常是 Dockerfile 所在的目录)复制到构建中的容器镜像中。它们在功能上相似,但也存在一些差异。

ADD 指令

格式:

代码语言:javascript复制
  ADD <src> <dest>

说明:

  • <src> 可以是 Dockerfile 所在目录的相对路径,也可以是一个 URL,或者是一个 tar 文件(在这种情况下,它将被自动解压)。
  • <dest> 是容器中的绝对路径,或者是相对于 WORKDIR 指令设置的路径。

特点:

  • ADD 会保留文件的权限,但所有文件和文件夹的权限会被设置为 0755,uid 和 gid 被设置为 0。
  • 如果 <src> 是一个目录,那么只有目录内的内容会被复制,不包括目录本身。
  • 如果 <src> 是一个可识别的压缩格式,Docker 会自动解压缩它。
  •  如果 <src> 是一个文件,并且 <dest> 以斜杠 / 结尾,那么 <src> 将被拷贝到 <dest> 目录下。
  •  如果 <src> 是一个文件,并且 <dest> 没有以斜杠 / 结尾,那么 <src> 的内容将被写入 <dest>。

COPY 指令

格式:

代码语言:javascript复制
COPY <src> <dest>

说明:

  • COPY 只能访问 Dockerfile 所在目录(构建上下文)中的文件,不能访问 URL 或 tar 文件。
  • <dest> 的工作方式与 ADD 相同

特点:

  • COPY 不会自动解压缩 tar 文件,它仅仅是复制文件或目录。
  • COPY 在权限和所有权方面比 ADD 更透明,它保留了文件原有的权限和所有权。

使用示例

  • 使用 ADD 复制本地文件:
代码语言:javascript复制
ADD local-file /dest-path
  • 使用 ADD 从 URL 下载文件:
代码语言:javascript复制
ADD http://example.com/remote-file /dest-path
  • 使用 COPY 复制本地文件:
代码语言:javascript复制
COPY local-file /dest-path

注意事项

  • 安全性: ADD 可以下载文件,因此如果使用 URL 作为 <src>,需要注意安全性和信任问题。
  • 透明度: COPY 在大多数情况下更推荐使用,因为它的行为更可预测,更透明。
  • 解压缩: 如果需要复制并解压缩 tar 文件,确保使用 ADD 指令。

▌ENV

ENV 指令在 Dockerfile 中用于设置环境变量,这些环境变量在后续的 RUNCMDENTRYPOINTCOPYADD 指令中都可用,并且会持续存在于镜像中,直到容器的生命周期结束。

ENV 指令的格式

ENV 指令有两种格式:

  1. 单个变量: ENV <key> <value>
  2. 多个变量:
代码语言:javascript复制
ENV <key1>=<value1> <key2>=<value2> ...

使用示例

代码语言:javascript复制
FROM ubuntuENV APP_HOME /appENV PATH=$APP_HOME:$PATH

在这个例子中,我们设置了两个环境变量:

  • APP_HOME 被设置为 /app
  • PATH 被修改为在原有的 PATH 基础上添加了 APP_HOME 的值。

注意事项

  • 环境变量的覆盖: 如果在构建过程中多次设置了相同的环境变量,只有最后设置的值会被保留。
  • 环境变量的继承: 环境变量会从基础镜像继承,并且可以被当前镜像中的 ENV 指令修改。
  • 安全性: 避免在 ENV 指令中设置敏感信息,如密码或密钥。

▌VOLUME

VOLUME 指令在 Dockerfile 中用于定义容器中的一个挂载点,它使得该目录可以作为数据卷,实现数据的持久化存储。在 Docker 中,数据卷是持久化存储和共享数据的一种机制,它们可以独立于容器的生命周期,即使容器被删除,数据卷中的数据也不会丢失。

VOLUME 指令的格式

代码语言:javascript复制
VOLUME ["<mountpoint>"]
  • <mountpoint> 是容器内部的绝对路径,它指定了挂载点的位置。

使用示例

代码语言:javascript复制
FROM baseVOLUME ["/tmp/data"]

在这个例子中,/tmp/data 目录被定义为数据卷,它允许容器在运行时将该目录挂载到宿主机或其他容器的文件系统上。

运行容器时使用数据卷

当使用 docker run 命令启动容器时,可以通过 -v--volume 选项来挂载数据卷:

代码语言:javascript复制
docker run -d --name my_container -v /tmp/data my_image

这个命令将宿主机上的 /tmp/data 目录挂载到容器内部的 /tmp/data 目录。

数据卷的共享

Docker 允许通过 --volumes-from 选项在容器之间共享数据卷:

代码语言:javascript复制
docker run -t -i -rm --volumes-from my_container -name another_container my_image bash
代码语言:javascript复制
在这个例子中,another_container 可以访问 my_container 的 /tmp/data 数据卷。

注意事项

  • 数据卷的生命周期:数据卷的生命周期独立于容器,容器删除后,数据卷中的数据仍然存在。
  • 数据卷的权限:数据卷的权限可能需要根据运行容器的用户权限进行适当配置。
  • 数据卷的备份:虽然数据卷提供了数据持久化,但仍建议定期备份重要数据。

▌EXPOSE

 EXPOSE 指令在 Dockerfile 中用于声明容器在运行时需要暴露的端口号,这些端口在容器内部的应用程序中用于监听。EXPOSE 指令不会实际上将端口映射到宿主机上,而是作为一个声明,告知用户哪些端口在运行容器时应该被映射。

EXPOSE 指令的格式

代码语言:javascript复制
EXPOSE <port> [<port>...]

这里的 <port> 可以是一个具体的端口号,也可以是一个端口范围。

使用示例

  1. 映射单个端口: EXPOSE 80
  2. 映射多个端口: EXPOSE 80 443

运行容器时的端口映射

尽管 EXPOSE 指令在 Dockerfile 中声明了需要暴露的端口,但实际的端口映射是在运行容器时通过 docker run 命令的 -p--publish 选项来完成的。

  • 随机映射宿主机端口:
代码语言:javascript复制
docker run -p 80 image

这将容器的 80 端口映射到宿主机的一个随机端口上。

  • 指定宿主机端口:
代码语言:javascript复制
docker run -p 8080:80 image

这将容器的 80 端口映射到宿主机的 8080 端口上。

  • 映射多个端口:
代码语言:javascript复制
docker run -p 8080:80 -p 8443:443 image

这将容器的 80 端口映射到宿主机的 8080 端口,同时将容器的 443 端口映射到宿主机的 8443 端口。

注意事项

  • 端口映射的安全性: 将容器端口映射到宿主机时,需要考虑安全性,确保不会暴露敏感服务。
  • 端口冲突: 确保宿主机上没有其他服务使用相同的端口,否则会导致映射失败。
  • 查看端口映射: 使用 docker ps 可以查看容器的端口映射情况,或者使用 docker port <container_id_or_name> <port> 来查看特定端口在宿主机上的映射。

▌CMD

 CMD 是 Dockerfile 中的一个指令,用于指定容器启动时默认执行的命令。这个指令非常重要,因为它定义了容器的预期行为或进程。以下是 CMD 指令的三种格式及其使用方式:

1. Exec 格式

代码语言:javascript复制
CMD ["executable", "param1", "param2"]

    这种格式使用 JSON 数组直接指定可执行文件及其参数。这是推荐的方式,因为它清晰、易于调试,并且可以确保可执行文件及其参数被正确地传递给 shell。

2. Shell 格式

代码语言:javascript复制
CMD command param1 param2

    这种格式在 shell (/bin/sh -c) 中执行命令。这适用于需要交互式 shell 或执行 shell 脚本的情况。

3. ENTRYPOINT 的默认参数

代码语言:javascript复制
CMD ["param1", "param2"]

    当 Dockerfile 中指定了 ENTRYPOINT 指令时,CMD 可以用于提供 ENTRYPOINT 的默认参数。在这种情况下,CMD 必须使用 JSON 数组格式。

示例

  • Exec 格式:
代码语言:javascript复制
CMD ["sh", "-c", "echo Hello World"]
  • Shell 格式:
代码语言:javascript复制
CMD echo Hello World
  • 与 ENTRYPOINT 配合使用:
代码语言:javascript复制
ENTRYPOINT ["/usr/bin/my_app"]CMD ["--arg1", "value1"]

在这个例子中,容器启动时将执行 /usr/bin/my_app --arg1 value1

注意事项

  • 单一性: 每个 Dockerfile 中只能有一条 CMD 命令。如果有多条,只有最后一条会被执行。
  • 覆盖: 用户在启动容器时指定的命令将覆盖 CMD 指定的命令。
  • 与 ENTRYPOINT 的关系: 如果 CMD 用于给 ENTRYPOINT 提供参数,它必须使用 JSON 数组格式。

▌ENTRYPOINT

 ENTRYPOINT 指令在 Dockerfile 中用于定义容器启动时执行的命令。它对于设置容器的行为非常关键,尤其是当你希望无论传递什么参数,容器都能以一种特定的方式运行时。

ENTRYPOINT 的两种格式

  • Exec 格式:
代码语言:javascript复制
ENTRYPOINT ["executable", "param1", "param2"]

这种格式使用 JSON 数组直接指定可执行文件及其参数。

  • Shell 格式:
代码语言:javascript复制
ENTRYPOINT command param1 param2

这种格式在 shell 中执行命令。注意,这种格式在 Dockerfile 中不太常用,因为它可能受到 shell 环境的影响,导致跨平台问题。

ENTRYPOINT 的使用情况

  • 独立使用: 当 ENTRYPOINT 独立使用时,它指定的命令将在容器启动时执行,并且不会被 docker run 提供的任何参数覆盖。
代码语言:javascript复制
FROM ubuntuENTRYPOINT ["top", "-b"]
  • 与 CMD 配合使用: 当 ENTRYPOINTCMD 配合使用时,CMD 指定的参数将传递给 ENTRYPOINT 指定的命令。在这种情况下,CMD 不是一个完整的命令,而是参数。
代码语言:javascript复制
FROM ubuntuCMD ["-l"]ENTRYPOINT ["/usr/bin/ls"]

在这个例子中,容器启动时将执行 /usr/bin/ls -l

注意事项

  • 单一性: 每个 Dockerfile 中只能有一个 ENTRYPOINT 指令。如果有多个,只有最后一个会生效。
  • 参数传递: 当 ENTRYPOINT 与 CMD 配合使用时,CMD 提供的参数将作为 ENTRYPOINT 命令的参数。
  • 覆盖问题: 如果在 ENTRYPOINT 之后还使用了 CMD,并且 CMD 是一个完整的命令,那么 ENTRYPOINT 将被覆盖。

▌USER

    在 Dockerfile 中使用 USER 指令可以指定运行容器时的用户。默认情况下,容器以 root 用户运行,但出于安全考虑,如果服务不需要管理员权限,可以通过 USER 指令指定一个非 root 用户来运行容器。

USER 指令的格式

代码语言:javascript复制
USER <用户名或UID>

或者

代码语言:javascript复制
USER <用户名>:<用户组或GID>

使用示例

  • 指定运行容器的用户:
代码语言:javascript复制
USER daemon
  • 创建用户并指定运行容器的用户:
代码语言:javascript复制
RUN groupadd -r postgres && useradd -r -g postgres postgresUSER postgres
  • ENTRYPOINT 结合使用: 如果服务的可执行文件接受用户参数,可以直接在 ENTRYPOINT 中指定:
代码语言:javascript复制
ENTRYPOINT ["memcached", "-u", "daemon"]

或者,如果服务的可执行文件不接受用户参数,可以在 USER 指令中指定:

代码语言:javascript复制
ENTRYPOINT ["memcached"]USER daemon

注意事项

  • 权限问题: 如果以非 root 用户运行,确保该用户具有执行所需操作的权限。
  • 层的顺序: USER 指令应该在需要以特定用户身份执行的命令之前。例如,任何 RUN 指令,如果需要特定用户权限,都应该在 USER 指令之后。
  • 用户存在: 在指定用户之前,确保该用户已经存在。可以使用 useradd 命令在镜像构建过程中创建用户。

▌WORKDIR

 WORKDIR 指令在 Dockerfile 中用于为容器设置工作目录,即容器内部的当前目录。这个目录对于后续的 RUNCMDENTRYPOINTCOPYADD 指令是生效的。如果 WORKDIR 指定的目录不存在,Docker 会自动创建所有需要的中间目录。

WORKDIR 指令的格式

代码语言:javascript复制
WORKDIR /path/to/workdir
  • /path/to/workdir 是容器内部的绝对路径,或者是相对于之前 WORKDIR 指令的相对路径。

使用示例

代码语言:javascript复制
# 设置工作目录为 /appWORKDIR /app
# 等同于 WORKDIR /appRUN mkdir app
# 将工作目录切换到上一步创建的 app 目录WORKDIR app
# 此时执行 vim a.txt,是在 /app/app 目录下执行RUN vim a.txt

注意事项

  • 相对路径:WORKDIR 可以接受相对路径,它相对于上一个 WORKDIR 指定的路径。
  • 路径叠加:连续使用 WORKDIR 指令可以叠加路径,Docker 会创建所有中间目录。
  • 环境变量:WORKDIR 也可以使用环境变量,例如 WORKDIR $USER/home

▌ONBUILD

 ONBUILD 是 Dockerfile 中的一个特殊指令,它用于在创建子镜像时自动执行特定的命令。这些命令在当前镜像构建过程中不会执行,而是在有人使用这个镜像作为基础镜像创建新镜像时触发。

ONBUILD 指令的格式

代码语言:javascript复制
ONBUILD <Dockerfile 指令>

这里的 <Dockerfile 指令> 可以是任何有效的 Dockerfile 指令,如 COPYRUNADD 等。

▌HEALTHCHECK

 HEALTHCHECK 是 Dockerfile 中的一个指令,用于指定如何对容器进行健康检查,这可以帮助确定容器是否仍在正常运行并且准备好接收流量。如果没有健康检查,容器管理工具(如 Docker 或 Kubernetes)可能很难知道一个容器是否已经失败或者无响应。

HEALTHCHECK 指令的格式

代码语言:javascript复制
HEALTHCHECK [OPTIONS] CMD command (容器必须返回的状态码)HEALTHCHECK [OPTIONS] NONE
  • --interval=<duration>:两次健康检查之间的时间间隔。
  • --timeout=<duration>:健康检查命令的超时时间。
  • --start-period=<duration>:在容器启动后,多久开始健康检查。
  • --retries=<num-retries>:健康检查失败后,容器重启前尝试的次数。

使用示例

以下是 HEALTHCHECK 指令的一个示例,它使用 curl 命令检查容器上的服务是否健康:

代码语言:javascript复制
HEALTHCHECK--interval=5m--timeout=3s--start-period=1m--retries=3  CMD curl -f http://localhost:80 || exit 1

在这个例子中:

  • --interval=5m:每 5 分钟执行一次健康检查。
  • --timeout=3s:如果健康检查命令在 3 秒内没有返回,它将被视为失败。
  • --start-period=1m:容器启动 1 分钟后开始健康检查。
  • --retries=3:如果健康检查连续失败 3 次,Docker 将认为容器不健康,并可能采取行动,如重启容器。

注意事项

  • 必须返回的状态码:健康检查命令必须返回 0(成功)或 1(失败)。
  • 容器内命令:CMD 后面跟的命令必须在容器内部运行,并且能够检测容器的健康状态。

综合案例

下面是一个使用上述指令的 Dockerfile 示例,构建一个运行 Nginx 服务的镜像:

代码语言:javascript复制
# 使用官方的 Ubuntu 基础镜像FROM ubuntu:18.04
# 设置环境变量,指定时区ENV TZ=UTC     DEBIAN_FRONTEND=noninteractive
# 更新包索引并安装 NginxRUN apt-get update && apt-get install -y nginx
# 将本地文件复制到容器中的 /app 目录COPY . /app
# 设置工作目录为 /appWORKDIR /app
# 监听的端口EXPOSE 80
# 设置容器启动时执行的命令CMD ["nginx", "-g", "daemon off;"]
# 设置健康检查HEALTHCHECK --retries=3 CMD [ "curl", "-f", "http://localhost" ]

构建镜像

代码语言:javascript复制
docker build -t dockerfile-sre-nginx -f demo_2.dockerfile .[ ] Building 69.6s (9/9) FINISHED                                             docker:default => [internal] load build definition from demo_2.dockerfile                             0.0s => => transferring dockerfile: 514B                                                    0.0s => [internal] load metadata for docker.io/library/ubuntu:18.04                         5.5s => [internal] load .dockerignore                                                       0.0s => => transferring context: 2B                                                         0.0s => [1/4] FROM docker.io/library/ubuntu:18.04@sha256:152dc042452c496007f07ca9127571cb9  8.3s => => resolve docker.io/library/ubuntu:18.04@sha256:152dc042452c496007f07ca9127571cb9  0.0s => => sha256:7c457f213c7634afb95a0fb2410a74b7b5bc0ba527033362c240c7 25.69MB / 25.69MB  6.4s => => sha256:152dc042452c496007f07ca9127571cb9c29697f42acbfad72324b2b 1.33kB / 1.33kB  0.0s => => sha256:dca176c9663a7ba4c1f0e710986f5a25e672842963d95b960191e2d9f718 424B / 424B  0.0s => => sha256:f9a80a55f492e823bf5d51f1bd5f87ea3eed1cb31788686aa99a2fb6 2.30kB / 2.30kB  0.0s => => extracting sha256:7c457f213c7634afb95a0fb2410a74b7b5bc0ba527033362c240c7a11bef4  1.7s => [internal] load build context                                                       2.9s => => transferring context: 187.38MB                                                   2.9s => [2/4] RUN apt-get update && apt-get install -y nginx                               54.4s => [3/4] COPY . /app                                                                   1.0s  => [4/4] WORKDIR /app                                                                  0.0s  => exporting to image                                                                  0.3s  => => exporting layers                                                                 0.3s  => => writing image sha256:d72611e47ccde8d858882cdc2a23bf166c8a96fcdf2973e273495e5ca6  0.0s  => => naming to docker.io/library/dockerfile-sre-nginx

启动容器

代码语言:javascript复制
#构建好的镜像docker image  lsREPOSITORY             TAG          IMAGE ID       CREATED         SIZEdockerfile-sre-nginx   latest       d72611e47ccd   8 minutes ago   356MB#启动容器docker run -it -d dockerfile-sre-nginx3a1bdfa88872ee2797181710b16189ec087ab2e67e13762c5eeacfa4a7e0163e

    使用这个 Dockerfile,我们可以通过 Docker 构建一个镜像,该镜像启动后会运行 Nginx 服务,并且可以通过健康检查来验证服务是否正常运行。

小结

    在本文中,我们探讨了 Dockerfile 的重要性以及如何有效利用它来自动化 Docker 镜像的构建过程。Dockerfile 提供了一种声明式的方法来定义镜像内容,使得镜像的构建变得简洁、高效且易于维护。  

我正在参与2024腾讯技术创作特训营最新征文,快来和我瓜分大奖!

0 人点赞