Dockerfile

1. 基础镜像的选择

基本原则:

  • 官方镜像优于非官方的镜像,如果没有官方镜像,则尽量选择Dockerfile开源的
  • 固定版本tag而不是每次都使用latest
  • 尽量选择体积小的镜像

2.通过RUN执行指令

RUN 主要用于在Image里执行指令, 比如安装软件, 下载文件等. 使用 \ 拼接命令防止不必要的分层

3.文件复制和目录操作

使用COPYADD 命令

  • 复制普通文件

    1
    2
    FROM python:3.9.5-alpine3.13
    COPY hello.py /app/hello.py
  • 复制压缩文件

    使用 ADD 时如果复制的是一个gzip等压缩文件时,ADD会帮助我们自动去解压缩文件。

    1
    2
    FROM python:3.9.5-alpine3.13
    ADD hello.tar.gz /app/
  • 命令的选择

    COPYADD 指令中选择的时候,可以遵循这样的原则,所有的文件复制均使用 COPY 指令,仅在需要自动解压缩的场合使用 ADD.

  • WORKDIR

    WORKDIR 指令为 Dockerfile 中紧随其后的 RUNCMDENTRYPOINTCOPYADD 指令设置工作目录。如果指定的工作目录不存在,即使在后续的 Dockerfile 指令中没有使用,它也会被创建。

4.构建参数和环境变量(ARG vs ENV)

当在 Docker 中构建镜像时,我们经常需要考虑如何传递配置信息和变量到镜像内部。

构建参数(ARG)

构建参数(ARG)是在构建镜像时传递给构建过程的变量。它们可以用于在构建过程中进行条件判断、选择不同的分支或在构建过程中传递信息。以下是使用 ARG 的示例:

1
2
3
4
5
# 使用构建参数定义变量
ARG APP_VERSION

# 在构建过程中使用构建参数
RUN echo "Building app version: $APP_VERSION"

在构建过程中,可以通过 --build-arg 选项来传递构建参数的值:

1
docker build --build-arg APP_VERSION=1.0 -t myapp .

环境变量(ENV)

环境变量(ENV)是在容器内运行时使用的变量。它们可以用于在容器内部设置配置、传递信息或指定行为。以下是使用 ENV 的示例:

1
2
3
4
5
6
# 设置环境变量
ENV DB_HOST=localhost
ENV DB_PORT=5432

# 在容器内使用环境变量
CMD echo "Connecting to database at $DB_HOST:$DB_PORT"

使用 -e 选项可以在运行容器时设置环境变量的值:

1
docker run -e DB_HOST=mydbhost -e DB_PORT=5432 myapp

ARG vs ENV:选择哪种方式?

在选择使用 ARG 还是 ENV 时,需要考虑以下几点:

  • ARG 用于构建时,ENV 用于运行时。如果您希望在构建过程中传递变量值,使用 ARG。如果需要在容器运行时设置配置或环境,使用 ENV
  • ARG 值只在构建过程中可用,不会在容器内保留。ENV 值将在容器内持续存在。
  • ENV 更适合在容器内部设置常驻环境变量,如配置信息、路径等。

综合考虑,根据需要选择适当的方式来管理构建参数和环境变量,以实现更有效的 Docker 镜像管理。

5.CMD 容器启动命令

在 Docker 中,CMD 是用于定义容器启动时默认执行的命令或应用程序。这个命令会在容器启动时自动执行,如果在 docker run 命令中没有指定要运行的特定命令。

以下是关于 CMD 的一些重要信息:

  • CMD 可以在 Dockerfile 中使用,用于指定在容器启动时要运行的默认命令。这个命令可以是应用程序,也可以是一条脚本。
  • 如果在 Dockerfile 中有多个 CMD 指令,只有最后一个会生效。
  • CMD 指令的格式可以是列表形式,也可以是字符串形式。列表形式使用 JSON 数组格式(例如 CMD ["executable","param1","param2"]),字符串形式则使用标准 shell 命令(例如 CMD executable param1 param2)。
  • 如果在 docker run 命令中指定了要运行的命令,它将覆盖 CMD 中定义的默认命令。
  • CMD 中指定的命令不会在容器内创建新的进程,而是替代了容器的默认进程。这意味着当指定的命令运行结束时,容器也会自动终止。

6.ENTRYPOINT 容器启动命令

在 Docker 中,ENTRYPOINT 用于定义容器启动时要执行的主要命令或应用程序。与 CMD 不同,ENTRYPOINT 定义的命令在容器启动时始终执行,无论是否在 docker run 命令中指定了其他命令。

以下是关于 ENTRYPOINT 的一些重要信息:

  • ENTRYPOINT 可以在 Dockerfile 中使用,用于指定容器启动时要运行的主要命令。这个命令可以是应用程序,也可以是一条脚本。
  • 如果在 docker run 命令中指定了要运行的命令,它将被视为 ENTRYPOINT 中命令的参数,而不会覆盖 ENTRYPOINT 定义的默认命令。
  • 如果需要在容器启动时传递参数,可以将这些参数放在 ENTRYPOINT 后面。例如:ENTRYPOINT ["executable", "param1", "param2"]
  • CMD 指令可以与 ENTRYPOINT 一起使用,提供默认的参数。这样可以在运行容器时覆盖 CMD 提供的默认参数,但不会影响 ENTRYPOINT 的命令。
  • CMD 类似,ENTRYPOINT 中指定的命令也不会在容器内创建新的进程,而是替代了容器的默认进程。这意味着当指定的命令运行结束时,容器也会自动终止。

7.Shell格式和Exec格式

在 Docker 的 CMDENTRYPOINT 指令中,有两种常见的格式:Shell 格式和 Exec 格式。

Shell 格式

在 Shell 格式中,CMDENTRYPOINT 的命令会被解释为在一个 shell 中执行。这意味着你可以使用 shell 的特性,比如通配符和变量替换。

例如,以下是一个使用 Shell 格式的示例:

1
CMD echo "Hello, $USER!"

在这个示例中,$USER 是一个 shell 变量,将会在容器启动时被替换成实际的用户名。

Exec 格式

在 Exec 格式中,CMDENTRYPOINT 的命令会被直接执行,不会被解释成 shell 命令。这意味着没有 shell 特性可用,也没有变量替换。

例如,以下是一个使用 Exec 格式的示例:

1
CMD ["echo", "Hello, World!"]

在这个示例中,命令 ["echo", "Hello, World!"] 会被直接执行,不会进行任何 shell 解释或变量替换。

如何选择

选择使用哪种格式取决于你的需求。如果需要使用 shell 的特性或变量替换,那么可以使用 Shell 格式。但是,Shell 格式可能会引入一些潜在的问题,比如需要处理 shell 的转义和安全性问题。

Exec 格式通常更安全,因为它避免了 shell 的解释和可能的漏洞。它也更适合于不依赖于 shell 特性的命令。

总之,根据实际情况选择合适的格式,以满足容器的需求。

当你想要在 CMD 中使用带有 Shell 变量的命令,你可以使用 Shell 格式,如下所示:

1
CMD echo "Hello, $USER!"

在这个例子中,$USER 是一个 shell 变量,将会在容器启动时被替换成实际的用户名。然而,如果你想在 Exec 格式中使用变量,你需要使用 Shell 的执行命令:

1
CMD ["/bin/sh", "-c", "echo Hello, $USER!"]

在这个示例中,我们使用了 /bin/sh 作为执行的 shell,然后使用 -c 选项传递要执行的命令。这样,$USER 变量会在 shell 中被解释和替换。

请注意,虽然这个例子使用了 Exec 格式,但是它还是在容器内部启动了一个 shell 进程,然后执行了命令。这可能会引入一些不必要的额外开销,因此在选择 Shell 格式和 Exec 格式时,需要根据实际需求进行权衡。

8.合理使用缓存

在编写 Dockerfile 时,合理使用缓存是优化构建过程和减少镜像构建时间的重要技巧之一。

Docker 在构建镜像时会使用缓存,以便重复使用之前的构建步骤。

以下是一些合理使用缓存的技巧:

1. 优先复用不变的步骤

将不太可能发生变化的步骤放在 Dockerfile 的前面,以便在构建过程中复用缓存。比如,安装基础依赖、复制代码等步骤通常不会频繁变化。

2. 分阶段构建

将 Dockerfile 分成多个阶段(FROM),每个阶段执行特定的任务。这样可以确保只有在前面阶段的内容发生变化时才会重新构建。不变的阶段的缓存会被复用。

1
2
3
4
5
6
7
8
9
10
11
# 阶段一:构建应用
FROM node:14 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm build

# 阶段二:运行应用
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html

3. 使用 --no-cache

有时,为了确保获取最新的依赖或代码,你可以在构建时禁用缓存,以防止旧的缓存内容被使用。

1
docker build --no-cache -t myapp .

4. 按需精确刷新缓存

如果你只想刷新特定步骤的缓存,可以在 Dockerfile 中做一些改变,例如修改一个文件的时间戳,以触发后续步骤的重新构建。

5. 使用 .dockerignore 文件

.dockerignore 文件可以用来指定哪些文件不需要被包含在构建上下文中,从而减少构建上下文的大小,提高构建速度。

6. 小心使用 COPYADD

COPYADD 步骤会影响缓存的使用。如果源文件没有变化,那么缓存会被重用。但是如果源文件发生了变化,所有后续步骤都将无法复用缓存。所以,在使用 COPYADD 时要注意只复制必要的文件。

总之,合理使用缓存可以显著提高 Docker 镜像的构建效率。通过优化 Dockerfile 的结构和使用缓存技巧,你可以减少构建时间,提高开发效率。

9.镜像的多阶段构建

Dockerfile 的多阶段构建是一种强大的技巧,可以在构建镜像时实现更高效的资源利用和减小镜像大小。它适用于需要在构建过程中生成多个临时文件、编译代码或处理多个步骤的场景。以下是关于镜像的多阶段构建的一些技巧:

1. 减小镜像大小

多阶段构建允许你在不同的构建阶段中创建临时镜像,最终仅将需要的文件和结果复制到最终的镜像中。这样可以避免将构建时产生的临时文件和不必要的依赖包含在最终镜像中,从而减小镜像的大小。

2. 分隔构建过程

将不同的构建步骤放在不同的构建阶段中,使得每个阶段专注于一个特定的任务。这有助于保持 Dockerfile 的可读性和维护性,同时也能更好地控制每个步骤的缓存和重新构建。

3. 使用不同的基础镜像

不同的构建阶段可以使用不同的基础镜像,以适应不同的需求。例如,一个阶段可以使用包含编译工具的基础镜像,而另一个阶段可以使用更轻量的生产环境基础镜像。

4. 使用 COPY --from 复制文件

通过使用 COPY --from 语法,你可以从一个构建阶段复制文件到另一个构建阶段,而不必将整个临时镜像中的文件都复制到最终镜像中。

1
2
3
4
5
6
7
8
9
10
DockerfileCopy code# 第一个阶段:编译
FROM golang:1.16 AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp

# 第二个阶段:最终镜像
FROM alpine:latest
COPY --from=builder /app/myapp /usr/bin/
CMD ["myapp"]

在上面的示例中,最终镜像只包含了编译后的可执行文件,而不包含编译工具和源代码。

5. 清理不必要的文件

在每个构建阶段的结束时,可以使用 RUN 命令来清理不再需要的临时文件、缓存和依赖项,以进一步减小镜像的大小。

总之,多阶段构建是一个优秀的技巧,可以创建更小、更高效的 Docker 镜像。通过在不同的阶段处理不同的任务,你可以将构建过程分割为更小的部分,并在最终镜像中仅包含必要的内容。这对于开发、测试和部署都有很大的好处。