# BuildKit 离线构建方案

本方案使用 Docker BuildKit 的缓存机制，实现在弱网/无网环境下的镜像构建。

## 方案概述

```
┌─────────────────────────────────────────────────────────────────┐
│  有网环境 (Build Machine)                                       │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────────────┐ │
│  │ 基础镜像    │    │ BuildKit    │    │ 外部资源            │ │
│  │ docker pull │ +  │ 缓存导出    │ +  │ (模型/源码)         │ │
│  └─────────────┘    └─────────────┘    └─────────────────────┘ │
│           │                  │                  │               │
│           └──────────────────┼──────────────────┘               │
│                              ▼                                  │
│                    ┌──────────────────┐                         │
│                    │ build-cache.tar.gz│                        │
│                    └────────┬─────────┘                         │
└─────────────────────────────┼───────────────────────────────────┘
                              │ 传输到无网环境
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│  无网环境 (Offline Machine)                                     │
│                    ┌──────────────────┐                         │
│                    │ build-cache.tar.gz│                        │
│                    └────────┬─────────┘                         │
│                             ▼                                   │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────────────┐ │
│  │ docker load │    │ BuildKit    │    │ 本地资源挂载        │ │
│  │ 基础镜像    │ +  │ 缓存导入    │ +  │ (模型/源码)         │ │
│  └─────────────┘    └─────────────┘    └─────────────────────┘ │
│           │                  │                  │               │
│           └──────────────────┼──────────────────┘               │
│                              ▼                                  │
│                         构建成功！                              │
└─────────────────────────────────────────────────────────────────┘
```

## 快速开始

### 方法一：使用 Makefile 扩展（推荐）

#### 1. 合并 Makefile

将 `Makefile.offline.mk` 追加到主 Makefile：

```bash
# Linux/Mac
cat Makefile.offline.mk >> Makefile

# Windows (PowerShell)
Get-Content Makefile.offline.mk | Add-Content Makefile
```

#### 2. 有网环境导出缓存

```bash
# 导出所有缓存（包括基础镜像、BuildKit 缓存、外部资源）
make offline-export

# 或者指定输出目录
make offline-export CACHE_DIR=/path/to/cache
```

执行完成后，会生成压缩包：`build-cache-YYYYMMDD.tar.gz`

#### 3. 传输到无网环境

```bash
# 使用 scp 或其他方式传输
scp build-cache-20250202.tar.gz user@offline-server:/opt/datamate/

# 或者使用 U 盘等物理介质
```

#### 4. 无网环境构建（推荐使用传统方式）

```bash
# 解压缓存
tar -xzf build-cache-20250202.tar.gz

# 诊断环境（检查基础镜像等）
make offline-diagnose

# 方法 A：传统 docker build（推荐，更稳定）
make offline-setup
make offline-build-classic

# 方法 B：BuildKit 构建（如果方法 A 失败）
make offline-setup
make offline-build

# 或者指定版本号
make offline-build-classic OFFLINE_VERSION=v1.0.0
```

**⚠️ 重要提示**：如果遇到镜像拉取问题，请使用 `make offline-build-classic` 而不是 `make offline-build`。

### 方法二：使用独立脚本

#### 导出缓存

```bash
cd scripts/offline
./export-cache.sh /path/to/output
```

#### 离线构建

```bash
cd scripts/offline
./build-offline.sh /path/to/cache [version]
```

## 详细说明

### 缓存内容

缓存目录结构：

```
build-cache/
├── buildkit/                  # BuildKit 缓存
│   ├── backend-cache/
│   ├── backend-python-cache/
│   ├── database-cache/
│   ├── frontend-cache/
│   ├── gateway-cache/
│   ├── runtime-cache/
│   ├── deer-flow-backend-cache/
│   ├── deer-flow-frontend-cache/
│   └── mineru-cache/
├── images/
│   └── base-images.tar       # 基础镜像集合
└── resources/                 # 外部资源
    ├── models/
    │   ├── ch_ppocr_mobile_v2.0_cls_infer.tar    # PaddleOCR 模型
    │   └── zh_core_web_sm-3.8.0-py3-none-any.whl # spaCy 模型
    ├── DataX/                # DataX 源码
    └── deer-flow/            # deer-flow 源码
```

### 单个服务构建

```bash
# 仅构建 backend
make backend-offline-build

# 仅构建 runtime
make runtime-offline-build

# 仅构建 deer-flow-backend
make deer-flow-backend-offline-build
```

### 增量更新

如果只有部分服务代码变更，可以只导出该服务的缓存：

```bash
# 重新导出 backend 缓存
docker buildx build \
  --cache-to type=local,dest=./build-cache/buildkit/backend-cache,mode=max \
  -f scripts/images/backend/Dockerfile \
  -t datamate-backend:cache .

# 传输并重新构建
tar -czf build-cache-partial.tar.gz build-cache/buildkit/backend-cache
# ... 传输到无网环境 ...
make backend-offline-build
```

## APT 缓存问题详解

### 问题描述

即使使用了 `--mount=type=cache,target=/var/cache/apt`，Dockerfile 中的 `apt-get update` 仍会尝试从网络获取包列表（list 数据），导致无网环境下构建失败：

```
Err:1 http://mirrors.aliyun.com/debian bookworm InRelease
  Could not resolve 'mirrors.aliyun.com'
Reading package lists...
E: Failed to fetch http://mirrors.aliyun.com/debian/dists/bookworm/InRelease
```

### 根本原因

- `--mount=type=cache,target=/var/cache/apt` 只缓存下载的 `.deb` 包
- `apt-get update` 会尝试从配置的源获取最新的包索引（InRelease/Packages 文件）
- `/var/lib/apt/lists/` 目录存储包索引，但通常不在缓存范围内

### 解决方案

#### 方案 1: 使用预装 APT 包的基础镜像（推荐）

这是最有效的方法：

**步骤 1**: 在有网环境构建预装所有依赖的基础镜像

```bash
# 构建并保存带 APT 预装包的基础镜像
./scripts/offline/build-base-images.sh
```

这会创建以下预装基础镜像：
- `datamate-java-base` - 用于 backend、gateway（预装 vim、python3、libreoffice 等）
- `datamate-python-base` - 用于 backend-python（预装 openjdk、nfs-common 等）
- `datamate-runtime-base` - 用于 runtime（预装 libgl1、tesseract-ocr 等）
- `deer-flow-backend-base` - 用于 deer-flow-backend
- `mineru-base` - 用于 mineru

**步骤 2**: 在无网环境使用这些基础镜像构建

```bash
# 加载包含预装基础镜像的 tar 包
docker load -i build-cache/images/base-images-with-apt.tar

# 使用最终版构建脚本
./scripts/offline/build-offline-final.sh
```

#### 方案 2: 修改 Dockerfile 跳过 apt update

如果确定不需要安装新包，可以修改 Dockerfile：

```dockerfile
# 原代码
RUN apt-get update && apt-get install -y xxx

# 修改为（离线环境）
# RUN apt-get update && \
RUN apt-get install -y xxx || true
```

#### 方案 3: 挂载 apt lists 缓存

在有网环境预先下载并保存 apt lists：

```bash
# 有网环境：保存 apt lists
docker run --rm \
    -v "$(pwd)/apt-lists:/var/lib/apt/lists" \
    eclipse-temurin:21-jdk \
    apt-get update

# 无网环境：挂载保存的 lists
docker build \
    --mount=type=bind,source=$(pwd)/apt-lists,target=/var/lib/apt/lists,ro \
    -f Dockerfile .
```

**注意**: BuildKit 的 `--mount=type=bind` 在 `docker build` 中不直接支持，需要在 Dockerfile 中使用。

---

## 故障排查

### 问题 1: 构建时仍然尝试拉取镜像（最常见）

**现象**:
```
ERROR: failed to solve: pulling from host ...
或
ERROR: pull access denied, repository does not exist or may require authorization
```

**原因**: 
- 基础镜像未正确加载
- BuildKit 尝试验证远程镜像

**解决方案**:

1. **使用传统构建方式（推荐）**:
```bash
make offline-build-classic
```

2. **手动加载基础镜像**:
```bash
# 加载基础镜像
docker load -i build-cache/images/base-images.tar

# 验证镜像存在
docker images | grep -E "(maven|eclipse-temurin|mysql|node|nginx)"
```

3. **使用 Docker 守护进程的离线模式**:
```bash
# 编辑 /etc/docker/daemon.json
{
  "registry-mirrors": [],
  "insecure-registries": []
}

# 重启 Docker
sudo systemctl restart docker
```

### 问题 2: 缓存导入失败

```
ERROR: failed to solve: failed to read cache metadata
```

**解决**: 缓存目录可能损坏，重新在有网环境导出。

### 问题 3: 基础镜像不存在

```
ERROR: pull access denied
```

**解决**: 
1. 先执行 `make offline-setup` 加载基础镜像
2. 运行 `make offline-diagnose` 检查缺失的镜像
3. 重新导出缓存时确保包含所有基础镜像

### 问题 4: 网络连接错误（无网环境）

```
ERROR: failed to do request: dial tcp: lookup ...
```

**解决**: 检查 Dockerfile 中是否还有网络依赖（如 `git clone`、`wget`、`pip install` 等），可能需要修改 Dockerfile 使用本地资源。

### 问题 5: 内存不足

BuildKit 缓存可能占用大量内存，可以设置资源限制：

```bash
# 创建带资源限制的 buildx 构建器
docker buildx create --name offline-builder \
  --driver docker-container \
  --driver-opt memory=8g \
  --use
```

### 问题 6: BuildKit 构建器无法使用本地镜像

**现象**: 即使镜像已加载，BuildKit 仍提示找不到镜像

**解决**: BuildKit 的 `docker-container` 驱动无法直接访问本地镜像。使用以下方法之一：

**方法 A**: 使用传统 Docker 构建（推荐）
```bash
make offline-build-classic
```

**方法 B**: 将镜像推送到本地 registry
```bash
# 启动本地 registry
docker run -d -p 5000:5000 --name registry registry:2

# 标记并推送镜像到本地 registry
docker tag maven:3-eclipse-temurin-21 localhost:5000/maven:3-eclipse-temurin-21
docker push localhost:5000/maven:3-eclipse-temurin-21

# 修改 Dockerfile 使用本地 registry
# FROM localhost:5000/maven:3-eclipse-temurin-21
```

**方法 C**: 使用 `docker` 驱动的 buildx 构建器（不需要推送镜像，但有其他限制）
```bash
# 创建使用 docker 驱动的构建器
docker buildx create --name offline-builder --driver docker --use

# 但这种方式无法使用 --cache-from type=local
# 仅适用于简单的离线构建场景
```

## 限制说明

1. **镜像版本**: 基础镜像版本必须与缓存导出时一致
2. **Dockerfile 变更**: 如果 Dockerfile 发生较大变更，可能需要重新导出缓存
3. **资源文件**: mineru 镜像中的模型下载（`mineru-models-download`）仍需要网络，如果需要在完全无网环境使用，需要预先将模型文件挂载到镜像中

## 高级用法

### 自定义缓存位置

```bash
make offline-export CACHE_DIR=/mnt/nas/build-cache
make offline-build CACHE_DIR=/mnt/nas/build-cache
```

### 导出特定平台缓存

```bash
# 导出 ARM64 平台的缓存
docker buildx build \
  --platform linux/arm64 \
  --cache-to type=local,dest=./build-cache/buildkit/backend-cache,mode=max \
  -f scripts/images/backend/Dockerfile .
```

### 使用远程缓存（有网环境）

```bash
# 导出到 S3/MinIO
docker buildx build \
  --cache-to type=s3,region=us-east-1,bucket=mybucket,name=backend-cache \
  -f scripts/images/backend/Dockerfile .

# 从 S3 导入
docker buildx build \
  --cache-from type=s3,region=us-east-1,bucket=mybucket,name=backend-cache \
  -f scripts/images/backend/Dockerfile .
```

## 文件清单

```
scripts/offline/
├── export-cache.sh                     # 有网环境导出缓存脚本
├── build-base-images.sh                # 构建 APT 预装基础镜像
├── build-offline.sh                    # 基础离线构建脚本（BuildKit）
├── build-offline-v2.sh                 # 增强版离线构建脚本
├── build-offline-classic.sh            # 传统 docker build 脚本
├── build-offline-final.sh              # 最终版（使用预装基础镜像，推荐）
├── diagnose.sh                         # 环境诊断脚本
├── Dockerfile.base-images              # 预装 APT 包的基础镜像定义
├── Dockerfile.backend.offline          # backend 离线 Dockerfile（使用预装基础镜像）
├── Dockerfile.gateway.offline          # gateway 离线 Dockerfile（使用预装基础镜像）
├── Dockerfile.backend-python.offline   # backend-python 离线 Dockerfile
├── Dockerfile.backend-python.offline-v2 # backend-python 离线 Dockerfile v2（使用预装基础镜像）
├── Dockerfile.runtime.offline          # runtime 离线 Dockerfile
├── Dockerfile.runtime.offline-v2       # runtime 离线 Dockerfile v2（使用预装基础镜像）
├── Dockerfile.deer-flow-backend.offline    # deer-flow-backend 离线 Dockerfile
├── Dockerfile.deer-flow-frontend.offline   # deer-flow-frontend 离线 Dockerfile
├── Makefile.offline                    # 独立离线构建 Makefile
└── README.md                           # 本文档

Makefile.offline.mk                     # Makefile 扩展（追加到主 Makefile）
```

## 推荐工作流（解决 APT 问题版）

### 工作流 A: 使用预装 APT 包的基础镜像（彻底解决 APT 问题）

```bash
# ========== 有网环境 ==========

# 1. 构建并保存带 APT 预装包的基础镜像
./scripts/offline/build-base-images.sh
# 输出: build-cache/images/base-images-with-apt.tar

# 2. 导出其他缓存（BuildKit 缓存、外部资源）
./scripts/offline/export-cache.sh

# 3. 打包传输
scp build-cache/images/base-images-with-apt.tar user@offline-server:/opt/datamate/build-cache/images/
scp build-cache-*.tar.gz user@offline-server:/opt/datamate/

# ========== 无网环境 ==========

cd /opt/datamate

# 4. 解压
tar -xzf build-cache-*.tar.gz

# 5. 加载预装基础镜像（关键！）
docker load -i build-cache/images/base-images-with-apt.tar

# 6. 使用最终版脚本构建
./scripts/offline/build-offline-final.sh
```

### 工作流 B: 简单场景（使用传统构建）

如果 APT 包需求简单，可以直接使用传统构建：

```bash
# 有网环境
make offline-export

# 传输到无网环境
scp build-cache-*.tar.gz offline-server:/path/

# 无网环境
tar -xzf build-cache-*.tar.gz
make offline-diagnose        # 检查环境
make offline-build-classic   # 使用传统构建
```

## 参考

- [Docker BuildKit Documentation](https://docs.docker.com/build/buildkit/)
- [Cache Storage Backends](https://docs.docker.com/build/cache/backends/)
