# 离线镜像(tarball)部署说明 本文说明 **方式 B**:厂商在可联网环境构建并导出 API 镜像为 `tar`/`tar.gz`,客户在目标服务器 **不拉取 API 镜像构建上下文** 的情况下,通过 `docker load` 加载后启动完整后端(PostgreSQL + MinIO + API)。 > 说明:仓库默认的 `backend/docker-compose.yml` 中 `api` 使用 `build:`。客户侧使用 tarball 时,必须使用下方 **「仅使用已加载镜像」** 的 Compose 片段(将 `build` 换成 `image`),否则仍会尝试本地构建。 --- ## 一、厂商侧:构建与导出 在已具备完整源码与 Docker 的环境中(与生产交付相同架构,一般为 **linux/amd64**): ### 1. 构建并打标签 ```bash cd /path/to/operation-room-monitor/backend docker compose build api docker tag operation-room-monitor-server-api:latest <交付镜像名>:<版本号> # 示例:docker tag operation-room-monitor-server-api:latest acme/or-monitor-api:1.0.0 ``` 若在 Apple Silicon 等设备上打包给 x86 服务器,需启用跨平台构建(示例): ```bash docker buildx build --platform linux/amd64 -t <交付镜像名>:<版本号> --load . ``` ### 2. 导出为 tarball 单机文件交付常用压缩包: ```bash docker save <交付镜像名>:<版本号> | gzip -9 > or-monitor-api_<版本号>_linux-amd64.tar.gz ``` 导出多个镜像(可选,用于 **完全离线** 且客户机不能拉 Postgres/MinIO 时): ```bash docker pull m.daocloud.io/docker.io/library/postgres:16-alpine docker pull m.daocloud.io/docker.io/minio/minio:latest docker save \ <交付镜像名>:<版本号> \ m.daocloud.io/docker.io/library/postgres:16-alpine \ m.daocloud.io/docker.io/minio/minio:latest \ | gzip -9 > or-monitor-stack_<版本号>_offline.tar.gz ``` ### 3. 建议一并交付的文件 | 文件 | 说明 | |------|------| | `or-monitor-api_*.tar.gz`(或全栈 `*_offline.tar.gz`) | 镜像包 | | 本文档 | 客户部署步骤 | | `docker-compose.yml` **或** 下文「离线 Compose」全文 | `api` 使用 `image:` 而非 `build:`(位于 `backend/`) | | `backend/.env.example`(按项目实际改名) | 环境变量模板,勿包含真实密钥 plaintext 于公共渠道 | |(可选)语音确认前端静态包、`OR_SITE_CONFIG` 样例 | 按需 | --- ## 二、客户侧:环境要求 - **Docker** 与 **Docker Compose**(Compose V2:`docker compose`)。 - **CPU 架构**:与 tarball 构建时声明一致(常见 **amd64**)。 - **磁盘**:PostgreSQL / MinIO 使用命名卷持久化;外加镜像解压空间,建议预留数十 GB。 - **端口**(默认值,可在 `.env` 中修改): - `38080`:HTTP API(`API_PORT`) - `9000` / `9001`:MinIO API / 控制台 - `5432`:仅在容器网络内使用,默认 **不映射到宿主机** - **GPU(必需)**:镜像内含 CUDA 版 PyTorch;客户机需安装 NVIDIA 驱动与 **NVIDIA Container Toolkit**,并保证 `nvidia-smi` 与 `docker run --gpus all` 正常。Compose 模板中 `api` 服务默认启用 `gpus: all`。 --- ## 三、客户侧:加载镜像 将 tarball 拷贝到服务器后: ```bash gzip -dc or-monitor-api_<版本号>_linux-amd64.tar.gz | docker load # 或:docker load -i or-monitor-api_<版本号>_linux-amd64.tar ``` 确认镜像已存在: ```bash docker images | grep <交付镜像名> ``` 若为「全栈离线包」,同样对整体 `tar.gz` 执行一次 `docker load` 即可导入多个镜像。 --- ## 四、客户侧:Compose 配置(使用已加载镜像) 在部署目录放置 `docker-compose.yml`,**将 `api` 服务改为使用预加载镜像**,不要保留 `build:`。可直接使用下面模板(把镜像名与版本改成实际交付值): ```yaml # 与仓库主 compose 行为一致,但 api 使用已 load 的镜像(无 build) services: db: image: m.daocloud.io/docker.io/library/postgres:16-alpine environment: POSTGRES_USER: ${POSTGRES_USER:-postgres} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} POSTGRES_DB: ${POSTGRES_DB:-operation_room} volumes: - pgdata:/var/lib/postgresql/data restart: unless-stopped healthcheck: test: ["CMD-SHELL", "pg_isready -U \"$${POSTGRES_USER}\" -d \"$${POSTGRES_DB}\""] interval: 5s timeout: 5s retries: 20 start_period: 5s minio: image: m.daocloud.io/docker.io/minio/minio:latest command: server /data --console-address ":9001" environment: MINIO_ROOT_USER: ${MINIO_ACCESS_KEY:-minioadmin} MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:-minioadmin} ports: - "${MINIO_PORT:-9000}:9000" - "${MINIO_CONSOLE_PORT:-9001}:9001" volumes: - minio_data:/data restart: unless-stopped healthcheck: test: ["CMD", "curl", "-f", "http://127.0.0.1:9000/minio/health/live"] interval: 10s timeout: 5s retries: 5 start_period: 10s api: image: <交付镜像名>:<版本号> gpus: all extra_hosts: - "host.docker.internal:host-gateway" environment: POSTGRES_USER: ${POSTGRES_USER:-postgres} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} POSTGRES_DB: ${POSTGRES_DB:-operation_room} POSTGRES_HOST: db POSTGRES_PORT: 5432 CONSUMABLE_CLASSIFIER_IMGSZ: ${CONSUMABLE_CLASSIFIER_IMGSZ:-224} CONSUMABLE_CLASSIFIER_DEVICE: ${CONSUMABLE_CLASSIFIER_DEVICE:-} CONSUMABLE_CLASSIFIER_TOPK: ${CONSUMABLE_CLASSIFIER_TOPK:-5} CONSUMABLE_MIN_CLS_CONFIDENCE: ${CONSUMABLE_MIN_CLS_CONFIDENCE:-0.5} CONSUMABLE_VISION_WINDOW_SEC: ${CONSUMABLE_VISION_WINDOW_SEC:-15} HAND_DETECTION_WEIGHTS: ${HAND_DETECTION_WEIGHTS:-} HAND_DETECTION_IMGSZ: ${HAND_DETECTION_IMGSZ:-640} HAND_DETECTION_DEVICE: ${HAND_DETECTION_DEVICE:-} VIDEO_DEFAULT_BACKEND: ${VIDEO_DEFAULT_BACKEND:-rtsp} VIDEO_RTSP_URL_TEMPLATE: ${VIDEO_RTSP_URL_TEMPLATE:-} OR_SITE_CONFIG_JSON_FILE: ${OR_SITE_CONFIG_JSON_FILE:-} VIDEO_CAMERA_BACKEND_OVERRIDES_JSON: ${VIDEO_CAMERA_BACKEND_OVERRIDES_JSON:-} HIKVISION_SDK_ENABLED: ${HIKVISION_SDK_ENABLED:-false} HIKVISION_LIB_DIR: ${HIKVISION_LIB_DIR:-/opt/hikvision/lib} HIKVISION_DEVICE_IP: ${HIKVISION_DEVICE_IP:-} HIKVISION_USER: ${HIKVISION_USER:-} HIKVISION_PASSWORD: ${HIKVISION_PASSWORD:-} HIKVISION_PREVIEW_RTSP_TEMPLATE: ${HIKVISION_PREVIEW_RTSP_TEMPLATE:-} OPENCV_FFMPEG_CAPTURE_OPTIONS: ${OPENCV_FFMPEG_CAPTURE_OPTIONS:-rtsp_transport;tcp} BAIDU_APP_ID: ${BAIDU_APP_ID:-} BAIDU_API_KEY: ${BAIDU_API_KEY:-} BAIDU_SECRET_KEY: ${BAIDU_SECRET_KEY:-} BAIDU_ASR_DEV_PID: ${BAIDU_ASR_DEV_PID:-1537} MINIO_ENDPOINT: ${DOCKER_MINIO_ENDPOINT:-minio:9000} MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY:-minioadmin} MINIO_SECRET_KEY: ${MINIO_SECRET_KEY:-minioadmin} MINIO_BUCKET: ${MINIO_BUCKET:-operation-room-voice} MINIO_SECURE: ${MINIO_SECURE:-false} MINIO_REGION: ${MINIO_REGION:-} DEMO_CORS_ENABLED: ${DEMO_CORS_ENABLED:-true} DEMO_CORS_ORIGINS: ${DEMO_CORS_ORIGINS:-*} DEMO_ORCHESTRATOR_ENABLED: ${DEMO_ORCHESTRATOR_ENABLED:-false} DEMO_ORCHESTRATOR_RTSP_PORT: ${DEMO_ORCHESTRATOR_RTSP_PORT:-18554} DEMO_ORCHESTRATOR_RTSP_JSON_HOST: ${DOCKER_DEMO_ORCHESTRATOR_RTSP_JSON_HOST:-host.docker.internal} MEDIAMTX_DOCKER_IMAGE: ${MEDIAMTX_DOCKER_IMAGE:-m.daocloud.io/docker.io/bluenviron/mediamtx:latest} command: > sh -c "alembic upgrade head && uvicorn main:app --host 0.0.0.0 --port 8000" ports: - "${API_PORT:-38080}:8000" depends_on: db: condition: service_healthy minio: condition: service_started restart: unless-stopped healthcheck: test: [ "CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=2)", ] interval: 10s timeout: 5s retries: 5 start_period: 20s volumes: pgdata: minio_data: ``` **离线且未交付 Postgres/MinIO 镜像时**:需客户机对上述 `db`、`minio` 的 `image:` 仍能拉取(或改用厂商提供的全栈 `docker save` 包,`image` 名需与导出时完全一致)。 --- ## 五、环境变量与密钥 在同一目录(`backend/`)放置 `.env`(可参考 `backend/.env.example`)。至少建议关注: - `POSTGRES_PASSWORD`:生产请务必改为强密码。 - `API_PORT`:宿主机监听端口。 - 语音链路:`BAIDU_*`、`MINIO_*`(容器内需访问 `MINIO_ENDPOINT`,默认已通过 `DOCKER_MINIO_ENDPOINT` 指向 `minio:9000`)。 - `OR_SITE_CONFIG_JSON_FILE`:容器内可读路径下的站点 JSON;若在宿主机维护,需 **`volumes`** 挂载进 `api` 容器(挂载方式由项目单独约定)。 - RTSP:`host.docker.internal` 用于访问**宿主机**上的假流;详见 `docs/video-backends.md`。 --- ## 六、启动与健康检查 ```bash docker compose up -d docker compose ps curl -sf http://127.0.0.1:38080/health ``` 局域网终端将「服务端 Base URL」配置为:`http://<服务器局域网IP>:38080`(若修改了 `API_PORT` 则同步修改端口)。 查看日志: ```bash docker compose logs -f api ``` --- ## 七、升级 API 镜像 1. 客户停止 API(或整栈):`docker compose stop api`。 2. 加载新 tarball:`docker load -i …`。 3. 将 `docker-compose.yml` 中 `api.image` 更新为新 `<交付镜像名>:<新版本号>`(或保持不变若标签滚动更新,需先在客户机 `docker rmi` 旧镜像或使用明确版本标签)。 4. `docker compose up -d`。API 容器启动仍会执行 `alembic upgrade head`。 --- ## 八、常见问题 **Q:`docker compose up` 仍尝试 build?** A:`api` 服务仍存在 `build:` 配置时会构建。必须使用本文第四节模板,仅用 `image:`。 **Q:无 GPU 报错?** A:本仓库默认要求 GPU 推理。若必须在无 GPU 环境运行,需移除 `api` 下 `gpus: all` 并使用 CPU 兼容镜像(推理会显著变慢,需与厂商确认)。 **Q:PostgreSQL / MinIO 数据在哪?** A:Docker 命名卷(如 `pgdata`、`minio_data`);`docker compose down -v` 会删除数据,操作前确认备份。 **Q:`host.docker.internal` 在 Linux 无效?** A:Compose 已为 `api` 配置 `extra_hosts: host.docker.internal:host-gateway`(Docker 20.10+)。若自定义运行 `docker run`,需自行添加同等 `extra_hosts`。 --- ## 九、与安全相关 - tarball 等价于可复制软件制品,分发渠道注意保密与校验(校验和、签名)。 - 勿将生产密钥写入镜像层;一律通过 `.env` 或编排系统注入运行时环境变量。 如需语音确认前端、演示客户端与本后端的局域网访问说明,参见 `docs/Docker部署.md`、`clients/voice-confirmation/README.md`、`clients/demo-client/README.md`。