Dockerfile详解:构建云原生应用的基石

370 阅读17分钟

Dockerfile详解:构建云原生应用的基石

前言

在云原生专栏的《从零开始:深入理解容器与 Docker 的云原生之旅》文章中,我们进行了容器的相关介绍,顺带介绍了docker的常用命令,理论概念居多,这篇 文章我们介绍Dockerfile,看完这篇文章,你将学到docker构建缓存、构建上下文、多段构建、Dockerfile常用指令等知识。

创建docker镜像

我们先从一个实际案例出发,笔者之前开发过一个覆盖率统计的服务,当然Dockerfile是运维小伙伴帮忙完成的,就把这个作为案例来讲解。 先附上Dockerfile文件内容,如下

FROM python:3.11

WORKDIR /app

COPY . .

RUN pip install -i https://mirrors.aliyun.com/pypi/simple/ -r requirements.txt

CMD ["python", "main.py"]

先不解释,先执行docker build . (进入Dockerfile所在目录执行,当然也可以通过-f来指定文件路径),来看看镜像构建日志

Step 1/5 : FROM python:3.11
3.11: Pulling from library/python
c6cf28de8a06: Pull complete
891494355808: Pull complete
6582c62583ef: Pull complete
bf2c3e352f3d: Pull complete
a99509a32390: Pull complete
e2199d338e7c: Pull complete
9103dd101567: Pull complete
3fcf68ca7880: Pull complete
Digest: sha256:091e0f5da680e5c972c59cb7eca172141bb6350045b592c284e2fd3bf2916dd9
Status: Downloaded newer image for python:3.11
 ---> dc33876ad8f2
Step 2/5 : WORKDIR /app
 ---> Running in 37a7ff8d7448
Removing intermediate container 37a7ff8d7448
 ---> 80950a9488ec
Step 3/5 : COPY . .
 ---> 941a272ae357
Step 4/5 : RUN pip install -i https://mirrors.aliyun.com/pypi/simple/ -r requirements.txt
 ---> Running in 9307886b1bd9
Looking in indexes: https://mirrors.aliyun.com/pypi/simple/
Collecting alembic==1.13.1 (from -r requirements.txt (line 1))
  Downloading https://mirrors.aliyun.com/pypi/packages/7f/50/9fb3a5c80df6eb6516693270621676980acd6d5a9a7efdbfa273f8d616c7/alembic-1.13.1-py3-none-any.whl (233 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 233.4/233.4 kB 1.8 MB/s eta 0:00:00
Collecting annotated-types==0.6.0 (from -r requirements.txt (line 2))
  Downloading https://mirrors.aliyun.com/pypi/packages/28/78/d31230046e58c207284c6b2c4e8d96e6d3cb4e52354721b944d3e1ee4aa5/annotated_types-0.6.0-py3-none-any.whl (12 kB)
.........
.........
.........
Installing collected packages: websockets, uvloop, typing-extensions, sniffio, smmap, PyYAML, python-dotenv, MarkupSafe, idna, httptools, h11, greenlet, click, annotated-types, uvicorn, SQLAlchemy, pydantic-core, Mako, Jinja2, gitdb, anyio, watchfiles, starlette, pydantic, GitPython, alembic, pydantic-settings, fastapi
Successfully installed GitPython-3.1.42 Jinja2-3.1.3 Mako-1.3.2 MarkupSafe-2.1.5 PyYAML-6.0.1 SQLAlchemy-2.0.27 alembic-1.13.1 annotated-types-0.6.0 anyio-4.3.0 click-8.1.7 fastapi-0.109.2 gitdb-4.0.11 greenlet-3.0.3 h11-0.14.0 httptools-0.6.1 idna-3.6 pydantic-2.6.1 pydantic-core-2.16.2 pydantic-settings-2.2.1 python-dotenv-1.0.1 smmap-5.0.1 sniffio-1.3.0 starlette-0.36.3 typing-extensions-4.9.0 uvicorn-0.27.1 uvloop-0.19.0 watchfiles-0.21.0 websockets-12.0
Removing intermediate container 9307886b1bd9
 ---> 972bc427d351
Step 5/5 : CMD ["python", "main.py"]
 ---> Running in 2f96fca75764
Removing intermediate container 2f96fca75764
 ---> b787138bd6f2
Successfully built b787138bd6f2

从日志中,应该也可以看出点门道来,其实就是按照Dockerfile文件中的指令一步步执行的,下面我们详细探讨一下

构建上下文

构建上下文是指在构建 Docker 镜像时,Docker 守护进程(daemon)所需的文件和目录的集合。通常,构建上下文是指包含 Dockerfile 的目录及其所有子目录和文件。 在运行 docker build 命令时,你需要指定一个路径作为构建上下文。Docker 守护进程将从该路径开始,将所有文件和目录打包并发送到 Docker 守护进程,以供构建使用。 构建上下文的作用包括:

  1. 提供 Dockerfile:构建上下文中必须包含 Dockerfile 文件,Docker 将根据其中的指令来构建镜像。
  2. 提供构建所需的所有文件和目录:Docker 将构建上下文中的所有文件和目录视为构建镜像所需的资源。这包括 Dockerfile 中使用的所有文件、依赖项、配置文件等。
  3. 传输到 Docker 守护进程:构建上下文中的所有文件和目录将被打包,并通过与 Docker 守护进程的通信通道传输到守护进程。

因此,构建上下文是构建过程中所需的所有文件和目录的集合,它影响着构建的效率、资源消耗和最终镜像的大小。通过合理管理构建上下文,可以优化构建过程,提高构建效率,并生成更小的镜像。 所以,我们可以得出一个最佳实战:,把没用的文件要从构建上下文时去除,因为它会导致传输时间长,构建需要的资源多,构建出的镜像大等问题,可以通过.dockerignore文件从编译上下文排除某些文件。

镜像构建日志

先解释一下上文的案例: Dockerfile 文件

FROM python:3.11

该指令指定了基础镜像,本例中是使用 Python 3.11 官方镜像作为基础。

WORKDIR /app

该指令设置了容器内的工作目录为 /app ,后续的指令将在该目录下执行。

COPY . .

该指令将当前目录(即 Dockerfile 所在目录)的所有文件和目录复制到容器内的 /app 目录中。

RUN pip install -i https://mirrors.aliyun.com/pypi/simple/ -r requirements.txt

该指令使用 pip 命令安装在当前目录下的 requirements.txt 文件中列出的 Python 依赖包。 -i 参数指定了使用阿里云镜像源进行安装。

CMD ["python", "main.py"]

该指令定义了容器启动时要执行的默认命令。在本例中,它运行了 python main.py 命令,启动了名为 main.py 的 Python 主程序。

通过以上这些指令,Docker 可以根据 Dockerfile 构建一个镜像,其中包括基础镜像、项目文件和依赖项。 从日志中可以看到,当使用 docker build 命令构建镜像并运行容器时,容器将按照这些指令进行设置和执行。 如果我们再次执行docker build .进行构建,还会像上面那样输出日志吗?试试就知道了,下面是我们再次构建的日志

Step 1/5 : FROM python:3.11
 ---> dc33876ad8f2
Step 2/5 : WORKDIR /app
 ---> Using cache
 ---> 80950a9488ec
Step 3/5 : COPY . .
 ---> Using cache
 ---> 941a272ae357
Step 4/5 : RUN pip install -i https://mirrors.aliyun.com/pypi/simple/ -r requirements.txt
 ---> Using cache
 ---> 972bc427d351
Step 5/5 : CMD ["python", "main.py"]
 ---> Using cache
 ---> b787138bd6f2
Successfully built b787138bd6f2

可以看到,日志简短了很多,仔细看,都是使用的缓存。啥是cache呢?

Build Cache

Docker 构建过程中的缓存是指 Docker 在构建镜像时会尽可能地重用之前构建过程中生成的中间层镜像,以减少重复工作和加快构建速度的机制。这种缓存机制可以在一定程度上提高构建效率,特别是当多次构建相似的镜像时。

Docker 的缓存机制工作原理如下:

  1. 基于指令的缓存 :Docker 在执行 Dockerfile 中的每个指令时,会检查之前是否已经存在具有相同指令的中间层镜像。如果存在,则会尝试重用该中间层镜像,而不是重新执行该指令。这样可以大大减少构建时间。
  2. 指令顺序的重要性 :Docker 会逐条执行 Dockerfile 中的指令,并根据指令的内容和顺序来确定是否可以重用缓存。如果某个指令发生了变化,或者它之前的某个指令发生了变化,那么后续的指令可能无法重用缓存。
  3. --no-cache 选项 :在某些情况下,你可能希望禁用缓存,强制 Docker 重新执行所有指令并构建新的镜像。可以使用 docker build --no-cache 命令来禁用缓存。

从上面原理中,我们又可以得出一个最佳实战:原理中说某条指令发生变好,后续指令都无法使用缓存。那么,为了有效利用缓存,可以将不经常变化的指令放在 Dockerfile 的前面,而将经常变化的指令放在后面。这样可以最大程度地重用缓存,减少构建时间

  • 针对 ADD 和 COPY 指令,Docker 判断该镜像层每一个文件的内容并生成一个 checksum,与现存镜 像比较时,Docker 比较的是二者的 checksum。
  • 其他指令,比如 RUN apt-get -y update,Docker 简单比较与现存镜像中的指令字串是否一致

多段构建

多段构建(Multi-stage build)是一种在 Docker 中使用多个阶段来构建镜像的方法。它允许在单个 Dockerfile 中定义多个构建阶段,并且每个阶段都可以从前一个阶段中继承或复制所需的文件。

多段构建的主要优势是减小最终镜像的大小,因为它可以帮助您排除构建过程中产生的临时文件和不必要的依赖项。通过在不同的阶段中只保留必要的文件和依赖项,可以显著减少镜像的大小,并提高构建效率。

下面是一个使用多段构建的示例,假设我们要构建一个 Node.js Web 应用程序的镜像:

# 第一阶段:构建阶段
FROM node:14 AS builder

WORKDIR /app

COPY package*.json ./

RUN npm install

COPY . .

RUN npm run build

# 第二阶段:运行阶段
FROM nginx:latest

COPY --from=builder /app/dist /usr/share/nginx/html

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

这个 Dockerfile 分为两个阶段:

  1. 第一阶段是构建阶段(builder),基于 Node.js 镜像。在这个阶段,我们将应用程序的源代码复制到容器内,并安装依赖项,然后执行构建命令(如编译、打包等)生成静态文件。
  2. 第二阶段是运行阶段,基于 Nginx 镜像。在这个阶段,我们从构建阶段中复制编译好的静态文件到 Nginx 的默认 HTML 目录,并暴露端口 80。最后,我们使用 CMD 指令运行 Nginx 服务器。

通过使用多段构建,我们可以在第二阶段只保留了编译好的静态文件和 Nginx 服务器,而不包含构建工具和源代码等临时文件。从而避免了不必要的文件和依赖项进入最终的镜像中,减小了镜像的体积。

Dockerfile 常用指令

当编写 Dockerfile 时,可以使用多种指令来定义构建镜像的过程。以下是一些常用的 Dockerfile 指令的介绍,以及相应的示例说明:

  1. FROM:指定基础镜像,用于构建当前镜像。

    FROM ubuntu:latest
    
  2. RUN:在镜像中执行命令。可以用于安装软件包、运行脚本等操作。这两条命令应该永远用&&连接,如果分开执行,RUN apt-get update 构建层被缓存,可能会导致新 package 无法安装

    RUN apt-get update && apt-get install -y curl
    
  3. COPY:将文件从构建环境复制到镜像中的指定位置。

    COPY app.py /app/
    
  4. ADD:类似于 COPY,但还支持 URL 和解压缩功能。不推荐使用 ADD 复制本地文件,而推荐使用 COPY。

    ADD https://example.com/file.tar.gz /tmp/
    
  5. WORKDIR:设置工作目录,后续的指令会在该目录下执行。

    WORKDIR /app
    
  6. ENV:设置环境变量。

    ENV PORT=8080
    
  7. EXPOSE:声明容器运行时监听的端口。是镜像创建者和使用者的约定

    EXPOSE 9898
    
  8. CMD:指定容器启动后要执行的命令。可以有多个 CMD,但只有最后一个生效。

    CMD ["python", "app.py"]
    
  9. ENTRYPOINT:指定容器启动时要执行的可执行文件或命令。与 CMD 不同,ENTRYPOINT 的参数不会被覆盖,而是作为参数传递给 ENTRYPOINT 指定的命令。如果用户在运行容器时提供了命令参数,这些参数会被附加到 ENTRYPOINT 指定的命令后面。

    ENTRYPOINT ["echo", "Hello, World!"]
    
  10. VOLUME:声明持久化存储的挂载点。

    VOLUME /data
    

除了上述介绍的常用指令外,还有其他一些在 Dockerfile 中的其他指令。

  1. ARG:定义构建过程中使用的变量。可以通过 --build-arg 标志传递值给这些变量。

    ARG VERSION=latest
    
  2. LABEL:为镜像添加元数据,如作者、版本等信息。

    LABEL maintainer="user@example.com" description="Docker image for your Python application",
    
  3. USER:指定运行容器时使用的用户名或 UID。

    USER appuser
    
  4. HEALTHCHECK:定义容器的健康检查机制。

    HEALTHCHECK --interval=30s --timeout=3s \
      CMD curl -f http://localhost/ || exit 1
    
  5. SHELL:设置执行 RUN、CMD 和 ENTRYPOINT 的默认 Shell。

    SHELL ["/bin/bash", "-c"]
    
  6. ONBUILD:在后续构建中触发特定的操作。当当前镜像被用作另一个镜像的基础镜像时,ONBUILD 指令会在后续构建中执行。

    ONBUILD COPY . /app
    
  7. STOPSIGNAL:设置容器停止时发送的系统调用信号。

    STOPSIGNAL SIGTERM
    

ENTRYPOINT的最佳实战:用 ENTRYPOINT 定义镜像主命令,并通过 CMD 定义主要参数,像如下这样

ENTRYPOINT ["python", "main.py"]

CMD ["--help"]

最佳实践

我们的目标:易管理、镜像小、层级少、利用缓存。展开说说

  • 不要安装无效软件包。

  • 最小化层级数

    • 最新的 docker 只有 RUN, COPY,ADD 创建新层,其他指令创建临时层,不会增加镜像大小。 • 比如 EXPOSE 指令就不会生成新层。
    • 多条 RUN 命令可通过连接符连接成一条指令集以减少层数。
    • 通过多段构建减少镜像层数。
  • 把多行参数按字母排序,可以减少可能出现的重复参数,并且提高可读性。

  • 编写 dockerfile 的时候,应该把变更频率低的编译指令优先构建,有效利用 build cache。

  • 复制文件时,每个文件应独立复制,这确保某个文件变更时,只影响改文件对应的缓存。

镜像管理

当涉及到 Docker 镜像的导入、导出和远程仓库的使用时,以下几个命令是非常有用的:

  1. docker save/load:

    • docker save 命令用于将一个或多个镜像保存为一个 tar 归档文件。这个归档文件可以被传输到其他机器上,然后使用 docker load 命令加载为镜像。

    • 例子:

      docker save -o my_images.tar image1 image2  # 将镜像保存为 my_images.tar 文件
      docker load -i my_images.tar  # 加载从文件中导出的镜像
      
  2. docker tag:

    • docker tag 命令用于给本地镜像设置新的标签,通常用于准备将镜像推送到远程仓库。

    • 例子:

      docker tag my_image:latest my_repo/my_image:v1.0  # 为本地镜像添加新的标签
      

      这条命令会将本地的 myimage:latest 镜像添加一个新的标签 myrepo/myimage:1.0。

  3. docker push/pull:

    • docker push 命令用于将本地镜像推送到远程仓库,而 docker pull 命令则用于从远程仓库拉取镜像到本地。

    • 例子:

      docker push my_repo/my_image:1.0  # 将本地镜像推送到远程仓库
      docker pull my_repo/my_image:1.0  # 从远程仓库拉取镜像到本地
      

我们可以构建镜像时使用-t参数指定REPOSITORY 和 TAGdocker build -t myapp:1.0 .

创建私有镜像仓库

要创建私有镜像仓库并将镜像推送到该私有仓库,可以按照以下步骤进行操作:

  1. 安装 Docker Registry:

    • 首先,您需要在您的服务器上安装 Docker Registry。Docker Registry 是 Docker 官方提供的用于存储和管理 Docker 镜像的开源服务。

    • 可以使用以下命令拉取官方的 Registry 镜像:

      docker pull registry
      
  2. 运行私有镜像仓库:

    • 使用以下命令在服务器上运行私有镜像仓库:

      docker run -d -p 5000:5000 --restart=always --name registry registry
      
    • 这会在本地的 5000 端口启动一个私有镜像仓库。

  3. 标记镜像:

    • 在推送镜像之前,您需要为要推送的镜像添加私有仓库地址的前缀。

    • 使用 docker tag 命令为镜像添加标签:

      docker tag my_image:latest localhost:5000/my_image:latest
      
  4. 登录并推送镜像:

    • 登录到私有镜像仓库:

      docker login localhost:5000
      
    • 输入用户名和密码登录后,即可推送镜像:

      docker push localhost:5000/my_image:latest
      
  5. 验证镜像是否推送成功:

    • 您可以通过浏览器或者使用 curl 命令来验证私有镜像仓库中是否成功推送了镜像:

      curl http://localhost:5000/v2/_catalog
      

官方提供的公共仓库:hub.docker.com/

最后

相信学完这篇文章,你要为自己的服务构建一个镜像,自己也可以写Dockerfile,是不是爽歪歪。距离云原生出师又近了一步。