#!/usr/bin/env bash set -euo pipefail VERSION="0.1.0" OUTPUT_DIR="" SKIP_BUILD=0 SKIP_PULL=0 usage() { cat <<'EOF' Usage: ./scripts/package-offline-server.sh [options] Options: --version VERSION Image/package version, default: 0.1.0 --output-dir DIR Output package directory, default: ./dist/SimCAEServerDockerPackage --skip-build Do not build simcae-update-server image --skip-pull Do not pull MinIO images -h, --help Show this help EOF } while [[ $# -gt 0 ]]; do case "$1" in --version) VERSION="${2:?--version requires a value}" shift 2 ;; --output-dir) OUTPUT_DIR="${2:?--output-dir requires a value}" shift 2 ;; --skip-build) SKIP_BUILD=1 shift ;; --skip-pull) SKIP_PULL=1 shift ;; -h|--help) usage exit 0 ;; *) echo "Unknown option: $1" >&2 usage >&2 exit 1 ;; esac done require_file() { local path="$1" local message="$2" if [[ ! -e "$path" ]]; then echo "$message" >&2 exit 1 fi } require_cmd() { local name="$1" if ! command -v "$name" >/dev/null 2>&1; then echo "Required command is missing: $name" >&2 exit 1 fi } require_cmd docker require_cmd tar SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" if [[ -z "$OUTPUT_DIR" ]]; then OUTPUT_DIR="$PROJECT_ROOT/dist/SimCAEServerDockerPackage" fi IMAGE_NAME="simcae-update-server:$VERSION" MINIO_IMAGE="minio/minio:RELEASE.2025-04-22T22-12-26Z" MC_IMAGE="minio/mc:RELEASE.2025-04-16T18-13-26Z" PACKAGE_DIR="$OUTPUT_DIR" IMAGES_DIR="$PACKAGE_DIR/images" KEYS_DIR="$PACKAGE_DIR/keys" TAR_FILE="$IMAGES_DIR/simcae-server-all-images_$VERSION.tar" ARCHIVE_FILE="$PACKAGE_DIR.tar.gz" require_file "$PROJECT_ROOT/Dockerfile" "Dockerfile is missing." require_file "$PROJECT_ROOT/docker-compose.image.yml" "docker-compose.image.yml is missing." require_file "$PROJECT_ROOT/.env.docker.example" ".env.docker.example is missing." require_file "$PROJECT_ROOT/admin-ui/package.json" "admin-ui/package.json is missing." require_file "$PROJECT_ROOT/keys/manifest_private_key.pem" "keys/manifest_private_key.pem is missing." require_file "$PROJECT_ROOT/keys/manifest_public_key.pem" "keys/manifest_public_key.pem is missing." if [[ "$SKIP_BUILD" -eq 0 ]]; then docker build -t "$IMAGE_NAME" -f "$PROJECT_ROOT/Dockerfile" "$PROJECT_ROOT" fi if [[ "$SKIP_PULL" -eq 0 ]]; then docker pull "$MINIO_IMAGE" docker pull "$MC_IMAGE" fi rm -rf "$PACKAGE_DIR" mkdir -p "$IMAGES_DIR" "$KEYS_DIR" cp "$PROJECT_ROOT/docker-compose.image.yml" "$PACKAGE_DIR/docker-compose.yml" cp "$PROJECT_ROOT/.env.docker.example" "$PACKAGE_DIR/.env.example" cp "$PROJECT_ROOT/keys/manifest_private_key.pem" "$KEYS_DIR/manifest_private_key.pem" cp "$PROJECT_ROOT/keys/manifest_public_key.pem" "$KEYS_DIR/manifest_public_key.pem" cat > "$PACKAGE_DIR/README.md" <<'EOF_README' # SimCAE 服务端 Docker 打包和部署指南 本文从“维护者打包”到“使用者部署”完整说明 SimCAE 服务端 Docker 包的交付流程。 服务端 Docker 包用于把 SimCAE 自动升级服务端部署到你的服务器。包里已经包含后端 API、pure-admin(Vue3 + Element Plus)管理后台、MinIO 对象存储、初始化工具、配置模板、签名密钥和 Docker 编排文件;目标服务器即使不能联网,也可以按本文完成部署。 如果你是维护者,要先按“一、维护者:生成 Docker 离线部署包”打包。如果你已经拿到 `SimCAEServerDockerPackage.tar.gz`,直接从“三、使用者:最小部署流程”开始。 ## 先看这里:你要做哪件事 | 你的目标 | 直接看哪一节 | | --- | --- | | 从源码重新生成离线部署包 | 一、维护者:生成 Docker 离线部署包 | | 了解交付包里有哪些文件 | 二、这个包里面有什么 | | 在服务器上部署并启动 | 三、使用者:最小部署流程 | | 修改 `.env` 配置 | 四、.env 配置怎么填 | | 发布 Windows/Linux 客户端版本 | 六、发布新版本 | | 查看日志、重启、备份、升级 | 七、常用命令 | ## 一、维护者:生成 Docker 离线部署包 在服务端源码所在机器上执行。通常这台机器是 Ubuntu,并且已经安装 Docker。 进入服务端源码目录: ```bash cd update-server ``` 执行打包脚本: ```bash bash ./scripts/package-offline-server.sh \ --version __VERSION__ \ --output-dir ./dist/SimCAEServerDockerPackage ``` 脚本会做这些事情: ```text 1. 构建 simcae-update-server:__VERSION__ 镜像。 2. 拉取 MinIO 和 MinIO Client 镜像。 3. 复制 docker-compose.yml、.env.example、签名密钥和部署说明。 4. 把 API、MinIO、MinIO Client 三个镜像保存到 images/simcae-server-all-images___VERSION__.tar。 5. 生成完整目录 dist/SimCAEServerDockerPackage/。 6. 生成最终交付压缩包 dist/SimCAEServerDockerPackage.tar.gz。 ``` 打包完成后,重点确认这两个输出: ```text dist/SimCAEServerDockerPackage/ dist/SimCAEServerDockerPackage.tar.gz ``` 通常只需要把下面这个文件发给部署人员或拷贝到目标服务器: ```text dist/SimCAEServerDockerPackage.tar.gz ``` 如果只是重新打包已有镜像,可以使用: ```bash bash ./scripts/package-offline-server.sh \ --version __VERSION__ \ --output-dir ./dist/SimCAEServerDockerPackage \ --skip-build ``` 如果打包机器不能联网,但 MinIO 镜像已经提前存在本机,可以使用: ```bash bash ./scripts/package-offline-server.sh \ --version __VERSION__ \ --output-dir ./dist/SimCAEServerDockerPackage \ --skip-pull ``` 注意:`keys/manifest_private_key.pem` 会被放进部署包,用于服务端发布版本时签名 Manifest。这个私钥必须保护好,不要公开上传。 ## 二、这个包里面有什么 解压后目录结构如下: ```text SimCAEServerDockerPackage/ README.md 当前说明文档 docker-compose.yml 容器编排文件 .env.example 环境变量模板 load-images.sh 导入 Docker 镜像并创建 .env 的脚本 images/ simcae-server-all-images___VERSION__.tar Docker 镜像包,包含 api、minio、minio-init keys/ manifest_private_key.pem 服务端 Manifest 签名私钥 manifest_public_key.pem 与客户端配套的验签公钥 ``` 这些文件的关系可以这样理解: ```text images/*.tar 程序镜像包,相当于安装材料 docker-compose.yml 容器部署图纸,说明启动哪些服务、端口怎么映射、目录怎么挂载 .env 你的实际配置,第一次运行 load-images.sh 时由 .env.example 生成 keys/ 发布版本时用于签名 Manifest,客户端用对应公钥验签 runtime/ 启动后自动生成,保存服务端数据库、上传临时文件、崩溃报告等 minio_data/ 启动后自动生成,保存升级包文件 ``` ## 三、使用者:最小部署流程 先把 `SimCAEServerDockerPackage.tar.gz` 拷贝到要部署的 Ubuntu 服务器上,然后执行: ```bash tar -xzf SimCAEServerDockerPackage.tar.gz cd SimCAEServerDockerPackage pwd ls ``` 你需要确认当前目录就是解压后的 `SimCAEServerDockerPackage` 目录,并且能看到: ```text docker-compose.yml .env.example load-images.sh images/ keys/ ``` 后面所有 `docker compose` 命令都要在这个目录执行,因为 `docker-compose.yml` 和 `.env` 都在这里。 继续在 `SimCAEServerDockerPackage` 目录执行: ```bash bash ./load-images.sh ``` 这个脚本会做两件事: ```text 1. docker load 导入 images/simcae-server-all-images___VERSION__.tar 里的镜像 2. 如果当前目录没有 .env,就从 .env.example 复制一份 .env 3. 自动创建 runtime/、minio_data/ 等运行目录,并尽量修正目录权限 ``` 然后修改 `.env`: ```bash nano .env ``` 最少只需要把 `MINIO_PUBLIC_ENDPOINT` 改成客户端能访问到的服务器地址: ```text MINIO_PUBLIC_ENDPOINT=http://你的服务器IP:9000 ``` 例如服务器 IP 是 `192.168.229.128`: ```text MINIO_PUBLIC_ENDPOINT=http://192.168.229.128:9000 ``` 启动服务: ```bash docker compose up -d ``` 查看状态和后端日志: ```bash docker compose ps docker compose logs -f api ``` 浏览器访问: ```text 后台/API: http://你的服务器IP:8000/ MinIO 控制台: http://你的服务器IP:9001/ ``` 如果服务器开启防火墙,至少放行: ```text 8000 后台/API 9000 客户端下载升级文件 9001 MinIO 控制台,可选 ``` ## 四、.env 配置怎么填 `.env.example` 里已经给了一组能直接试跑的默认值。下面按重要程度说明。 ### 必须确认或修改 | 字段 | 是否必填 | 默认能否直接用 | 怎么填 | | --- | --- | --- | --- | | `MINIO_PUBLIC_ENDPOINT` | 必填 | 不能直接用于正式环境 | 改成客户端能访问到的 MinIO 地址,格式是 `http://服务器IP:9000`。客户端会用这个地址下载升级文件。 | | `SERVER_PORT` | 必填 | 可以 | 后台/API 端口,默认 `8000`。如果服务器 8000 被占用,可以改成其他端口。 | | `PUBLIC_API_BASE_URL` | 可不填 | 可以 | 管理页生成 qrc 服务端配置 `server_config.json` 时使用的后端 API 地址。不填时自动使用当前访问后台的地址;如果经过域名、反向代理或端口映射,建议填成客户端实际能访问的地址,例如 `http://服务器IP:8000`。 | | `RELEASE_MAIN_EXECUTABLE` | 必填 | SimCAE 默认可以 | 发布包里主程序的相对路径。Windows 示例:`bin/SimCAE.exe`;Linux 示例:`bin/SimCAE`。 | | `CLIENT_API_TOKEN` | 必填 | 可以 | 客户端访问服务端 API 的令牌。必须和客户端 `config/app_config.json` 里的 `client_token` 完全一致。 | | `ADMIN_USERNAME` / `ADMIN_PASSWORD` | 必填 | 可以 | 管理后台初始用户名和密码。首次启动且数据库里没有管理员用户时,会自动创建这个账号。网页登录时输入这组账号密码。 | | `ADMIN_JWT_SECRET` | 必填 | 可以 | 管理后台 JWT 签名密钥。正式部署建议改成随机长字符串,并长期保持不变;改掉后旧登录 token 会失效。 | | `ADMIN_TOKEN` | 必填 | 可以 | 服务端兜底令牌。管理后台不再用它直接登录;它仍用于 `ADMIN_JWT_SECRET` 未设置时的默认签名密钥,以及 `CRASH_ADMIN_TOKEN` 为空时的崩溃报告管理兜底令牌。 | | `LICENSE_KEY_ENCRYPTION_SECRET` | 必填 | 可以 | 后台授权列表显示 License Key 时使用的加密密钥。正式部署建议修改,并且部署后长期保持不变;如果后续改掉它,旧 License 仍可用于客户端校验,但后台无法再显示旧 License Key 原文。 | | `MINIO_ACCESS_KEY` | 必填 | 可以 | MinIO 用户名。默认可试跑,正式环境建议改。 | | `MINIO_SECRET_KEY` | 必填 | 可以 | MinIO 密码。默认可试跑,正式环境建议改。 | ### 通常不用改 | 字段 | 作用 | 默认值说明 | | --- | --- | --- | | `SIMCAE_UPDATE_SERVER_IMAGE` | API 镜像名称 | 打包脚本会自动写成当前版本,例如 `simcae-update-server:__VERSION__`。 | | `APP_UID` / `APP_GID` | API 容器写入 `runtime/` 时使用的用户 ID | `load-images.sh` 会尽量自动改成当前服务器用户。遇到权限问题时再检查。 | | `SERVICE_TITLE` | 管理后台标题 | 默认 `SimCAE Update Service`。 | | `CORS_ALLOW_ORIGINS` | 跨域来源 | 内网部署保持 `*` 即可。 | | `TARGET_PLATFORM` / `TARGET_ARCH` | 发布包目标平台 | Windows x64 填 `windows` / `x64`;Linux x64 填 `linux` / `x64`。 | | `SIGNING_KEY_ID` | Manifest 签名密钥编号 | 默认 `manifest-key-v1`。只有更换签名体系时才需要改。 | | `MINIO_API_PORT` | MinIO 文件下载端口 | 默认 `9000`。客户端下载升级文件要能访问这个端口。 | | `MINIO_CONSOLE_PORT` | MinIO 控制台端口 | 默认 `9001`。不需要控制台时可以不开放到外部。 | | `MINIO_BUCKET` | MinIO 存储桶名 | 默认 `updates`。 | | `SIGN_EXPIRE_MIN` | 升级文件下载链接有效期 | 默认 `60` 分钟。 | | `MINIO_CONNECT_TIMEOUT_SEC` / `MINIO_READ_TIMEOUT_SEC` / `MINIO_RETRY_TOTAL` / `MINIO_HEALTH_TIMEOUT_SEC` | MinIO 连接超时与重试 | 默认适合内网部署。 | ### 发布保护参数 这些字段用于防止上传超大目录导致服务器磁盘、内存压力过大。默认一般不用改。 | 字段 | 作用 | 默认值 | | --- | --- | --- | | `PUBLISH_MAX_REQUEST_MB` | 单次发布请求最大大小,单位 MB | `4096` | | `PUBLISH_MAX_FILES` | 单次发布最多文件数 | `20000` | | `PUBLISH_MAX_FIELDS` | 表单字段数上限,通常要大于文件数 | `20100` | | `UPLOAD_SPACE_RESERVE_MB` | 上传时额外保留的磁盘空间,单位 MB | `256` | 如果正式发布目录超过 4GB,可以在确认服务器磁盘空间足够后调大 `PUBLISH_MAX_REQUEST_MB`。如果页面提示空间不足,优先清理旧版本、扩容磁盘,或把服务端部署到更大的数据盘。 ### 崩溃报告接口参数 | 字段 | 是否必填 | 默认能否直接用 | 怎么填 | | --- | --- | --- | --- | | `CRASH_SERVICE_VERSION` | 必填 | 可以 | 崩溃报告接口版本,默认 `1.0.0`。 | | `CRASH_REPORT_TOKEN` | 必填 | 可以 | 客户端上传崩溃报告时使用的令牌。正式环境建议改。 | | `CRASH_SYMBOL_TOKEN` | 必填 | 可以 | 上传符号文件时使用的令牌。正式环境建议改。 | | `CRASH_ADMIN_TOKEN` | 可不填 | 可以 | 崩溃报告管理令牌。不填时使用 `ADMIN_TOKEN`。 | | `CRASH_METADATA_MAX_KB` | 必填 | 可以 | 崩溃报告 metadata 最大大小。 | | `CRASH_MINIDUMP_MAX_MB` | 必填 | 可以 | minidump 最大大小。 | | `CRASH_ATTACHMENTS_MAX_MB` | 必填 | 可以 | 附件最大大小。 | | `CRASH_REQUEST_MAX_MB` | 必填 | 可以 | 崩溃报告完整请求最大大小。 | | `CRASH_SYMBOLS_MAX_MB` | 必填 | 可以 | 符号文件最大大小。 | ## 五、客户端配置要同步哪些值 推荐直接使用后台页面生成: ```text 1. 登录后台 2. 选择应用和渠道 3. 如需预置授权,先创建或选择一个 License,页面会自动把可查看的 License 填入“客户端配置生成” 4. 打开“客户端配置生成” 5. 点击“生成配套配置” 6. 点击“复制配置”,粘贴到客户端 `bin/config/app_config.json` 7. 点击“复制 qrc 配置”,粘贴到客户端源码 `config/server_config.json` 8. 重新编译 Launcher / Updater / Bootstrap,让 `api_base_url` 通过 qrc 编进程序 ``` 客户端 `config/app_config.json` 至少要和服务端保持这个值一致: ```json { "client_token": "和服务端 CLIENT_API_TOKEN 一样" } ``` 如果服务端 `.env` 里保持默认: ```text CLIENT_API_TOKEN=SimCAEClientToken2026 ``` 客户端就填: ```json "client_token": "SimCAEClientToken2026" ``` `api_base_url` 是后端 API 地址,走 `SERVER_PORT`,不是 MinIO 地址。它现在位于客户端源码 `config/server_config.json`,并通过 qrc 编译进程序,不再写入客户端 `app_config.json` 或注册表。 ## 六、发布新版本 进入后台后,“发布新版本”支持两种方式: ```text 1. 选择软件发布根目录,例如 SimCAE 目录。Windows 目录内应包含 bin/SimCAE.exe;Linux 目录内应包含 bin/SimCAE 2. 上传压缩发布包,支持 zip、tar.gz、tgz、tar.bz2、tbz2、rar ``` 压缩发布包上传到服务器后,服务端会先解压,再校验是否包含 `.env` 中 `RELEASE_MAIN_EXECUTABLE` 指定的主程序路径。Windows 默认是: ```text RELEASE_MAIN_EXECUTABLE=bin/SimCAE.exe ``` Linux 客户端部署时建议改成: ```text TARGET_PLATFORM=linux RELEASE_MAIN_EXECUTABLE=bin/SimCAE CLIENT_MAIN_EXECUTABLE=SimCAE CLIENT_LAUNCHER_EXECUTABLE=Launcher CLIENT_UPDATER_EXECUTABLE=Updater CLIENT_BOOTSTRAP_EXECUTABLE=Bootstrap ``` `.rar` 需要服务器镜像内有 rar 解压工具。当前 Docker 镜像已内置 `bsdtar`;如果某些 rar 变体仍然解压失败,请改用 zip/tar.gz/tar.bz2,或在服务器镜像中补充 `unrar`/`7z`。 ## 七、常用命令 确认你仍然在 `SimCAEServerDockerPackage` 目录: ```bash pwd ``` 启动: ```bash docker compose up -d ``` 查看状态: ```bash docker compose ps ``` 查看后端日志: ```bash docker compose logs -f api ``` 查看所有容器日志: ```bash docker compose logs -f ``` 重启后端: ```bash docker compose restart api ``` 停止服务: ```bash docker compose down ``` 如果提示 `docker: 'compose' is not a docker command`,说明服务器没有安装 Docker Compose plugin,需要先安装它。 ## 八、数据、备份和迁移 运行后会生成这些目录或文件: ```text .env 当前服务器实际配置 runtime/ 服务端数据库、上传临时目录、崩溃报告等 minio_data/ MinIO 对象数据,也就是上传后的升级文件 keys/ Manifest 签名密钥 ``` 备份或迁移正式环境时,重点备份: ```text .env runtime/ minio_data/ keys/ docker-compose.yml ``` 不要只备份容器。容器可以由镜像重新创建,真正重要的数据在上面这些挂载目录里。 ## 九、常见问题 ### 启动后看不到后端日志 `docker compose up -d` 是后台启动,不会持续打印日志。用下面命令看后端日志: ```bash docker compose logs -f api ``` ### API 容器反复重启,提示 Permission denied: '/data/uploads' 这是 `runtime/` 目录权限不对。进入 `SimCAEServerDockerPackage` 目录后执行: ```bash docker compose down mkdir -p runtime/uploads runtime/upload_spool runtime/crash_storage sudo chown -R $(id -u):$(id -g) runtime docker compose up -d ``` ### 网页能打开,但客户端提示令牌校验失败 检查客户端 `config/app_config.json`: ```json "client_token": "..." ``` 必须和服务端 `.env`: ```text CLIENT_API_TOKEN=... ``` 完全一致。 ### 客户端能连 API,但下载升级文件失败 检查 `.env`: ```text MINIO_PUBLIC_ENDPOINT=http://你的服务器IP:9000 ``` 这个地址必须从客户端能访问。还要确认服务器防火墙放行了 `9000` 端口。 ## 十、安全提醒 `keys/manifest_private_key.pem` 是服务端签 Manifest 的私钥,必须保护好。客户端 SDK 里的 `manifest_public_key.pem` 必须和这个私钥配套,否则客户端会 Manifest 验签失败。 默认用户名、默认密码、默认 token 和默认 MinIO 密码可以直接试跑。正式部署建议改掉,避免多个环境共用同一套公开示例值。 EOF_README python3 - "$PACKAGE_DIR/README.md" "$VERSION" <<'PYREADME' from pathlib import Path import sys path = Path(sys.argv[1]) version = sys.argv[2] text = path.read_text(encoding='utf-8').replace('__VERSION__', version) path.write_text(text, encoding='utf-8') PYREADME cat > "$PACKAGE_DIR/load-images.sh" </dev/null || true else echo "Warning: ./runtime is not writable by the current user." echo "Run: sudo chown -R \$APP_UID_VALUE:\$APP_GID_VALUE ./runtime" fi echo "Next: edit .env, then run: docker compose up -d" EOF chmod +x "$PACKAGE_DIR/load-images.sh" python3 - "$PACKAGE_DIR/.env.example" "$IMAGE_NAME" <<'PYENV' from pathlib import Path import sys path = Path(sys.argv[1]) image = sys.argv[2] text = path.read_text(encoding='utf-8') text = text.replace('SIMCAE_UPDATE_SERVER_IMAGE=simcae-update-server:0.1.0', f'SIMCAE_UPDATE_SERVER_IMAGE={image}') path.write_text(text, encoding='utf-8') PYENV docker save -o "$TAR_FILE" "$IMAGE_NAME" "$MINIO_IMAGE" "$MC_IMAGE" rm -f "$ARCHIVE_FILE" tar -czf "$ARCHIVE_FILE" -C "$(dirname "$PACKAGE_DIR")" "$(basename "$PACKAGE_DIR")" echo "Offline package directory: $PACKAGE_DIR" echo "Offline package archive: $ARCHIVE_FILE" echo "Images tar: $TAR_FILE"