docker自动化
了解背景
Docker 是一种轻量级的容器化技术,主要用于应用程序的环境迁移和一致性。你需要知道下面的基本概念:
- 了解镜像、容器的基本概念:https://yeasy.gitbook.io/docker_practice/basic_concept
- 数据卷的基本概念:https://yeasy.gitbook.io/docker_practice/data_management/volume
- 网络方面,端口映射、docker compose 容器互联:https://yeasy.gitbook.io/docker_practice/network/port_mapping
OK,上面三个基本概念,你就足够使用 docker 了,然后我们来了解 docker 最实用的地方:
- 环境迁移,在不同的机器上保持一致性,一次创建或配置,可以在任意地方正常运行。。
- 直接在 docker 内利用 linux 环境和路径挂载来开发。
- 隔离依赖、统一开发环境、快速启动。
- 利用 Linux 容器作为开发环境:在不同平台(Windows、Mac、Linux)上保持一致的开发环境。
- 通过 volumes 实现容器与主机的文件共享,利用挂载路径在本地编辑代码,并在容器中即时运行。典型场景:在容器中运行应用,同时在本地编辑源代码(热重载),也可以使用 vscode remote.
docker 还有更加高级的配置,比如说可以让容器直接绑定网卡,处理网络包的数据,进行网卡级别的数据转发工作。这在软路由或者旁路由中用处比较多。这里会涉及到很多的细节,比如使用网卡的时候,和 host 的网络冲突和隔离等。作为开发和运行环境,暂时不会接触到这些。
用起来
Dockerfile
这是镜像的配置文件,教程可以参考:https://yeasy.gitbook.io/docker_practice/image/build ,我们会更多的从例子学习。
1 | # syntax=docker/dockerfile:1.4 # 指定Dockerfile的语法版本,确保使用最新功能 |
基础命令
- FROM 设置基础镜像,
- ENV 设置环境变量,构建和运行的时候都会生效。
- WORKDIR 指定工作目录,
USER
则是改变之后层的执行RUN
,CMD
以及ENTRYPOINT
这类命令的身份。- COPY 复制项目文件到镜像,
EXPOSE
指令是声明容器运行时提供服务的端口,这只是一个声明,在容器运行时并不会因为这个声明应用就会开启这个端口的服务。在 Dockerfile 中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另一个用处则是在运行时使用随机端口映射时,也就是docker run -P
时,会自动随机映射EXPOSE
的端口。- CMD 主要用于定义容器启动时的默认命令或参数,可以被用户在 docker run 命令行中覆盖。注意建议使用这种列表的格式,因为能够避免一些多重引号的冲突。
- ENTRYPOINT:用于固定的默认启动,指定了一个容器启动时必须执行的命令,这个命令不容易被覆盖,除非使用 --entrypoint 选项。应该放在 CMD 前面,并且用于完成固定的初始化任务和运行服务。
HEALTHCHECK
指令是告诉 Docker 应该如何进行判断容器的状态是否正常,-interval=<间隔>
:两次健康检查的间隔,默认为 30 秒;-timeout=<时长>
:健康检查命令运行超时时间,如果超过这个时间,本次健康检查就被视为失败,默认 30 秒;-retries=<次数>
:当连续失败指定次数后,则将容器状态视为unhealthy
,默认 3 次。
多级构建
这里采用了多阶段构建,builder 这个镜像实际上只使用了 /root/.local 里构建好的东西,这个镜像会保存作为缓存,但是不会用于运行应用。python:3.9-slim 是实际运行应用的镜像,因为构建时安装的很多依赖,在运行的时候不需要。这么做有如下好处:
-
减少镜像大小:builder 阶段安装了所有开发工具和依赖,但它不会包含在最终的运行镜像中。最终镜像只包含最少的运行环境和应用程序本身。例如,python:3.9-slim 镜像的体积非常小(约 55MB),相比于完整的 python:3.9 镜像(约 885MB),这在部署生产环境时极大减少了资源占用。
-
提高构建速度:通过 Docker 的缓存机制,当你修改代码但没有修改依赖时,Docker 只需要重新复制代码,而不必重新安装依赖。如果依赖没有变化,Docker 会使用 builder 阶段的缓存,加速后续的构建。
构建、运行、停止、删除容器
构建命令:
1 | docker build -f docker/Dockerfile -t my-app .. |
这里指定构建的镜像的命名,还有上下文 ..
是上级目录,会作为相对路径的参考。-t 选项为生成的镜像指定一个名称,果你想要指定版本或标签,可以这样使用 my-app:1.0
构建镜像后,你可以通过 docker run 命令来运行容器。
1 | docker run -d -p 8080:80 \ |
-d:-d 表示容器将在后台运行(“detached mode”),即以守护进程的方式运行。这对于运行长期服务(如 Web 服务器)很有用。
-p 8080:80:-p 选项将宿主机的端口映射到容器的端口。在这个例子中,将宿主机的 8080 端口映射到容器的 80 端口,假设你的应用在容器内的 80 端口上运行。访问 http://localhost:8080 就相当于访问容器内的服务。
-v 是挂载文件到容器,
-name 是自定义容器的名字,
-name my-app-container:为容器指定一个名称,方便后续操作和管理。你可以通过容器名称来启动、停止、删除或查看日志。如果不指定名称,Docker 会随机生成一个名称。
my-app:这是刚才构建的镜像名称。Docker 会基于这个镜像启动容器。
这里是运行默认的命令,我们可以在 my-app 后面加上自己的命令,用于自定义的运行。例如 docker run -d -p 8080:80 --name my-app-container my-app python [app.py](http://app.py/)
如果希望交互式的运行,
1 | docker run -it --name my-app-container my-app bash |
-it:-i 表示交互模式,-t 表示为容器分配一个伪终端。使用这两个选项可以进入容器并与容器进行交互。
bash:启动容器时运行 bash,这样你可以获得容器内的命令行。
当你不再需要容器时,可以通过以下命令停止容器:
1 | docker stop my-app-container |
docker stop:停止一个运行中的容器。你可以通过容器的名称或 ID 来指定它。
当你不再需要容器时,可以通过以下命令删除容器:
1 | docker rm my-app-container |
导入密钥的最佳实践
有时候容器里需要导入密钥来运行某些应用,一般的做法可以:
- 使用卷挂载,然后读取,可以参考之前的命令。
- 运行的时候指定环境变量,可以参考之前的命令。
- 使用 docker compose 来运行
❗ 不要在 dockerfile 里写入隐私的密钥,环境变量是可以查看的
Docker compose
Compose
定位是 「定义和运行多个 Docker 容器的应用(Defining and running multi-container Docker applications)」,其前身是开源项目 Fig。一个 Dockerfile
模板文件,可以让用户很方便的定义一个单独的应用容器。然而,在日常工作中,经常会碰到需要多个容器相互配合来完成某项任务的情况。例如要实现一个 Web 项目,除了 Web 服务容器本身,往往还需要再加上后端的数据库服务容器,甚至还包括负载均衡容器等。
Compose
恰好满足了这样的需求。它允许用户通过一个单独的 docker-compose.yml
模板文件(YAML 格式)来定义一组相关联的应用容器为一个项目(project)。
Compose
中有两个重要的概念:
- 服务 (
service
):一个应用的容器,实际上可以包括若干运行相同镜像的容器实例。 - 项目 (
project
):由一组关联的应用容器组成的一个完整业务单元,在docker-compose.yml
文件中定义。
Compose
的默认管理对象是项目,通过子命令对项目中的一组容器进行便捷地生命周期管理。
编写 docker compose
主要规定了每个服务怎么启动,以及依赖关系。学习下面的例子,模仿配置的格式:
- container_name,image,ports 这些都是固定的写法。
- 默认从当前目录的 .env 文件读取环境变量,并且相对路径是相对命令行所在的命令,而不是 dockerfile 里那样,手动设置的上下文。所以从哪里启动 docker compose,是比较重要的。
- healthcheck 也比较常用,因为其他服务可能依赖这个服务正常运行,所以设置了检查健康的方式。用的方式是命令的返回码为 0。比如 curl 返回结果非 2xx,或者超时;命令行命令执行异常。
volumes
有两种类型,一种是卷,类似 esdata01,直接挂载到目录。一种是本地文件,用相对位置寻找,./init.sql:/docker-entrypoint-initdb.d/init.sql,有些特定位置的文件会执行特定操作。比如说当数据库未初始化时,会执行。
对于项目,build 会根据指定的 dockerfile 构建,得到镜像。但是除了 build 之外 context 路径毫无影响,其他的配置都是用命令行路径作为相对路径的参考。如果要设置默认的命令,可以参考
1 | entrypoint: ["python", "/prompt-engine/src/main.py"] |
1 | services: |
常用命令
- docker-compose up命令用于构建和启动所有在 docker-compose.yml 中定义的服务。
作用:构建镜像(如果需要),并启动所有的服务。启动后,所有服务会在前台显示日志,直到你停止它们。
-d:在后台启动容器(detached mode)。
–build:强制重新构建镜像。 - docker-compose down 删除所有容器,并且会删除由 Compose 创建的网络。
–volumes:同时删除与容器关联的持久化数据卷。 - docker-compose build 命令用于显式地构建所有服务的镜像,而不启动容器。
–no-cache:不使用缓存,强制重新构建镜像。
–pull:在构建时拉取最新的基础镜像。 - docker-compose run 命令用于在一个特定的服务上运行一个命令,通常适用于需要交互式调试或运行一次性任务的场景。它只会启动你指定的服务,而不是整个 docker-compose.yml 中的所有服务。–rm:在命令执行完毕后自动删除容器,这对于命令行工具或一次性运行的任务非常有用。
1
docker-compose run --rm prompt-engine python /prompt-engine/src/main.py -fpath projects/data -id 1234 -cmd detect -o output/result.xlsx
-d:后台运行容器,适合需要持久运行的服务(但不适合一次性任务)。 - docker-compose start 和 docker-compose stop 这些命令用于启动和停止已经存在的服务容器。
- docker-compose exec 命令允许你进入正在运行的容器,执行命令或者开启交互式终端。它类似于 docker exec,但在 Docker Compose 环境中使用。
1
docker-compose exec prompt-engine bash
- docker-compose config 来查看 Compose 文件中定义的环境变量和配置。
最佳实践
常规加快构建
选择合适的基础镜像。基础镜像的选择对镜像大小和应用性能影响很大。通常建议选择精简版镜像,如 python:3.9-slim 或 alpine,以减小镜像大小,减少冗余的包依赖。镜像的东西越少,安全漏洞越少。
使用多阶段构建。这个参考前面的章节。
避免不必要的层。因为每一层都是有缓存的,层越多,复杂性极速上升。一般来说做法如下:
-
合并要执行的 shell 命令,并且在执行完命令后删除不必要的缓存和依赖。
1
2
3
4
5
6
7
8
9
10# 不优化的写法:每个 RUN 都会创建一个新层
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git
RUN rm -rf /var/lib/apt/lists/* # 清理缓存
# 优化的写法:合并到一个 RUN 中,只创建一个层
RUN apt-get update && \
apt-get install -y curl git && \
rm -rf /var/lib/apt/lists/* -
Copy 指令越少越好,合并起来,而且只 copy 必要的文件。
1
2
3
4
5
6# 不优化的写法:每个 COPY 都会创建一个新层
COPY file1.py /app/
COPY file2.py /app/
# 优化的写法:合并到一个 COPY 指令,减少层数
COPY file1.py file2.py /app/ -
**将不常变动的依赖放在前面。**Docker 使用分层缓存,如果某一层没有变化,Docker 会重用缓存层。将依赖安装、基础配置等不常变化的指令放在 Dockerfile 的前面,而将代码的 COPY 操作放在后面,这样可以避免每次修改代码时重新安装依赖。
1
2
3
4
5
6# 先安装依赖(通常不变动)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 再复制应用代码(经常变化)
COPY . . -
**使用 --no-install-recommends 安装软件包。**使用 apt-get install 时,默认会安装许多推荐包(recommends),但这些推荐包可能并不是必要的。通过 --no-install-recommends 选项,你可以避免安装不需要的软件包。
1
2
3RUN apt-get update && \
apt-get install -y --no-install-recommends curl git && \
rm -rf /var/lib/apt/lists/*
使用BuildKit
额外缓存
手动增加缓存数据,除了 volumes 这种持久化的给挂载给容器的数据,还有一些用于加速构建的特殊缓存设置。这些缓存执行完命令就会卸载,因此不会增加容器大小。并且缓存还会自动的更新。
1 | RUN npm i --registry=https://registry.npm.taobao.org \ |
以上的改变,在执行每个 RUN 的时候,都会挂载缓存数据到 target 目录,然后再去执行。
在多阶段构建的时候,还有特殊优化,用于获取上一次构建产生的缓存,而不是直接 COPY。这和使用 id 标识缓存的用途是不一样的,但是实际效果差不多。
1 | RUN --mount=type=cache,target=/tmp/dist,from=builder,source=/app/dist \ |
传递多级构建的产物
类似这样的代码,多级构建的时候可以挂载上一级的产物。和 cache 的区别在于,它不会缓存。
1 | # syntax = docker/dockerfile:experimental |
优化 IO 密集的临时操作
tmpfs 是一种基于内存的文件系统,数据存储在 RAM(随机存取存储器)中,而不是持久存储设备(如硬盘)。在 Linux 系统中,/tmp 目录通常使用 tmpfs。
那么构建过程中有很多的临时数据读写,而且不大(不超过物理内存的一半),执行完了就不需要,那么就比较时候。
1 | RUN --mount=type=tmpfs,target=/tmpfs \ |
一般的场景有:
- 编译中间文件:如果在构建过程中有些中间文件(如编译产物)只需要暂时存储,且不需要在构建结束后保留,tmpfs 是很好的选择。Rust 就很多的那种 target 中间文件。
- 解压缩与临时数据处理:先把数据解压到 tmpfs,然后再使用。或者下载到 tmpfs,再操作。
这也是许多脚本选择下载和解压的位置是 /tmp
的原因。
挂载密钥文件
这个的特点就是,不会缓存,执行 RUN 结束之后就消失了。它的特点就是,这个文件处于隐私文件系统,构建的时候安全性更高。
1 | # syntax = docker/dockerfile:experimental |
构建的时候要提供密钥: docker build --secret id=aws,src=$HOME/.aws/credentials -t my-app .
1 | services: |