工程实践:对已有的Docker镜像进行增量更新

近期的工作中有这么一个场景:项目代码需要进行频繁的更新,并打包成镜像在客户现场进行部署。由于需要使用CUDA的镜像,因此每次打包完的tar包都有13G之多。为了解决每次更新都会出现的传输过慢问题,我们采用了大镜像包+小更新镜像包的方式。

思路很简单,就是先构建一个大的基础包,每次将需要更新的内容传进容器,再build一个新版本。

基础镜像

首先,我们有一个基础镜像的服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
version: '3.1'

services:
# cuda_base:
# platform: linux/amd64
# build:
# context: ../infers
# dockerfile: Dockerfile.base
# image: behavior-detector-cuda-base:${TAG}

frontend:
platform: linux/amd64
build:
context: ../frontend
dockerfile: Dockerfile
image: behavior-detector-frontend:${TAG}

infers:
platform: linux/amd64
build:
context: ../infers
dockerfile: Dockerfile
args:
- TAG=${TAG}
image: behavior-detector-infers:${TAG}
# depends_on:
# - cuda_base

backend:
platform: linux/amd64
build:
context: ../backend
dockerfile: Dockerfile
args:
- TAG=${TAG}
image: behavior-detector-backend:${TAG}
depends_on:
- frontend

通过构建以上的compose文件,就能得到前后端和推理端的基础镜像。

更新包脚本

我们通常在项目的各个路径下进行开发。当我们把infers或者backend开发完毕后,就需要对这两个项目的文件夹进行打包。为此,我们定义一个save_patch.sh来将内容进行打包。

1
2
3
4
5
6
7
8
9
10
11
#! /usr/bin/env bash
PROJECT_DIR=$(cd `dirname $0`/..; pwd)

. .env
# 加载.env文件 设置临时环境变量

# 获取上一层目录的绝对路径

tar -cvf infers_patch_${TAG}.${PATCH_VERSION}.tar -C ${PROJECT_DIR}/infers/ .

tar -cvf backend_patch_${TAG}.${PATCH_VERSION}.tar -C ${PROJECT_DIR}/backend/ .

上面的脚本做了两件事,读取配置文件;将推理和后端的文件夹infersbackend打成tar包。

加载包脚本

当我们拿到了更新包以后,就需要将包导入到已有的镜像,并发布一版新镜像。这里给出一个load_patch.sh

1
2
3
4
5
6
7
8
9
10
11
#! /usr/bin/env bash

# 加载.env文件 设置临时环境变量
. .env

# 当前目录
CURRENT_DIR=$(cd `dirname $0`; pwd)

\cp -rf ${CURRENT_DIR}/infers_patch_${TAG}.${PATCH_VERSION}.tar infers_patch.tar
\cp -rf ${CURRENT_DIR}/backend_patch_${TAG}.${PATCH_VERSION}.tar backend_patch.tar
docker compose -f docker-compose.patch.yaml build

这里做的几件事:将更新包复制成infers_path.tarbackend_patch.tar。这两个包会被之后的compose文件使用来构建镜像。

Docker Compose Patch

构建更新后的镜像需要用到docker compose.patch.yaml文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
version: '3.1'

services:
infers:
platform: linux/amd64
# 定义构建的上下文
build:
context: .
dockerfile: Dockerfile.infers.patch
args:
- TAG=${TAG}
- PATCH_VERSION=${PATCH_VERSION}
# 定义构建的镜像标签
image: behavior-detector-infers:${TAG}.${PATCH_VERSION}

backend:
platform: linux/amd64
build:
context: .
dockerfile: Dockerfile.backend.patch
args:
- TAG=${TAG}
- PATCH_VERSION=${PATCH_VERSION}
image: behavior-detector-backend:${TAG}.${PATCH_VERSION}

当我们build的时候,就会生成两个镜像:behavior-detector-infersbehavior-detector-backend

更新包Dockfile

项目中频繁更新的部分是infersbackend容器,因此为这两个部分的更新包单独写Dockerfile。

后端更新包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ARG TAG=1.0.0
ARG PATCH_VERSION=1
FROM behavior-detector-backend:${TAG}

# 设置工作目录
WORKDIR /backend_app

# 删除目录下的文件
RUN rm -rf /backend_app/*

# 复制项目文件
COPY backend_patch.tar /backend_app/
RUN tar xvf /backend_app/backend_patch.tar -C /backend_app

# 暴露端口号
# 80 前端服务
EXPOSE 80
# python 服务
EXPOSE 7091
EXPOSE 7092
EXPOSE 7093

# 启动 FastAPI 应用
CMD ["bash", "start.sh"]

推理更新包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ARG TAG=1.0.0
ARG PATCH_VERSION=1
FROM behavior-detector-infers:${TAG}

# 设置工作目录
WORKDIR /infer_app

# 复制项目文件
RUN rm -rf /infer_app/*

COPY infers_patch.tar /infer_app/
RUN tar xvf /infer_app/infers_patch.tar -C /infer_app

# 暴露端口
EXPOSE 58090

# 默认命令
CMD ["bash", "start.sh", "prod"]

Docker Compose Prod

最后还需要有一个实际部署的docker compose文件,这个文件是实际真正部署的容器镜像:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
version: '3.1'
services:
redis-service:
image: redis:latest
restart: always
ports:
- "6379:6379"

detector-service-api:
image: behavior-detector-backend:${TAG}.${PATCH_VERSION}
restart: always
environment:
- RUNTIME_ENV=prod
- RUNTIME_SERVICE_TYPE=api

- POSTGRESQL_HOST=xxxx
- POSTGRESQL_PORT=xxxx
- POSTGRESQL_DBNAME=xxxx
- POSTGRESQL_USER=xxxx
- POSTGRESQL_PASSWD=xxxx

- REDIS_HOST=xxxx
- REDIS_PORT=6379
- REDIS_PASSWD=

volumes:
- /app/logs:/app/logs
- /app/captures:/app/captures
- /app/targets:/app/targets
- /app/html:/app/html

ports:
- "7901:80"

depends_on:
- redis-service

detector-service-stream:
image: behavior-detector-backend:${TAG}.${PATCH_VERSION}
restart: always
environment:

- RUNTIME_ENV=prod
- RUNTIME_SERVICE_TYPE=stream

- POSTGRESQL_HOST=xxxx
- POSTGRESQL_PORT=xxxx
- POSTGRESQL_DBNAME=xxxx
- POSTGRESQL_USER=xxxx
- POSTGRESQL_PASSWD=xxxx

- REDIS_HOST=xxxx
- REDIS_PORT=6379
- REDIS_PASSWD=

volumes:
- /app/logs:/app/logs
- /app/captures:/app/captures
- /app/targets:/app/targets
- /app/html:/app/html

depends_on:
- redis-service

staytime:
image: behavior-detector-infers:${TAG}.${PATCH_VERSION}
restart: always
ports:
- "58093:58090"
volumes:
- /app:/app
environment:
- ENV_MODEL_NAME=staytime
- ENV_GUNICORN_WORKERS=1
- ENV_CUDA_DEVICE=0
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [ gpu ]

实际使用中,使用docker compose -f docker-compose.prod.yaml up -d即可进行部署。

2024/4/22 于苏州