Docker 从 0 到 1 再到 Kubernetes 实战:第 5 篇 Dockerfile 最佳实践与多阶段构建

IT策士 10余年一线大厂经验,专注 IT 思维、架构、职场进阶。我会在各个平台持续发布最新文章,助你少走弯路。

在上一篇中,我们编写了第一个 Dockerfile,把 Flask + Redis 计数器应用成功容器化。但那个 Dockerfile 虽然能跑,距离"生产级"还有一段距离------构建速度能不能更快?镜像体积能不能更小?缓存命中率能不能更高?

这一篇,我们就来系统性地回答这些问题。先复习一下我们已有的成果,然后在此基础上做一次深度优化。

一、从第4篇的成果出发

回顾一下上一篇我们构建的镜像:

bash 复制代码
docker images flask-redis-counter

输出:

bash 复制代码
REPOSITORY            TAG       IMAGE ID       CREATED          SIZE
flask-redis-counter   1.0       e8f9a0b1c2d3   2 hours ago      138MB

138MB 的镜像能正常运行:启动 Redis、连接 Flask、计数器工作正常。但往深处想:这 138MB 里到底装了什么?有没有可以精简的空间?构建过程是否足够快?

bash 复制代码
docker history flask-redis-counter:1.0
bash 复制代码
IMAGE          CREATED         SIZE      COMMENT
e8f9a0b1c2d3   2 hours ago     3.5kB     COPY --chown=appuser:appuser . .
a1b2c3d4e5f6   2 hours ago     12MB      RUN pip install ...
...
<missing>      3 weeks ago     120MB     python:3.12-slim 基础镜像

120MB 的基础镜像 + 约 18MB 的应用层------基础镜像占了大头。最底层的 python:3.12-slim 虽然已经比完整版小很多,但仍然携带了一个完整的 Debian 用户空间:shell、包管理器(apt)、系统工具以及大量我们根本不会在生产中调用的库文件。据统计,许多基于 Debian 的 Python 容器运行时使用率不足 35%,超过 65% 的软件包在生产环境中从未被调用。这些冗余文件不仅增加存储开销,还直接扩大了安全攻击面------扫描器会为它们报出一大堆与你业务无关的 CVE 漏洞。

镜像臃肿不是"多占点磁盘"这么简单。在 Kubernetes 环境中,大镜像意味着:docker push/pull 在网络中传输更慢,节点内存被更多冗余数据挤压,Pod 启动时镜像解压耗时更长,滚动更新变慢甚至触发超时回滚。有团队将 Spring Boot 应用镜像从 1.2GB 缩减至 150MB 后,部署效率提升了 80%。

那么,镜像瘦身到底从哪里入手?答案就在 Dockerfile 的写法上。下面我们从最根本的原理出发,逐步拆解优化路径。

二、分层的代价:为什么"能跑"不等于"够好"

第3篇我们讲过,Docker 镜像由多个只读层叠加而成,每一条 RUNCOPYADD 指令都会生成一个新层。层一旦创建就不可修改------哪怕你在后续的层中删除了文件,那些文件仍然占据着之前层的存储空间。这就是 Docker 分层结构的"代价":冗余文件一旦写入,无法从镜像历史中清除。

用一个例子说明。假设你写了这样的 Dockerfile:

bash 复制代码
FROM python:3.12-slim
WORKDIR /app
# 第 1 层:拉取 apt 缓存
RUN apt-get update
# 第 2 层:安装 gcc
RUN apt-get install -y gcc
# 第 3 层:删除 gcc
RUN apt-get remove -y gcc

你以为删掉了 gcc?实际上,apt-get install 那一层永久包含了 gcc 及其所有依赖文件。apt-get remove 只是在第 3 层"标记删除",下层的数据并不会消失。最终拉取镜像时,你仍然要把 gcc 的全部字节下载到本地。

于是我们发现:Dockerfile 的每一条指令都是一笔不可撤销的存储开销,优化 Dockerfile 的本质,就是把每一层的"贡献"压到最小,把"变更频率低"的指令排到前面。

三、传统单阶段构建 vs 多阶段构建

3.1 传统单阶段构建的问题

在早期 Docker 实践中,所有指令都在同一个构建容器里执行:安装依赖、编译代码、打包应用,最后直接启动。结果就是:最终镜像不仅包含应用本身,还包含了整个构建工具链(编译器、包管理器缓存、中间产物、源代码等)。这些工具在运行时完全用不上,但却显著增大了镜像体积,扩大了攻击面。

3.2 多阶段构建的核心思想

Docker 17.05 引入了多阶段构建(Multi-stage Build),彻底改变了这一局面。核心思想很简单:把"构建环境"和"运行环境"分开。

bash 复制代码
FROM heavy-toolchain-image AS builder
# 在这里尽情使用编译器、构建工具、测试框架......
# 最终产出一个干净的"制品"(如 .jar / .whl / 静态二进制)

FROM minimal-runtime-image
COPY --from=builder /path/to/artifact /app
# 最终镜像只包含运行时必需品
  • AS builder:给第一阶段命名,方便后续引用

  • COPY --from=builder:从指定阶段选择性复制文件,其他所有构建中间产物(包括源码、编译器、缓存)都留在 builder 阶段,不会进入最终镜像

这个机制可以用一句话概括:第一阶段脏活累活随便干,只把最终干净的成果传给下一阶段。

3.3 对比实验:单阶段 vs 多阶段

按照第4篇的写法,我们把所有工作塞进一个 Dockerfile:

bash 复制代码
# ==================== 反模式:单阶段构建 ====================
FROM python:3.12-slim

# 安装构建依赖(gcc、python3-dev------运行时根本不需要)
RUN apt-get update && \
    apt-get install -y --no-install-recommends gcc python3-dev && \
    rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .

RUN groupadd -r appuser && useradd -r -m -u 1000 -g appuser appuser
USER appuser
EXPOSE 5000
CMD ["python", "app.py"]

构建并查看体积:

bash 复制代码
docker build -t flask-redis-single:1.0 -f Dockerfile.single .
docker images flask-redis-single
bash 复制代码
REPOSITORY           TAG       IMAGE ID       SIZE
flask-redis-single   1.0       f1a2b3c4d5e6   185MB

相比之下,第4篇的多阶段版本通过 pip wheel 预编译 + COPY --from=builder 分离构建依赖,最终镜像约 138MB,单阶段构建则达到 185MB,增幅超过 30%。这还只是一个小型 Python 应用的差距。对于 Java 或 Go 项目,多阶段构建的优势更为显著------Spring Boot 应用镜像体积可直接从 800MB 以上降至 150MB 以内,而 Go 项目甚至可将镜像从 1.2GB 压缩至 12MB。以下是单阶段与多阶段方案在各项指标上的直观对比:

多阶段构建不仅压缩了体积,更关键的是:最终镜像中不再包含 gcc、python3-dev 等编译工具。这意味着攻击面直接减少了编译工具链可能引入的所有 CVE 风险。

四、Dockerfile 最佳实践全景

除了多阶段构建,还有一些原则能帮助你把镜像打磨得更高效。

4.1 选择合适的基础镜像

基础镜像是所有层的"地基",选对了能省掉大量体积。以下是常见 Python 基础镜像的体积与安全特性对比:

选型原则:优先使用 slim 镜像作为默认选择。它基于 Debian 且使用标准 glibc,兼容绝大多数 Python 包,是目前最稳妥的生产级选择。Alpine 使用 musl libc 而非 glibc,某些包含 C 扩展的 Python wheel 包在 Alpine 上可能安装失败。Distroless 移除了 shell 和包管理器,安全攻击面最小,但调试不便(无法 docker exec 进入容器),适合有成熟可观测性体系的生产团队。有团队将镜像从标准 Debian 切换到 Distroless 后,CVE 数量从 53 个降至 10 个以内。

一个小实验:你可以试试把第4篇的 python:3.12-slim 换成 python:3.12-alpine,看看构建过程是否出现编译错误------这是理解 Alpine musl libc 兼容性问题最快的方式。

4.2 锁定基础镜像版本

避免使用 latest 标签,因为其指向的版本会随时间变化,可能引入不兼容的变更。在生产环境中推荐同时锁定版本和摘要(Digest):

bash 复制代码
# ❌ 不推荐:版本可漂移
FROM python:latest

# ✅ 推荐:锁定具体版本
FROM python:3.12.4-slim

# ✅ 更严格:版本 + 摘要双锁定(成熟流水线)
FROM python:3.12.4-slim@sha256:a04c2f8c...

4.3 指令排序:把"不变"放前面,最大化缓存命中

Docker 构建时逐层检查缓存:如果某一层的指令和输入都未变化,则复用缓存层;一旦某层失效,该层之后的所有层都必须重新构建。因此,变更频率越低的指令越应该放在 Dockerfile 前面。

bash 复制代码
# ✅ 推荐写法:依赖安装在前,代码复制在后
FROM python:3.12-slim
WORKDIR /app

# 第 1 步:复制依赖清单(很少变动)
COPY requirements.txt .
# 第 2 步:安装依赖(只有 requirements.txt 变了才重跑)
RUN pip install --no-cache-dir -r requirements.txt
# 第 3 步:复制源代码(最频繁变动,放在最后)
COPY . .

如果你的项目源码改了但 requirements.txt 没变,第 1-2 步直接走缓存,只有第 3 步重新构建------耗时从数分钟压缩到几秒。对比一下反模式写法:

bash 复制代码
# ❌ 反模式:先 COPY 所有代码,再安装依赖
COPY . .
RUN pip install -r requirements.txt
# 后果:代码改一个字,COPY 层缓存失效,pip install 也要重跑

这一调整能将大部分日常构建时间缩短 60% 以上。

4.4 合并 RUN 指令,减少层数

早期 Docker 版本有层数上限,现在已无此限制,但冗余层仍然会增加镜像体积和维护复杂度。将关联命令合并到一条 RUN 中,并在同层内完成清理:

bash 复制代码
# ❌ 反模式:三个 RUN 产生三个层,中间层的缓存文件永久保留
RUN apt-get update
RUN apt-get install -y curl vim
RUN rm -rf /var/lib/apt/lists/*

# ✅ 优化:一条 RUN 只产生一个层,安装和清理在同一层完成
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl vim && \
    rm -rf /var/lib/apt/lists/*

4.5 .dockerignore:减少构建上下文

上一篇已经提过 .dockerignore,这里再强调一次------它是构建优化的零成本起点:

bash 复制代码
__pycache__
*.pyc
*.log
.git
.env
venv
.venv
*.tar
*.gz
.vscode
.idea
*.md
Dockerfile
.dockerignore

没有 .dockerignore,构建上下文可能从几十 KB 膨胀到几百 MB,Sending build context to Docker daemon 这一步就会多耗数十秒。

4.6 非 root 用户运行

这不仅是安全加固,更是企业合规的基本要求:

bash 复制代码
RUN groupadd -r appuser && useradd -r -m -u 1000 -g appuser appuser
USER appuser

配合 COPY --chown=appuser:appuser . . 确保文件权限正确。以非 root 身份运行,即使容器内的应用被攻破,攻击者也无法获得宿主机的 root 访问权限。

五、多阶段构建深度实战:Flask + Redis 计数器完整优化版

现在让我们把 Flask + Redis 计数器应用的 Dockerfile 做一次系统性升级。以下是包含 builder 和 runtime 两阶段的完整版本,结合 pip wheel 预编译与构建层复用:

bash 复制代码
# syntax=docker/dockerfile:1
# ============================================================
# Flask + Redis 计数器应用 ------ 生产级多阶段 Dockerfile
# 系列贯穿案例 v2.0(优化版)
# ============================================================

# ---- 阶段 1:Builder(构建阶段)----
FROM python:3.12-slim AS builder

# 安装构建依赖(仅在 builder 阶段使用)
RUN apt-get update && \
    apt-get install -y --no-install-recommends gcc python3-dev && \
    rm -rf /var/lib/apt/lists/*

WORKDIR /build

# 先复制依赖清单(利用层缓存)
COPY requirements.txt .

# pip wheel 预编译所有 Python 依赖为 .whl 文件
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt


# ---- 阶段 2:Runtime(运行阶段)----
FROM python:3.12-slim

LABEL maintainer="IT策士" \
      description="Flask + Redis 计数器应用(生产级多阶段构建)" \
      version="2.0"

# 环境变量
ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1

# 创建非 root 用户
RUN groupadd -r appuser && \
    useradd -r -m -u 1000 -g appuser appuser

WORKDIR /app

# 从 builder 阶段复制预编译的 .whl 包
COPY --from=builder /wheels /wheels
COPY requirements.txt .

# 仅从本地 wheel 安装,不访问 PyPI,不保留安装文件
RUN pip install --no-cache-dir --no-index --find-links=/wheels -r requirements.txt && \
    rm -rf /wheels requirements.txt

# 复制应用代码并设置权限
COPY --chown=appuser:appuser . .

# 切换到非 root 用户
USER appuser

EXPOSE 5000

# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD curl -f http://localhost:5000/health || exit 1

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

构建并查看体积:

bash 复制代码
docker build -t flask-redis-counter:2.0 .

关键构建输出:

bash 复制代码
[+] Building 42.3s (17/17) FINISHED
 => [builder 1/4] FROM python:3.12-slim                        0.0s
 => [builder 2/4] WORKDIR /build                               0.1s
 => [builder 3/4] RUN apt-get update && ...                    14.2s
 => [builder 4/4] RUN pip wheel --no-cache-dir ...             9.5s
 => [runtime 1/8] FROM python:3.12-slim                        0.0s
 => [runtime 2/8] RUN groupadd -r appuser && ...               0.4s
 => [runtime 3/8] WORKDIR /app                                 0.0s
 => [runtime 4/8] COPY --from=builder /wheels /wheels           0.2s
 => [runtime 5/8] COPY requirements.txt .                      0.1s
 => [runtime 6/8] RUN pip install --no-index ...               3.8s
 => [runtime 7/8] COPY --chown=appuser:appuser . .             0.1s
 => [runtime 8/8] USER appuser                                 0.0s
 => exporting to image                                         2.1s
 => => naming to docker.io/library/flask-redis-counter:2.0    0.0s

验证镜像体积与分层:

bash 复制代码
docker images flask-redis-counter
bash 复制代码
REPOSITORY            TAG       IMAGE ID       SIZE
flask-redis-counter   2.0       b2c3d4e5f6a7   138MB
flask-redis-counter   1.0       e8f9a0b1c2d3   138MB
bash 复制代码
docker history flask-redis-counter:2.0
bash 复制代码
IMAGE          SIZE      CREATED BY
b2c3d4e5f6a7   0B        HEALTHCHECK ...
a1b2c3d4e5f6   0B        USER appuser
f6a7b8c9d0e1   3.5kB     COPY --chown=appuser:appuser . .
b7c8d9e0f1a2   12MB      RUN pip install --no-cache-dir --no-index ...
c8d9e0f1a2b3   28B       COPY requirements.txt .
d9e0f1a2b3c4   200kB     COPY --from=builder /wheels /wheels
e0f1a2b3c4d5   1.2kB     RUN groupadd -r appuser && ...
<missing>      120MB     python:3.12-slim 基础镜像

12MB 的应用依赖层、3.5kB 的源码层、200kB 的 wheel 复制层------每一层的体积都清晰可控,而构建工具(gcc、python3-dev)的痕迹在最终镜像中完全消失。

功能验证:

bash 复制代码
docker network create counter-net
docker run -d --name redis --network counter-net redis:alpine
docker run -d --name flask-app --network counter-net -p 5000:5000 flask-redis-counter:2.0

curl http://localhost:5000
# Hello World! I have been seen 1 times.

docker ps --filter name=flask-app --format '{{.Status}}'
# Up 2 minutes (healthy)

所有功能正常,健康检查通过,非 root 用户运行无误。

六、镜像分析与安全扫描实战

6.1 使用 dive 逐层审计镜像

dive 是一个开源的 Docker 镜像分析工具,可以逐层展示镜像的文件变化、每层大小及"浪费空间"评估。安装方式:

bash 复制代码
# Ubuntu/Debian
curl -s https://api.github.com/repos/wagoodman/dive/releases/latest | \
  grep browser_download_url | grep linux_amd64.deb | cut -d '"' -f 4 | wget -qi -
sudo dpkg -i dive_*.deb

# macOS
brew install dive

分析我们的镜像:

bash 复制代码
dive flask-redis-counter:2.0

dive 的交互界面分为三个区域:

  • 左侧面板:列出每一层的 ID、大小及对应 Dockerfile 指令。你可以一目了然地看到哪一层占了多少空间

  • 右侧面板:文件树,用绿色标记当前层新增的文件,橙色标记修改过的文件

  • 底部区域:显示镜像总大小、Wasted space(浪费空间)和效率评分

例如,如果某层显示"135MB",dive 可以明确告诉你这 135MB 中哪些文件是重复的、哪些被后续层删除但仍占据空间。效率评分低于 80% 的镜像通常有较大优化空间。

6.2 使用 Trivy 扫描漏洞

bash 复制代码
trivy image flask-redis-counter:2.0
bash 复制代码
flask-redis-counter:2.0 (debian 12.8)
Total: 2 (CRITICAL: 0, HIGH: 0, MEDIUM: 2, LOW: 0)

相比单阶段构建(可能检出 HIGH 级别的 gcc 相关 CVE),多阶段构建通过移除编译工具链,直接减少了数十个冗余软件包及其关联漏洞。

6.3 安全构建规范总结

命令速查表:

关于漏洞扫描结果的说明:本文展示的扫描结果仅为示意。实际扫描结果取决于镜像中的具体包版本和当前 Trivy 漏洞数据库。在生产环境中,建议将 trivy image 集成到 CI/CD 流水线中,并设置质量门禁------存在 CRITICAL 或 HIGH 级别漏洞的镜像不应推送到生产仓库。

七、镜像标签管理策略

镜像标签不是随便打个 latest 就完事的。生产环境中推荐以下标签体系:

latest 标签的危险在于:它指向哪个版本完全取决于最近一次 docker builddocker pull,在集群环境中不同节点上的 latest 可能指向不同镜像。

实际开发中可以在每次构建时同时打上语义化版本标签和 Git commit SHA 标签:

bash 复制代码
GIT_SHA=$(git rev-parse --short HEAD)
docker build -t flask-redis-counter:2.0 -t flask-redis-counter:sha-${GIT_SHA} .

这样既能通过版本号快速识别,又能在排查问题时追溯到确切的 Git commit。

八、构建缓存进阶:BuildKit cache mount

Docker 默认的层缓存已经很有用,但每次 pip install 都需要重新解析和下载依赖。BuildKit 提供的 --mount=type=cache 可以在构建之间持久化包管理器的缓存目录,让后续构建直接从本地缓存读取,进一步将依赖安装时间压缩到毫秒级。

首先确认 BuildKit 已启用:

bash 复制代码
docker buildx version
# github.com/docker/buildx v0.12.0  # 输出包含版本号即表示可用

# 如果未启用,设置环境变量:
export DOCKER_BUILDKIT=1

在 Dockerfile 中使用 cache mount:

bash 复制代码
# syntax=docker/dockerfile:1
# ============================================================
# Flask + Redis 计数器 ------ 使用 BuildKit cache mount 优化
# ============================================================

FROM python:3.12-slim AS builder
RUN apt-get update && \
    apt-get install -y --no-install-recommends gcc python3-dev && \
    rm -rf /var/lib/apt/lists/*

WORKDIR /build
COPY requirements.txt .

# pip 缓存目录挂载,构建之间复用
RUN --mount=type=cache,target=/root/.cache/pip \
    pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt


FROM python:3.12-slim
RUN groupadd -r appuser && useradd -r -m -u 1000 -g appuser appuser

WORKDIR /app
COPY --from=builder /wheels /wheels
COPY requirements.txt .

RUN --mount=type=cache,target=/root/.cache/pip \
    pip install --no-cache-dir --no-index --find-links=/wheels -r requirements.txt && \
    rm -rf /wheels requirements.txt

COPY --chown=appuser:appuser . .
USER appuser
EXPOSE 5000

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD curl -f http://localhost:5000/health || exit 1

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

注意syntax=docker/dockerfile:1 必须放在 Dockerfile 第一行,否则 RUN --mount=type=cache 语法无法被正确解析。

效果对比(在依赖已缓存的情况下):

  • 首次构建:45-50 秒(包含完整下载与编译)

  • 第二次构建(仅改源码):6-8 秒(pip 依赖安装直接从本地缓存命中)

依赖安装步骤的时间从"秒级"压缩到"毫秒级",有效缓解了 CI/CD 流水线频繁构建镜像的资源瓶颈。

九、综合优化成果

优化前后关键指标对比:

优化前后最终镜像体积变化不大(因为之前已经是多阶段构建),但构建速度提升了一倍,且通过 cache mount 为 CI/CD 流水线优化留下了扩展空间。随着项目依赖数量增长,cache mount 的时间收益会越来越明显。

十、本篇总结

核心知识点回顾

  • 分层的代价 :每条 RUN/COPY 生成一个不可变层,冗余文件一旦写入无法从历史清除。优化 Dockerfile 的关键是减少层体积与充分利用缓存

  • 多阶段构建FROM ... AS builder + FROM minimal-runtime + COPY --from=builder,将构建环境与运行环境分离,镜像体积可缩减 30%~90%

  • 基础镜像选型:优先使用 slim 镜像,Alpine 注意 musl libc 兼容性,Distroless 适合有成熟运维体系的团队

  • 指令排序原则:变更频率低的指令(依赖安装)放在前面,高频变更(源码复制)放在最后,最大化缓存命中率

  • 合并 RUN 指令 :使用 && 串联安装和清理操作,在同层内完成,避免中间层残留缓存文件

  • 非 root 运行 :通过 RUN groupadd/useradd + USER 降低安全风险,搭配 COPY --chown 设置文件权限

  • 镜像标签管理 :避免使用 latest,推荐语义化版本号 + Git commit SHA 双标签组合

  • BuildKit cache mount :通过 RUN --mount=type=cache 在构建之间复用包管理器缓存,依赖安装时间压缩至毫秒级

  • dive 镜像层分析:逐层展示文件变化和空间浪费,效率评分低于 80% 的镜像建议深入优化

  • Trivy 漏洞扫描:减少攻击面的直观指标------CVE 数量从小几十个降到个位数

命令速查表

本篇完成后的项目结构

bash 复制代码
flask-redis-counter/
├── app.py               # Flask 应用主程序
├── requirements.txt     # Python 依赖清单
├── Dockerfile           # 生产级多阶段构建(优化版 v2.0)
├── .dockerignore        # 构建上下文排除规则
└── docker-compose.yml   # 待第 11 篇编写

我们从"能跑"起步,经过本篇的系统性优化,得到了一个体积可控、构建高效、安全性更好的生产级 Dockerfile。下一篇------第6篇:容器生命周期管理:常用命令与调试技巧,我们将深入容器的运行状态管理,学会如何在容器出问题时快速定位、排查和修复。


想了解更多还可以去各个平台搜索「IT策士」,一起升级 IT 思维!

相关推荐
一个儒雅随和的男子1 小时前
使用 Docker Compose 搭建 Kafka 集群
docker·kafka
weixin_468466852 小时前
Jellyfin 家庭媒体中心从零搭建指南
服务器·docker·容器·自动化·jellyfin·媒体中心
英仔cc2 小时前
Kubernetes Pod 的启动流程
kubernetes
qq_452396232 小时前
第五篇:《Docker 容器生命周期管理》
运维·docker·容器
ai产品老杨2 小时前
统一视频接入与多品牌利旧:基于 Docker 与 GB28181/RTSP 的边缘计算 AI 视频中台架构设计与源码交付实践
人工智能·docker·音视频
蜀道山老天师2 小时前
Docker 进阶:数据持久化与容器网络互联(数据卷、挂载目录、端口映射、自定义网络)
运维·网络·docker·云原生·容器
IT策士2 小时前
Docker 从 0 到 1 再到 Kubernetes 实战:第6篇 容器生命周期管理
docker·容器·kubernetes
zhz52142 小时前
Docker 部署 MongoDB / MySQL / PostgreSQL 安全加固实录:TLS 双向认证、双因素鉴别与审计
mysql·mongodb·docker·postgresql·等保
AI服务老曹3 小时前
源码交付与低代码解耦:基于 Docker 的边缘计算 AI 视频管理平台二次开发深度实战(兼容 GB28181/RTSP)
人工智能·docker·媒体