Dockerfile 是 Docker 镜像构建的核心,它通过一系列指令自动化地定义了镜像的构建过程。下面我们将详细介绍 Dockerfile 的制作流程,并通过案例展示其应用。
自动构建镜像的优势
在讨论 Dockerfile 的制作流程之前,我们先来探讨为什么要使用 Dockerfile 进行自动构建。
- 高效利用存储空间:Dockerfile 利用缓存机制,避免重复下载和处理相同的文件,减少了镜像的体积。
- 节省带宽:由于镜像分层存储,只有变化的层会被传输,这大大减少了传输数据的大小。
- 简化修改过程:Dockerfile 中的指令清晰记录了构建步骤,修改和更新更加直观和方便。
- 提高构建效率:Dockerfile 允许重用已有的镜像层,避免重复构建,加快了构建速度。
Dockerfile 简介
Dockerfile 是构建 Docker 镜像的核心脚本,它包含了一系列的指令,这些指令定义了镜像的构建过程。每个指令都会创建一个新的层,这些层最终组合成一个完整的镜像。Dockerfile 的设计哲学与 Makefile 类似,都用于自动化构建过程,但 Dockerfile 更专注于容器镜像的构建。
Dockerfile 的基本结构
一个 Dockerfile 通常包含以下四个部分:
- 基础镜像信息:指定从哪个现有的镜像开始构建。这是通过 FROM 指令实现的,它是 Dockerfile 中的第一条指令。
- 维护者信息:虽然这不是强制性的,但 MAINTAINER 或 LABEL 指令可以用来指定镜像的作者或维护者信息,这有助于镜像的管理和归属。
- 镜像操作指令:这些指令定义了镜像的内容和结构,包括安装软件包、复制文件、设置环境变量、创建用户和工作目录等。常见的指令有 RUN、COPY、ADD、ENV、WORKDIR 等。
- 容器启动时执行指令:定义容器启动时应该运行的命令。这通常由 CMD 或 ENTRYPOINT 指令指定,两者可以一起使用以提供默认行为和可覆盖的入口点。
制作 Dockerfile 的流程
手动构建镜像就像是直接烹饪一道菜,而 Dockerfile 则像是这道菜的食谱。使用 Dockerfile,你只需按照食谱上的步骤操作,就可以复现相同的菜式。
- 手动制作 Docker 镜像:首先手动创建一个 Docker 镜像,并记录下所有使用的命令。
- 编写 Dockerfile 文件:根据记录的命令,编写 Dockerfile,将命令转换为 Docker 可以理解的指令。
- 使用
docker build
构建镜像:运行docker build
命令,Docker 会根据 Dockerfile 中的指令构建镜像。 - 测试镜像功能:构建完成后,运行并测试镜像以确保其按预期工作。
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
指令中的命令较长,为了提高可读性和便于维护,可以使用反斜杠 进行命令换行:
RUN apt-get update && apt-get install -y curl vim
▌COPY&ADD
在 Dockerfile 中,ADD
和 COPY
是两个常用的指令,用于将文件从构建上下文(通常是 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 复制本地文件:
ADD local-file /dest-path
- 使用 ADD 从 URL 下载文件:
ADD http://example.com/remote-file /dest-path
- 使用 COPY 复制本地文件:
COPY local-file /dest-path
注意事项
- 安全性:
ADD
可以下载文件,因此如果使用 URL 作为<src>
,需要注意安全性和信任问题。 - 透明度:
COPY
在大多数情况下更推荐使用,因为它的行为更可预测,更透明。 - 解压缩: 如果需要复制并解压缩 tar 文件,确保使用
ADD
指令。
▌ENV
ENV
指令在 Dockerfile 中用于设置环境变量,这些环境变量在后续的 RUN
、CMD
、ENTRYPOINT
、COPY
和 ADD
指令中都可用,并且会持续存在于镜像中,直到容器的生命周期结束。
ENV 指令的格式
ENV
指令有两种格式:
- 单个变量: ENV <key> <value>
- 多个变量:
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
选项来挂载数据卷:
docker run -d --name my_container -v /tmp/data my_image
这个命令将宿主机上的 /tmp/data
目录挂载到容器内部的 /tmp/data
目录。
数据卷的共享
Docker 允许通过 --volumes-from
选项在容器之间共享数据卷:
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>
可以是一个具体的端口号,也可以是一个端口范围。
使用示例
- 映射单个端口: EXPOSE 80
- 映射多个端口: EXPOSE 80 443
运行容器时的端口映射
尽管 EXPOSE
指令在 Dockerfile 中声明了需要暴露的端口,但实际的端口映射是在运行容器时通过 docker run
命令的 -p
或 --publish
选项来完成的。
- 随机映射宿主机端口:
docker run -p 80 image
这将容器的 80 端口映射到宿主机的一个随机端口上。
- 指定宿主机端口:
docker run -p 8080:80 image
这将容器的 80 端口映射到宿主机的 8080 端口上。
- 映射多个端口:
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 格式:
CMD ["sh", "-c", "echo Hello World"]
- Shell 格式:
CMD echo Hello World
- 与 ENTRYPOINT 配合使用:
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 格式:
ENTRYPOINT ["executable", "param1", "param2"]
这种格式使用 JSON 数组直接指定可执行文件及其参数。
- Shell 格式:
ENTRYPOINT command param1 param2
这种格式在 shell 中执行命令。注意,这种格式在 Dockerfile 中不太常用,因为它可能受到 shell 环境的影响,导致跨平台问题。
ENTRYPOINT 的使用情况
- 独立使用: 当
ENTRYPOINT
独立使用时,它指定的命令将在容器启动时执行,并且不会被docker run
提供的任何参数覆盖。
FROM ubuntuENTRYPOINT ["top", "-b"]
- 与 CMD 配合使用: 当
ENTRYPOINT
与CMD
配合使用时,CMD
指定的参数将传递给ENTRYPOINT
指定的命令。在这种情况下,CMD
不是一个完整的命令,而是参数。
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>
使用示例
- 指定运行容器的用户:
USER daemon
- 创建用户并指定运行容器的用户:
RUN groupadd -r postgres && useradd -r -g postgres postgresUSER postgres
- 与
ENTRYPOINT
结合使用: 如果服务的可执行文件接受用户参数,可以直接在ENTRYPOINT
中指定:
ENTRYPOINT ["memcached", "-u", "daemon"]
或者,如果服务的可执行文件不接受用户参数,可以在 USER
指令中指定:
ENTRYPOINT ["memcached"]USER daemon
注意事项
- 权限问题: 如果以非
root
用户运行,确保该用户具有执行所需操作的权限。 - 层的顺序:
USER
指令应该在需要以特定用户身份执行的命令之前。例如,任何RUN
指令,如果需要特定用户权限,都应该在USER
指令之后。 - 用户存在: 在指定用户之前,确保该用户已经存在。可以使用
useradd
命令在镜像构建过程中创建用户。
▌WORKDIR
WORKDIR
指令在 Dockerfile 中用于为容器设置工作目录,即容器内部的当前目录。这个目录对于后续的 RUN
、CMD
、ENTRYPOINT
、COPY
和 ADD
指令是生效的。如果 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 指令,如 COPY
、RUN
、ADD
等。
▌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
命令检查容器上的服务是否健康:
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腾讯技术创作特训营最新征文,快来和我瓜分大奖!