| @@ -1,202 +1,184 @@ | |||
| # EMP 隔离测试环境部署说明 | |||
| # EMP 隔离部署说明 | |||
| 这套部署方案用于“服务器不放源码,只运行本地打好的 Docker 镜像包”的场景。 | |||
| 本目录用于在打包/测试服务器构建 Docker 镜像包,上传到腾讯云 COS,然后在甲方服务器通过包 URL 下载并部署。 | |||
| ## 目录说明 | |||
| 本地项目中: | |||
| ```text | |||
| deploy/isolated/ | |||
| build-package.ps1 # Windows 本地打包 | |||
| build-package.sh # Linux / macOS / WSL 本地打包 | |||
| docker-compose.runtime.yml # 服务器运行用 compose 模板 | |||
| .env.example # 服务器运行配置模板 | |||
| install.sh # 服务器安装脚本 | |||
| dockerfiles/ # 本地构建镜像用 Dockerfile | |||
| nginx/admin.conf # 前端 nginx 代理配置 | |||
| ``` | |||
| 默认环境为 `emp-test`,用于兼容原有流程;UAT 环境使用 `DEPLOY_ENV=emp-uat`。`emp-test` 和 `emp-uat` 都有独立环境变量模板,并共用一份运行用 compose 模板。 | |||
| 打包后产物在: | |||
| ## 目录说明 | |||
| ```text | |||
| deploy/isolated/dist/emp-test-runtime-时间戳.tar.gz | |||
| build-package.sh Linux / WSL 全量打包脚本 | |||
| build-package.ps1 Windows PowerShell 全量打包脚本 | |||
| build-update.sh 增量打包脚本 | |||
| publish-cos.sh 上传 COS 并输出签名 URL | |||
| deploy-from-url.sh 甲方服务器按 URL 下载并部署 | |||
| install.sh 甲方服务器全量安装脚本 | |||
| apply-update.sh 甲方服务器增量更新脚本 | |||
| profiles/docker-compose.yml emp-test / emp-uat 共用运行模板 | |||
| profiles/emp-test/.env.example | |||
| profiles/emp-uat/.env.example | |||
| docker-compose.runtime.yml 旧默认运行模板,保留兼容 | |||
| ``` | |||
| 服务器解压后只需要: | |||
| 打包时 compose 选择顺序为: | |||
| ```text | |||
| docker-compose.yml | |||
| .env | |||
| install.sh | |||
| images.tar | |||
| README.md | |||
| profiles/<DEPLOY_ENV>/docker-compose.yml | |||
| -> profiles/docker-compose.yml | |||
| -> docker-compose.runtime.yml | |||
| -> test/docker-compose.yml | |||
| ``` | |||
| ## 本地打包 | |||
| 正常情况下,`emp-test` 和 `emp-uat` 都使用 `profiles/docker-compose.yml`,差异只来自各自的 `.env.example`。 | |||
| Windows PowerShell: | |||
| ```powershell | |||
| cd E:\emp\deploy\isolated | |||
| .\build-package.ps1 | |||
| ``` | |||
| ## COS 配置 | |||
| Linux / WSL: | |||
| 打包服务器启用 `COS_UPLOAD=1` 前,需要设置: | |||
| ```bash | |||
| cd /path/to/emp/deploy/isolated | |||
| sh build-package.sh | |||
| export COS_SECRET_ID=change-me | |||
| export COS_SECRET_KEY=change-me | |||
| export COS_REGION=ap-chengdu | |||
| export COS_BUCKET=emp-example-bucket | |||
| ``` | |||
| 如果 `deploy` 是独立仓库,不在 `emp` 根目录下,需要指定业务代码根目录: | |||
| 可选配置: | |||
| ```bash | |||
| cd /home/git/emp_test_deploy/isolated | |||
| EMP_ROOT=/home/git/emp sh build-package.sh | |||
| export COS_SIGN_EXPIRE=604800 | |||
| export COS_PREFIX=deploy/emp-uat/runtime/custom | |||
| export COS_CONFIG_PATH=/path/to/.cos.yaml | |||
| ``` | |||
| `EMP_ROOT` 指向的目录下必须包含: | |||
| `publish-cos.sh` 使用腾讯云 `coscli cp` 上传包,并用 `coscli signurl` 生成临时下载 URL。打包服务器需要提前安装 `coscli`。 | |||
| ```text | |||
| emp_server/ | |||
| emp_admin/ | |||
| emp_ws/ | |||
| ``` | |||
| 如果本机已经构建好所有业务镜像,只想重新生成安装包: | |||
| ## UAT 全量打包并上传 | |||
| ```bash | |||
| SKIP_BUILD=1 sh build-package.sh | |||
| cd /home/git/emp/deploy/isolated | |||
| DEPLOY_ENV=emp-uat \ | |||
| COS_UPLOAD=1 \ | |||
| EMP_ROOT=/home/git/emp \ | |||
| ./build-package.sh | |||
| ``` | |||
| 国内 npm 慢时可以指定源: | |||
| Windows PowerShell: | |||
| ```bash | |||
| NPM_REGISTRY=https://registry.npmmirror.com sh build-package.sh | |||
| ```powershell | |||
| cd E:\emp\deploy\isolated | |||
| .\build-package.ps1 -DeployEnv emp-uat -CosUpload | |||
| ``` | |||
| 默认会把 MySQL、Redis、Kafka、TDengine、Nacos 的中间件镜像一起打进 `images.tar`,服务器不需要访问 Docker Hub。 | |||
| 输出示例: | |||
| ## 上传服务器 | |||
| ```bash | |||
| scp deploy/isolated/dist/emp-test-runtime-*.tar.gz root@服务器IP:/opt/ | |||
| ```text | |||
| Package: .../dist/emp-uat-runtime-20260611120000.tar.gz | |||
| COS Key: deploy/emp-uat/runtime/20260611120000/emp-uat-runtime-20260611120000.tar.gz | |||
| SHA256: ... | |||
| URL: https://... | |||
| ``` | |||
| 服务器执行: | |||
| ## 甲方服务器全量部署 | |||
| 先将 `deploy-from-url.sh` 放到甲方服务器。之后执行: | |||
| ```bash | |||
| mkdir -p /opt/emp-test | |||
| tar -xzf /opt/emp-test-runtime-*.tar.gz -C /opt/emp-test --strip-components=1 | |||
| cd /opt/emp-test | |||
| cp .env.example .env | |||
| vi .env | |||
| sh install.sh | |||
| DEPLOY_ENV=emp-uat \ | |||
| DEPLOY_HOME=/home/admin-x99/emp-uat \ | |||
| PACKAGE_SHA256=<打包输出的SHA256> \ | |||
| bash deploy-from-url.sh "<打包输出的URL>" | |||
| ``` | |||
| ## 必改配置 | |||
| `.env` 中至少要改这些: | |||
| 脚本会将包下载到: | |||
| ```env | |||
| PUBLIC_HOST=服务器外网IP或域名 | |||
| MYSQL_ROOT_PASSWORD=强密码 | |||
| DB_PWD=同 MYSQL_ROOT_PASSWORD | |||
| SIMULATOR_DB_PASSWORD=同 MYSQL_ROOT_PASSWORD | |||
| REDIS_PASSWORD=强密码 | |||
| PDF_FRONTEND_BASE_URL=http://服务器外网IP:4081 | |||
| ```text | |||
| /home/admin-x99/emp-uat/packages/<时间戳>/ | |||
| ``` | |||
| 如果端口被宿主机其他项目占用,改这些端口: | |||
| 然后把运行文件复制到: | |||
| ```env | |||
| ADMIN_HOST_PORT=4081 | |||
| GATEWAY_HOST_PORT=9000 | |||
| MYSQL_HOST_PORT=13306 | |||
| KAFKA_HOST_PORT=19094 | |||
| TDENGINE_REST_HOST_PORT=6041 | |||
| ```text | |||
| /home/admin-x99/emp-uat/runtime/ | |||
| ``` | |||
| ## 对外连接 | |||
| 并执行 `install.sh`。 | |||
| MySQL 8.0: | |||
| 如果目标目录下还没有 `.env`,`install.sh` 会先从 `.env.example` 生成 `.env` 并停止。修改密码、`PUBLIC_HOST`、端口和第三方配置后,再执行: | |||
| ```text | |||
| host: 服务器外网IP | |||
| port: MYSQL_HOST_PORT,默认 13306 | |||
| user: root | |||
| password: MYSQL_ROOT_PASSWORD | |||
| database: emp | |||
| ```bash | |||
| cd /home/admin-x99/emp-uat/runtime | |||
| DEPLOY_ENV=emp-uat bash install.sh | |||
| ``` | |||
| Kafka: | |||
| ## 同一台服务器部署 test 和 uat | |||
| ```text | |||
| bootstrap.servers=PUBLIC_HOST:KAFKA_HOST_PORT | |||
| 默认端口:19094 | |||
| ``` | |||
| 同一台甲方服务器可以同时部署两套环境。两套环境使用不同 `DEPLOY_ENV`、`DEPLOY_HOME`、compose project 和宿主机端口。 | |||
| TDengine REST: | |||
| `profiles/docker-compose.yml` 中网关、PDF、Nacos、Redis 默认只在 Docker 内网访问;前端 Nginx 容器会在 Docker 内网代理 `/api/` 和 `/socket.io/`。 | |||
| ```text | |||
| http://PUBLIC_HOST:TDENGINE_REST_HOST_PORT | |||
| 默认端口:6041 | |||
| ``` | |||
| | 环境 | 部署目录 | Compose 项目名 | 前端 | WS | MySQL | 本地 Kafka 可选 | TDengine REST | | |||
| | --- | --- | --- | --- | --- | --- | --- | --- | | |||
| | emp-test | `/home/admin-x99/emp-test` | `emp-test` | 4750 | 4751 | 4752 | 4753 | 4754 | | |||
| | emp-uat | `/home/admin-x99/emp-uat` | `emp-uat` | 4755 | 4756 | 4757 | 4758 | 4759 | | |||
| Nacos: | |||
| `4760` 预留备用。当前公共 compose 模板不对外暴露 Gateway、PDF、Nacos、Redis、TDengine RPC;如需额外暴露,再使用 `4760` 或向甲方申请新端口。 | |||
| ```text | |||
| http://PUBLIC_HOST:NACOS_HOST_PORT/nacos | |||
| 默认账号:nacos | |||
| 默认密码:nacos | |||
| ``` | |||
| 甲方服务器系统重装后,`emp-test` 和 `emp-uat` 都按全量部署重新执行一次;不要只打增量包。 | |||
| 前端: | |||
| Kafka 当前配置: | |||
| ```text | |||
| http://PUBLIC_HOST:ADMIN_HOST_PORT | |||
| ``` | |||
| - `emp-test` 模拟器推送:`ip-cld.cn:29362` / `test-vehicle-real-data`。 | |||
| - `emp-test` 后端消费:`ip-cld.cn:29362` / `YuanJing-test-vehicle-mock-data`。 | |||
| - `emp-uat` 模拟器推送:`ip-cld.cn:29362` / `uat-vehicle-real-data`。 | |||
| - `emp-uat` 后端消费:`ip-cld.cn:29362` / `YuanJing-uat-vehicle-mock-data`。 | |||
| ## 数据导入 | |||
| 内部 Kafka 镜像仍会打进离线包,但 `profiles/docker-compose.yml` 默认不启动 Kafka。需要本地联调内部 Kafka 时,再显式启用 compose profile:`COMPOSE_PROFILES=local-kafka`。 | |||
| MySQL 导入示例: | |||
| 部署 test: | |||
| ```bash | |||
| docker compose --env-file .env -f docker-compose.yml -p emp-test exec -T mysql \ | |||
| mysql -uroot -p"$MYSQL_ROOT_PASSWORD" emp < emp.sql | |||
| DEPLOY_ENV=emp-test \ | |||
| DEPLOY_HOME=/home/admin-x99/emp-test \ | |||
| PACKAGE_SHA256=<打包输出的SHA256> \ | |||
| bash deploy-from-url.sh "<emp-test包URL>" | |||
| ``` | |||
| TDengine 导入建议仍使用 `taosdump`: | |||
| 部署 uat: | |||
| ```bash | |||
| docker cp td_dump_dir emp-test-tdengine-1:/tmp/td_dump | |||
| docker compose --env-file .env -f docker-compose.yml -p emp-test exec tdengine \ | |||
| taosdump -u root -p"$TDENGINE_PWD" -i /tmp/td_dump | |||
| DEPLOY_ENV=emp-uat \ | |||
| DEPLOY_HOME=/home/admin-x99/emp-uat \ | |||
| PACKAGE_SHA256=<打包输出的SHA256> \ | |||
| bash deploy-from-url.sh "<emp-uat包URL>" | |||
| ``` | |||
| ## 常用命令 | |||
| Docker compose 会按项目名隔离容器、网络和数据卷,所以只要 `PROJECT_NAME` 不同,`emp-test` 和 `emp-uat` 的数据卷不会互相覆盖。 | |||
| 查看状态: | |||
| ## 甲方服务器常用命令 | |||
| ```bash | |||
| docker compose --env-file .env -f docker-compose.yml -p emp-test ps | |||
| ``` | |||
| 看日志: | |||
| cd /home/admin-x99/emp-uat/runtime | |||
| ```bash | |||
| docker compose --env-file .env -f docker-compose.yml -p emp-test logs -f emp-gateway | |||
| docker compose --env-file .env -f docker-compose.yml -p emp-uat ps | |||
| docker compose --env-file .env -f docker-compose.yml -p emp-uat logs -f emp-gateway | |||
| docker compose --env-file .env -f docker-compose.yml -p emp-uat down | |||
| ``` | |||
| 停止: | |||
| 查看 test 时把目录和项目名改为 `emp-test`: | |||
| ```bash | |||
| docker compose --env-file .env -f docker-compose.yml -p emp-test down | |||
| cd /home/admin-x99/emp-test/runtime | |||
| docker compose --env-file .env -f docker-compose.yml -p emp-test ps | |||
| ``` | |||
| 停止并删除数据卷: | |||
| ## 手工兜底部署 | |||
| 如果 COS 不可用,仍然可以通过其他方式把 `dist/` 下的包传到甲方服务器。全量包手工部署示例: | |||
| ```bash | |||
| docker compose --env-file .env -f docker-compose.yml -p emp-test down -v | |||
| mkdir -p /home/admin-x99/emp-uat/runtime | |||
| tar -xzf emp-uat-runtime-*.tar.gz -C /home/admin-x99/emp-uat/runtime --strip-components=1 | |||
| cd /home/admin-x99/emp-uat/runtime | |||
| DEPLOY_ENV=emp-uat bash install.sh | |||
| ``` | |||
| @@ -5,12 +5,14 @@ set -Eeuo pipefail | |||
| # Usage: | |||
| # bash apply-update.sh | |||
| # bash apply-update.sh emp-admin emp-monitor | |||
| # PROJECT_NAME=emp-test ENV_FILE=.env COMPOSE_FILE=docker-compose.yml bash apply-update.sh | |||
| # DEPLOY_ENV=emp-uat ENV_FILE=.env COMPOSE_FILE=docker-compose.yml bash apply-update.sh | |||
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | |||
| cd "$SCRIPT_DIR" | |||
| PROJECT_NAME="${PROJECT_NAME:-emp-test}" | |||
| DEPLOY_ENV="${DEPLOY_ENV:-emp-test}" | |||
| PROJECT_NAME="${PROJECT_NAME:-$DEPLOY_ENV}" | |||
| DEPLOY_HOME="${DEPLOY_HOME:-/home/admin-x99/$DEPLOY_ENV}" | |||
| ENV_FILE="${ENV_FILE:-.env}" | |||
| COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}" | |||
| IMAGE_TAR="${IMAGE_TAR:-images.tar}" | |||
| @@ -66,7 +68,9 @@ resolve_runtime_file() { | |||
| local candidates=( | |||
| "../runtime/$default_name" | |||
| "/home/admin-x99/emp-test/runtime/$default_name" | |||
| "$DEPLOY_HOME/runtime/$default_name" | |||
| "$DEPLOY_HOME/$default_name" | |||
| "/opt/$DEPLOY_ENV/$default_name" | |||
| ) | |||
| local candidate | |||
| for candidate in "${candidates[@]}"; do | |||
| @@ -1,10 +1,13 @@ | |||
| param( | |||
| [string]$DeployEnv = $(if ($env:DEPLOY_ENV) { $env:DEPLOY_ENV } else { "emp-test" }), | |||
| [string]$ImageNamespace = $(if ($env:IMAGE_NAMESPACE) { $env:IMAGE_NAMESPACE } else { "emp-test" }), | |||
| [string]$ImageTag = $(if ($env:IMAGE_TAG) { $env:IMAGE_TAG } else { Get-Date -Format "yyyyMMddHHmmss" }), | |||
| [string]$NpmRegistry = $(if ($env:NPM_REGISTRY) { $env:NPM_REGISTRY } else { "https://registry.npmjs.org" }), | |||
| [string]$EmpRoot = $(if ($env:EMP_ROOT) { $env:EMP_ROOT } else { "" }), | |||
| [string]$ProfileDir = $(if ($env:PROFILE_DIR) { $env:PROFILE_DIR } else { "" }), | |||
| [switch]$SkipBuild, | |||
| [switch]$NoMiddlewareImages | |||
| [switch]$NoMiddlewareImages, | |||
| [switch]$CosUpload | |||
| ) | |||
| $ErrorActionPreference = "Stop" | |||
| @@ -14,7 +17,10 @@ $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path | |||
| $RootDir = $null | |||
| $DistDir = Join-Path $ScriptDir "dist" | |||
| $BuildContextDir = Join-Path $ScriptDir ".build-context" | |||
| $PackageName = "emp-test-runtime-$ImageTag" | |||
| if (-not $env:IMAGE_NAMESPACE -and $ImageNamespace -eq "emp-test" -and $DeployEnv -ne "emp-test") { | |||
| $ImageNamespace = $DeployEnv | |||
| } | |||
| $PackageName = $(if ($env:PACKAGE_NAME) { $env:PACKAGE_NAME } else { "$DeployEnv-runtime-$ImageTag" }) | |||
| $PackageDir = Join-Path $DistDir $PackageName | |||
| $PackageArchive = Join-Path $DistDir "$PackageName.tar.gz" | |||
| @@ -202,18 +208,34 @@ function Prepare-Package { | |||
| $ComposeSource = Join-Path $ScriptDir "docker-compose.runtime.yml" | |||
| $EnvSource = Join-Path $ScriptDir ".env.example" | |||
| $ResolvedProfileDir = $(if ($ProfileDir) { $ProfileDir } else { Join-Path $ScriptDir "profiles\$DeployEnv" }) | |||
| $CommonProfileComposeSource = Join-Path $ScriptDir "profiles\docker-compose.yml" | |||
| $ProfileComposeSource = Join-Path $ResolvedProfileDir "docker-compose.yml" | |||
| $ProfileEnvSource = Join-Path $ResolvedProfileDir ".env.example" | |||
| $ProfileEnvTxtSource = Join-Path $ResolvedProfileDir "env.txt" | |||
| $TestComposeSource = Join-Path $ScriptDir "test\docker-compose.yml" | |||
| $TestEnvSource = Join-Path $ScriptDir "test\env.txt" | |||
| if (Test-Path $TestComposeSource) { | |||
| if (Test-Path $ProfileComposeSource) { | |||
| $ComposeSource = $ProfileComposeSource | |||
| } elseif (Test-Path $CommonProfileComposeSource) { | |||
| $ComposeSource = $CommonProfileComposeSource | |||
| } elseif ($DeployEnv -eq "emp-test" -and (Test-Path $TestComposeSource)) { | |||
| $ComposeSource = $TestComposeSource | |||
| } | |||
| if (Test-Path $TestEnvSource) { | |||
| if (Test-Path $ProfileEnvSource) { | |||
| $EnvSource = $ProfileEnvSource | |||
| } elseif (Test-Path $ProfileEnvTxtSource) { | |||
| $EnvSource = $ProfileEnvTxtSource | |||
| } elseif ($DeployEnv -eq "emp-test" -and (Test-Path $TestEnvSource)) { | |||
| $EnvSource = $TestEnvSource | |||
| } | |||
| Copy-Item $ComposeSource (Join-Path $PackageDir "docker-compose.yml") -Force | |||
| Copy-Item $EnvSource (Join-Path $PackageDir ".env.example") -Force | |||
| Copy-Item (Join-Path $ScriptDir "install.sh") (Join-Path $PackageDir "install.sh") -Force | |||
| Copy-Item (Join-Path $ScriptDir "deploy-from-url.sh") (Join-Path $PackageDir "deploy-from-url.sh") -Force | |||
| Copy-Item (Join-Path $ScriptDir "README.md") (Join-Path $PackageDir "README.md") -Force | |||
| } | |||
| @@ -244,6 +266,69 @@ function Archive-Package { | |||
| Write-Log "Package created: $PackageArchive" | |||
| } | |||
| function Publish-Package { | |||
| if (-not $CosUpload -and $env:COS_UPLOAD -ne "1") { | |||
| return | |||
| } | |||
| $CoscliBin = $(if ($env:COSCLI_BIN) { $env:COSCLI_BIN } else { "coscli" }) | |||
| Assert-Command $CoscliBin | |||
| if (-not $env:COS_BUCKET) { | |||
| throw "Missing COS_BUCKET" | |||
| } | |||
| $CoscliOptions = @() | |||
| if ($env:COS_CONFIG_PATH) { | |||
| $CoscliOptions += @("-c", $env:COS_CONFIG_PATH) | |||
| } else { | |||
| if (-not $env:COS_SECRET_ID) { throw "Missing COS_SECRET_ID or COS_CONFIG_PATH" } | |||
| if (-not $env:COS_SECRET_KEY) { throw "Missing COS_SECRET_KEY or COS_CONFIG_PATH" } | |||
| if (-not $env:COS_REGION) { throw "Missing COS_REGION" } | |||
| $CoscliOptions += @( | |||
| "--init-skip=true", | |||
| "-i", $env:COS_SECRET_ID, | |||
| "-k", $env:COS_SECRET_KEY, | |||
| "-e", "cos.$($env:COS_REGION).myqcloud.com" | |||
| ) | |||
| if ($env:COS_TOKEN) { | |||
| $CoscliOptions += @("--token", $env:COS_TOKEN) | |||
| } | |||
| } | |||
| $RunId = Get-Date -Format "yyyyMMddHHmmss" | |||
| $CosSignExpire = $(if ($env:COS_SIGN_EXPIRE) { $env:COS_SIGN_EXPIRE } else { "604800" }) | |||
| $DefaultPrefix = "deploy/$DeployEnv/runtime/$RunId" | |||
| $CosPrefix = $(if ($env:COS_PREFIX) { $env:COS_PREFIX } else { $DefaultPrefix }).Trim("/") | |||
| $PackageBase = Split-Path -Leaf $PackageArchive | |||
| $CosKey = $(if ($env:COS_KEY) { $env:COS_KEY } else { "$CosPrefix/$PackageBase" }).TrimStart("/") | |||
| $CosUri = "cos://$($env:COS_BUCKET)/$CosKey" | |||
| $Sha256 = (Get-FileHash $PackageArchive -Algorithm SHA256).Hash.ToLowerInvariant() | |||
| Write-Log "Upload package to COS: $CosUri" | |||
| & $CoscliBin @(@("cp", $PackageArchive, $CosUri) + $CoscliOptions) | |||
| if ($LASTEXITCODE -ne 0) { | |||
| throw "coscli cp failed, exit code: $LASTEXITCODE" | |||
| } | |||
| Write-Log "Generate signed URL, expire seconds: $CosSignExpire" | |||
| $SignedOutput = & $CoscliBin @(@("signurl", $CosUri, "--time", $CosSignExpire, "--simple-output") + $CoscliOptions) | |||
| if ($LASTEXITCODE -ne 0) { | |||
| throw "coscli signurl failed, exit code: $LASTEXITCODE" | |||
| } | |||
| $SignedUrl = ($SignedOutput | Select-Object -Last 1).ToString() | |||
| Write-Host "" | |||
| Write-Host "Package: $PackageArchive" | |||
| Write-Host "COS Key: $CosKey" | |||
| Write-Host "SHA256: $Sha256" | |||
| Write-Host "URL: $SignedUrl" | |||
| Write-Host "" | |||
| Write-Host "Target deploy command:" | |||
| Write-Host " DEPLOY_ENV=$DeployEnv PACKAGE_SHA256=$Sha256 bash deploy-from-url.sh `"$SignedUrl`"" | |||
| Write-Host "" | |||
| } | |||
| Add-KnownToolPaths | |||
| Assert-Command "docker" | |||
| Assert-DockerDaemon | |||
| @@ -252,6 +337,7 @@ Assert-Command "tar" | |||
| $RootDir = Resolve-EmpRoot | |||
| Write-Log "EMP root: $RootDir" | |||
| Write-Log "Deploy root: $ScriptDir" | |||
| Write-Log "Deploy env: $DeployEnv" | |||
| if (-not $SkipBuild) { | |||
| Assert-Command "mvn" | |||
| @@ -266,3 +352,4 @@ if (-not $SkipBuild) { | |||
| Prepare-Package | |||
| Save-Images | |||
| Archive-Package | |||
| Publish-Package | |||
| @@ -10,14 +10,16 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | |||
| DIST_DIR="$SCRIPT_DIR/dist" | |||
| BUILD_CONTEXT_DIR="$SCRIPT_DIR/.build-context" | |||
| IMAGE_NAMESPACE="${IMAGE_NAMESPACE:-emp-test}" | |||
| DEPLOY_ENV="${DEPLOY_ENV:-emp-test}" | |||
| IMAGE_NAMESPACE="${IMAGE_NAMESPACE:-$DEPLOY_ENV}" | |||
| IMAGE_TAG="${IMAGE_TAG:-$(date '+%Y%m%d%H%M%S')}" | |||
| PACKAGE_NAME="${PACKAGE_NAME:-emp-test-runtime-${IMAGE_TAG}}" | |||
| PACKAGE_NAME="${PACKAGE_NAME:-${DEPLOY_ENV}-runtime-${IMAGE_TAG}}" | |||
| PACKAGE_DIR="$DIST_DIR/$PACKAGE_NAME" | |||
| PACKAGE_ARCHIVE="$DIST_DIR/${PACKAGE_NAME}.tar.gz" | |||
| NPM_REGISTRY="${NPM_REGISTRY:-https://registry.npmjs.org}" | |||
| SKIP_BUILD="${SKIP_BUILD:-0}" | |||
| INCLUDE_MIDDLEWARE_IMAGES="${INCLUDE_MIDDLEWARE_IMAGES:-1}" | |||
| COS_UPLOAD="${COS_UPLOAD:-0}" | |||
| JAVA_MODULES=(emp_gateway emp_auth emp_monitor emp_data) | |||
| APP_IMAGES=( | |||
| @@ -150,21 +152,35 @@ prepare_package() { | |||
| rm -rf "$PACKAGE_DIR" | |||
| mkdir -p "$PACKAGE_DIR" | |||
| local common_profile_compose="$SCRIPT_DIR/profiles/docker-compose.yml" | |||
| local compose_src="$SCRIPT_DIR/docker-compose.runtime.yml" | |||
| local env_src="$SCRIPT_DIR/.env.example" | |||
| if [[ -f "$SCRIPT_DIR/test/docker-compose.yml" ]]; then | |||
| local profile_dir="${PROFILE_DIR:-$SCRIPT_DIR/profiles/$DEPLOY_ENV}" | |||
| if [[ -f "$profile_dir/docker-compose.yml" ]]; then | |||
| compose_src="$profile_dir/docker-compose.yml" | |||
| elif [[ -f "$common_profile_compose" ]]; then | |||
| compose_src="$common_profile_compose" | |||
| elif [[ "$DEPLOY_ENV" == "emp-test" && -f "$SCRIPT_DIR/test/docker-compose.yml" ]]; then | |||
| compose_src="$SCRIPT_DIR/test/docker-compose.yml" | |||
| fi | |||
| if [[ -f "$SCRIPT_DIR/test/env.txt" ]]; then | |||
| if [[ -f "$profile_dir/.env.example" ]]; then | |||
| env_src="$profile_dir/.env.example" | |||
| elif [[ -f "$profile_dir/env.txt" ]]; then | |||
| env_src="$profile_dir/env.txt" | |||
| elif [[ "$DEPLOY_ENV" == "emp-test" && -f "$SCRIPT_DIR/test/env.txt" ]]; then | |||
| env_src="$SCRIPT_DIR/test/env.txt" | |||
| fi | |||
| cp "$compose_src" "$PACKAGE_DIR/docker-compose.yml" | |||
| cp "$env_src" "$PACKAGE_DIR/.env.example" | |||
| cp "$SCRIPT_DIR/install.sh" "$PACKAGE_DIR/install.sh" | |||
| cp "$SCRIPT_DIR/deploy-from-url.sh" "$PACKAGE_DIR/deploy-from-url.sh" | |||
| cp "$SCRIPT_DIR/README.md" "$PACKAGE_DIR/README.md" | |||
| chmod +x "$PACKAGE_DIR/install.sh" | |||
| chmod +x "$PACKAGE_DIR/deploy-from-url.sh" | |||
| } | |||
| save_images() { | |||
| @@ -189,11 +205,21 @@ archive_package() { | |||
| log "Package created: $PACKAGE_ARCHIVE" | |||
| } | |||
| publish_package() { | |||
| if [[ "$COS_UPLOAD" != "1" ]]; then | |||
| return | |||
| fi | |||
| log "Upload package to COS" | |||
| DEPLOY_ENV="$DEPLOY_ENV" PACKAGE_KIND=runtime bash "$SCRIPT_DIR/publish-cos.sh" "$PACKAGE_ARCHIVE" | |||
| } | |||
| need_cmd docker | |||
| need_cmd tar | |||
| log "EMP root: $ROOT_DIR" | |||
| log "Deploy root: $SCRIPT_DIR" | |||
| log "Deploy env: $DEPLOY_ENV" | |||
| if [[ "$SKIP_BUILD" != "1" ]]; then | |||
| need_cmd mvn | |||
| @@ -208,3 +234,4 @@ fi | |||
| prepare_package | |||
| save_images | |||
| archive_package | |||
| publish_package | |||
| @@ -8,16 +8,18 @@ set -Eeuo pipefail | |||
| # EMP_ROOT=/path/to/emp IMAGE_TAG=20260602153000 ./build-update.sh emp-admin emp-monitor | |||
| # | |||
| # Output: | |||
| # dist/emp-test-update-${IMAGE_TAG}-${services}.tar.gz | |||
| # dist/${DEPLOY_ENV}-update-${IMAGE_TAG}-${services}.tar.gz | |||
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | |||
| DIST_DIR="${DIST_DIR:-$SCRIPT_DIR/dist}" | |||
| BUILD_CONTEXT_DIR="$SCRIPT_DIR/.build-context" | |||
| IMAGE_NAMESPACE="${IMAGE_NAMESPACE:-emp-test}" | |||
| DEPLOY_ENV="${DEPLOY_ENV:-emp-test}" | |||
| IMAGE_NAMESPACE="${IMAGE_NAMESPACE:-$DEPLOY_ENV}" | |||
| IMAGE_TAG="${IMAGE_TAG:-$(date '+%Y%m%d%H%M%S')}" | |||
| NPM_REGISTRY="${NPM_REGISTRY:-https://registry.npmjs.org}" | |||
| SKIP_BUILD="${SKIP_BUILD:-0}" | |||
| COS_UPLOAD="${COS_UPLOAD:-0}" | |||
| log() { | |||
| echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | |||
| @@ -212,7 +214,7 @@ build_admin_image() { | |||
| prepare_package() { | |||
| local services_slug="$1" | |||
| PACKAGE_NAME="${PACKAGE_NAME:-emp-test-update-${IMAGE_TAG}-${services_slug}}" | |||
| PACKAGE_NAME="${PACKAGE_NAME:-${DEPLOY_ENV}-update-${IMAGE_TAG}-${services_slug}}" | |||
| PACKAGE_DIR="$DIST_DIR/$PACKAGE_NAME" | |||
| PACKAGE_ARCHIVE="$DIST_DIR/${PACKAGE_NAME}.tar.gz" | |||
| @@ -228,6 +230,7 @@ prepare_package() { | |||
| done > "$PACKAGE_DIR/services.txt" | |||
| { | |||
| echo "deploy_env=$DEPLOY_ENV" | |||
| echo "image_namespace=$IMAGE_NAMESPACE" | |||
| echo "image_tag=$IMAGE_TAG" | |||
| echo "services=${REQUESTED_SERVICES[*]}" | |||
| @@ -255,6 +258,15 @@ archive_package() { | |||
| log "Update package created: $PACKAGE_ARCHIVE" | |||
| } | |||
| publish_package() { | |||
| if [[ "$COS_UPLOAD" != "1" ]]; then | |||
| return | |||
| fi | |||
| log "Upload update package to COS" | |||
| DEPLOY_ENV="$DEPLOY_ENV" PACKAGE_KIND=update bash "$SCRIPT_DIR/publish-cos.sh" "$PACKAGE_ARCHIVE" | |||
| } | |||
| need_cmd docker | |||
| need_cmd tar | |||
| @@ -266,6 +278,7 @@ SERVICES_SLUG="$(IFS=-; echo "${REQUESTED_SERVICES[*]}")" | |||
| log "EMP root: $ROOT_DIR" | |||
| log "Deploy root: $SCRIPT_DIR" | |||
| log "Deploy env: $DEPLOY_ENV" | |||
| log "Update services: ${REQUESTED_SERVICES[*]}" | |||
| log "Image tag: $IMAGE_TAG" | |||
| @@ -304,10 +317,14 @@ fi | |||
| prepare_package "$SERVICES_SLUG" | |||
| save_images | |||
| archive_package | |||
| publish_package | |||
| cat <<EOF | |||
| Next steps on target server: | |||
| DEPLOY_ENV=$DEPLOY_ENV bash deploy-from-url.sh "<COS signed URL>" | |||
| Manual fallback: | |||
| mkdir -p /tmp/emp-update | |||
| tar -xzf $(basename "$PACKAGE_ARCHIVE") -C /tmp/emp-update --strip-components=1 | |||
| cd /tmp/emp-update | |||
| @@ -0,0 +1,142 @@ | |||
| #!/usr/bin/env bash | |||
| set -Eeuo pipefail | |||
| # Download a package URL and deploy it on the target server. | |||
| # Usage: | |||
| # DEPLOY_ENV=emp-uat bash deploy-from-url.sh "<signed package URL>" | |||
| # DEPLOY_ENV=emp-uat PACKAGE_SHA256=<sha256> bash deploy-from-url.sh "<signed package URL>" | |||
| DEPLOY_ENV="${DEPLOY_ENV:-emp-test}" | |||
| PROJECT_NAME="${PROJECT_NAME:-$DEPLOY_ENV}" | |||
| DEPLOY_HOME="${DEPLOY_HOME:-/home/admin-x99/$DEPLOY_ENV}" | |||
| PACKAGE_URL="${1:-}" | |||
| EXPECTED_SHA256="${PACKAGE_SHA256:-${2:-}}" | |||
| RUN_ID="${RUN_ID:-$(date '+%Y%m%d%H%M%S')}" | |||
| PACKAGE_ROOT="${PACKAGE_ROOT:-$DEPLOY_HOME/packages}" | |||
| RUNTIME_DIR="${RUNTIME_DIR:-$DEPLOY_HOME/runtime}" | |||
| WORK_DIR="$PACKAGE_ROOT/$RUN_ID" | |||
| DOWNLOAD_FILE="$WORK_DIR/package.tar.gz" | |||
| EXTRACT_DIR="$WORK_DIR/extracted" | |||
| log() { | |||
| echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | |||
| } | |||
| die() { | |||
| echo "ERROR: $*" >&2 | |||
| exit 1 | |||
| } | |||
| need_cmd() { | |||
| command -v "$1" >/dev/null 2>&1 || die "Missing command: $1" | |||
| } | |||
| download_package() { | |||
| local url="$1" | |||
| local output="$2" | |||
| if command -v curl >/dev/null 2>&1; then | |||
| curl -fL --retry 3 --connect-timeout 20 -o "$output" "$url" | |||
| elif command -v wget >/dev/null 2>&1; then | |||
| wget -O "$output" "$url" | |||
| else | |||
| die "Missing curl or wget" | |||
| fi | |||
| } | |||
| calc_sha256() { | |||
| local file="$1" | |||
| if command -v sha256sum >/dev/null 2>&1; then | |||
| sha256sum "$file" | awk '{print $1}' | |||
| elif command -v shasum >/dev/null 2>&1; then | |||
| shasum -a 256 "$file" | awk '{print $1}' | |||
| else | |||
| die "Missing sha256sum or shasum for checksum verification" | |||
| fi | |||
| } | |||
| verify_checksum() { | |||
| local file="$1" | |||
| local expected="$2" | |||
| [[ -n "$expected" ]] || return | |||
| local actual | |||
| actual="$(calc_sha256 "$file")" | |||
| if [[ "$actual" != "$expected" ]]; then | |||
| die "SHA256 mismatch. expected=$expected actual=$actual" | |||
| fi | |||
| log "SHA256 verified: $actual" | |||
| } | |||
| copy_runtime_package() { | |||
| mkdir -p "$RUNTIME_DIR" | |||
| cp "$EXTRACT_DIR/docker-compose.yml" "$RUNTIME_DIR/docker-compose.yml" | |||
| cp "$EXTRACT_DIR/install.sh" "$RUNTIME_DIR/install.sh" | |||
| cp "$EXTRACT_DIR/images.tar" "$RUNTIME_DIR/images.tar" | |||
| if [[ -f "$EXTRACT_DIR/deploy-from-url.sh" ]]; then | |||
| cp "$EXTRACT_DIR/deploy-from-url.sh" "$RUNTIME_DIR/deploy-from-url.sh" | |||
| fi | |||
| if [[ -f "$EXTRACT_DIR/.env.example" ]]; then | |||
| cp "$EXTRACT_DIR/.env.example" "$RUNTIME_DIR/.env.example" | |||
| fi | |||
| if [[ -f "$EXTRACT_DIR/README.md" ]]; then | |||
| cp "$EXTRACT_DIR/README.md" "$RUNTIME_DIR/README.md" | |||
| fi | |||
| } | |||
| deploy_runtime_package() { | |||
| log "Deploy runtime package to $RUNTIME_DIR" | |||
| copy_runtime_package | |||
| ( | |||
| cd "$RUNTIME_DIR" | |||
| DEPLOY_ENV="$DEPLOY_ENV" \ | |||
| PROJECT_NAME="$PROJECT_NAME" \ | |||
| ENV_FILE=.env \ | |||
| COMPOSE_FILE=docker-compose.yml \ | |||
| IMAGE_TAR=images.tar \ | |||
| bash install.sh | |||
| ) | |||
| } | |||
| deploy_update_package() { | |||
| [[ -f "$RUNTIME_DIR/.env" ]] || die "Missing runtime env file: $RUNTIME_DIR/.env" | |||
| [[ -f "$RUNTIME_DIR/docker-compose.yml" ]] || die "Missing runtime compose file: $RUNTIME_DIR/docker-compose.yml" | |||
| log "Apply update package with runtime dir: $RUNTIME_DIR" | |||
| ( | |||
| cd "$EXTRACT_DIR" | |||
| DEPLOY_ENV="$DEPLOY_ENV" \ | |||
| DEPLOY_HOME="$DEPLOY_HOME" \ | |||
| PROJECT_NAME="$PROJECT_NAME" \ | |||
| ENV_FILE="$RUNTIME_DIR/.env" \ | |||
| COMPOSE_FILE="$RUNTIME_DIR/docker-compose.yml" \ | |||
| IMAGE_TAR=images.tar \ | |||
| bash apply-update.sh | |||
| ) | |||
| } | |||
| [[ -n "$PACKAGE_URL" ]] || die "Usage: DEPLOY_ENV=emp-uat bash deploy-from-url.sh <package-url>" | |||
| need_cmd tar | |||
| mkdir -p "$WORK_DIR" "$EXTRACT_DIR" | |||
| log "Deploy env: $DEPLOY_ENV" | |||
| log "Deploy home: $DEPLOY_HOME" | |||
| log "Download package" | |||
| download_package "$PACKAGE_URL" "$DOWNLOAD_FILE" | |||
| verify_checksum "$DOWNLOAD_FILE" "$EXPECTED_SHA256" | |||
| log "Extract package" | |||
| tar -xzf "$DOWNLOAD_FILE" -C "$EXTRACT_DIR" --strip-components=1 | |||
| if [[ -f "$EXTRACT_DIR/install.sh" && -f "$EXTRACT_DIR/docker-compose.yml" ]]; then | |||
| deploy_runtime_package | |||
| elif [[ -f "$EXTRACT_DIR/apply-update.sh" && -f "$EXTRACT_DIR/images.tar" ]]; then | |||
| deploy_update_package | |||
| else | |||
| die "Unknown package structure: $EXTRACT_DIR" | |||
| fi | |||
| log "Done. Package workspace: $WORK_DIR" | |||
| @@ -1,11 +1,14 @@ | |||
| #!/usr/bin/env bash | |||
| set -Eeuo pipefail | |||
| # 服务器运行脚本:只加载本地镜像包并启动 compose,不做源码构建。 | |||
| # Target server runtime installer. It only loads packaged Docker images and | |||
| # starts docker compose; it does not build source code. | |||
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | |||
| cd "$SCRIPT_DIR" | |||
| PROJECT_NAME="${PROJECT_NAME:-emp-test}" | |||
| DEPLOY_ENV="${DEPLOY_ENV:-emp-test}" | |||
| PROJECT_NAME="${PROJECT_NAME:-$DEPLOY_ENV}" | |||
| ENV_FILE="${ENV_FILE:-.env}" | |||
| COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}" | |||
| IMAGE_TAR="${IMAGE_TAR:-images.tar}" | |||
| @@ -20,7 +23,7 @@ die() { | |||
| } | |||
| need_cmd() { | |||
| command -v "$1" >/dev/null 2>&1 || die "缺少命令:$1" | |||
| command -v "$1" >/dev/null 2>&1 || die "Missing command: $1" | |||
| } | |||
| need_cmd docker | |||
| @@ -30,38 +33,37 @@ if docker compose version >/dev/null 2>&1; then | |||
| elif command -v docker-compose >/dev/null 2>&1; then | |||
| DC=(docker-compose) | |||
| else | |||
| die "未安装 docker compose" | |||
| die "Missing docker compose" | |||
| fi | |||
| if [[ ! -f "$ENV_FILE" ]]; then | |||
| cp .env.example "$ENV_FILE" | |||
| die "已生成 $ENV_FILE,请先修改密码、PUBLIC_HOST 和端口后重新执行:sh install.sh" | |||
| die "Generated $ENV_FILE. Update passwords, PUBLIC_HOST and ports, then rerun: bash install.sh" | |||
| fi | |||
| if [[ ! -f "$COMPOSE_FILE" ]]; then | |||
| die "未找到 $COMPOSE_FILE" | |||
| die "Missing compose file: $COMPOSE_FILE" | |||
| fi | |||
| if [[ -f "$IMAGE_TAR" ]]; then | |||
| log "加载镜像包:$IMAGE_TAR" | |||
| log "Load images: $IMAGE_TAR" | |||
| docker load -i "$IMAGE_TAR" | |||
| else | |||
| log "未找到 $IMAGE_TAR,将尝试使用本机已有镜像或在线拉取镜像" | |||
| log "Image tar not found: $IMAGE_TAR. Compose will use local or remote images." | |||
| fi | |||
| log "启动隔离测试环境:$PROJECT_NAME" | |||
| log "Start deploy env: $DEPLOY_ENV, project: $PROJECT_NAME" | |||
| "${DC[@]}" \ | |||
| --env-file "$ENV_FILE" \ | |||
| -f "$COMPOSE_FILE" \ | |||
| -p "$PROJECT_NAME" \ | |||
| up -d | |||
| log "当前容器状态" | |||
| log "Current service status" | |||
| "${DC[@]}" \ | |||
| --env-file "$ENV_FILE" \ | |||
| -f "$COMPOSE_FILE" \ | |||
| -p "$PROJECT_NAME" \ | |||
| ps | |||
| log "完成。前端地址请访问 .env 中的 PUBLIC_HOST:ADMIN_HOST_PORT。" | |||
| log "Done. Visit http://PUBLIC_HOST:ADMIN_HOST_PORT from the configured .env values." | |||
| @@ -0,0 +1,238 @@ | |||
| x-app-env: &app-env | |||
| env_file: | |||
| - .env | |||
| restart: unless-stopped | |||
| networks: | |||
| - emp-net | |||
| x-java-depends: &java-depends | |||
| nacos: | |||
| condition: service_healthy | |||
| mysql: | |||
| condition: service_healthy | |||
| redis: | |||
| condition: service_healthy | |||
| services: | |||
| mysql: | |||
| image: ${MYSQL_IMAGE:-mysql:8.0} | |||
| restart: unless-stopped | |||
| ports: | |||
| - "0.0.0.0:${MYSQL_HOST_PORT:-23306}:3306" | |||
| environment: | |||
| TZ: Asia/Shanghai | |||
| MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} | |||
| MYSQL_DATABASE: ${MYSQL_DATABASE:-emp} | |||
| command: | |||
| - --character-set-server=utf8mb4 | |||
| - --collation-server=utf8mb4_0900_ai_ci | |||
| - --default-time-zone=+08:00 | |||
| - --max-connections=1000 | |||
| volumes: | |||
| - mysql_data:/var/lib/mysql | |||
| healthcheck: | |||
| test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p\"$${MYSQL_ROOT_PASSWORD}\" --silent"] | |||
| interval: 10s | |||
| timeout: 5s | |||
| retries: 30 | |||
| networks: | |||
| - emp-net | |||
| redis: | |||
| image: ${REDIS_IMAGE:-redis:7-alpine} | |||
| restart: unless-stopped | |||
| environment: | |||
| REDIS_PASSWORD: ${REDIS_PASSWORD} | |||
| command: ["sh", "-c", "redis-server --appendonly yes --requirepass \"$${REDIS_PASSWORD}\""] | |||
| volumes: | |||
| - redis_data:/data | |||
| healthcheck: | |||
| test: ["CMD-SHELL", "redis-cli -a \"$${REDIS_PASSWORD}\" ping | grep -q PONG"] | |||
| interval: 10s | |||
| timeout: 5s | |||
| retries: 30 | |||
| networks: | |||
| - emp-net | |||
| kafka: | |||
| image: ${KAFKA_IMAGE:-bitnami/kafka:3.7.0} | |||
| profiles: | |||
| - local-kafka | |||
| restart: unless-stopped | |||
| ports: | |||
| - "0.0.0.0:${KAFKA_HOST_PORT:-29362}:9094" | |||
| environment: | |||
| ALLOW_PLAINTEXT_LISTENER: "yes" | |||
| KAFKA_CFG_NODE_ID: 1 | |||
| KAFKA_CFG_PROCESS_ROLES: controller,broker | |||
| KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093 | |||
| KAFKA_CFG_LISTENERS: INTERNAL://:9092,CONTROLLER://:9093,EXTERNAL://:9094 | |||
| KAFKA_CFG_ADVERTISED_LISTENERS: INTERNAL://kafka:9092,EXTERNAL://${PUBLIC_HOST}:${KAFKA_HOST_PORT:-29362} | |||
| KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT,CONTROLLER:PLAINTEXT,EXTERNAL:PLAINTEXT | |||
| KAFKA_CFG_INTER_BROKER_LISTENER_NAME: INTERNAL | |||
| KAFKA_CFG_CONTROLLER_LISTENER_NAMES: CONTROLLER | |||
| KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE: "true" | |||
| KAFKA_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 | |||
| KAFKA_CFG_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 | |||
| KAFKA_CFG_TRANSACTION_STATE_LOG_MIN_ISR: 1 | |||
| volumes: | |||
| - kafka_data:/bitnami/kafka | |||
| healthcheck: | |||
| test: ["CMD-SHELL", "/opt/bitnami/kafka/bin/kafka-topics.sh --bootstrap-server 127.0.0.1:9092 --list >/dev/null 2>&1"] | |||
| interval: 10s | |||
| timeout: 5s | |||
| retries: 30 | |||
| networks: | |||
| - emp-net | |||
| kafka-init: | |||
| image: ${KAFKA_IMAGE:-bitnami/kafka:3.7.0} | |||
| profiles: | |||
| - local-kafka | |||
| restart: "no" | |||
| depends_on: | |||
| kafka: | |||
| condition: service_healthy | |||
| entrypoint: ["/bin/bash", "-ec"] | |||
| environment: | |||
| KAFKA_TOPIC: ${KAFKA_TOPIC:-vehicle-data} | |||
| command: | | |||
| echo "create kafka topic: $${KAFKA_TOPIC}" | |||
| /opt/bitnami/kafka/bin/kafka-topics.sh \ | |||
| --bootstrap-server kafka:9092 \ | |||
| --create \ | |||
| --if-not-exists \ | |||
| --topic "$${KAFKA_TOPIC}" \ | |||
| --partitions 3 \ | |||
| --replication-factor 1 | |||
| /opt/bitnami/kafka/bin/kafka-topics.sh \ | |||
| --bootstrap-server kafka:9092 \ | |||
| --describe \ | |||
| --topic "$${KAFKA_TOPIC}" | |||
| networks: | |||
| - emp-net | |||
| tdengine: | |||
| image: ${TDENGINE_IMAGE:-tdengine/tdengine:3.3.6.0} | |||
| hostname: tdengine | |||
| privileged: true | |||
| restart: unless-stopped | |||
| ports: | |||
| - "0.0.0.0:${TDENGINE_REST_HOST_PORT:-37363}:6041" | |||
| environment: | |||
| TZ: Asia/Shanghai | |||
| TAOS_FQDN: tdengine | |||
| TDENGINE_DATABASE: ${TDENGINE_DATABASE:-emp} | |||
| volumes: | |||
| - tdengine_data:/var/lib/taos | |||
| - tdengine_log:/var/log/taos | |||
| healthcheck: | |||
| test: ["CMD-SHELL", "taos -s \"create database if not exists $${TDENGINE_DATABASE}; show databases;\" >/dev/null 2>&1"] | |||
| interval: 10s | |||
| timeout: 5s | |||
| retries: 30 | |||
| networks: | |||
| - emp-net | |||
| nacos: | |||
| image: ${NACOS_IMAGE:-nacos/nacos-server:v2.3.2-slim} | |||
| restart: unless-stopped | |||
| environment: | |||
| MODE: standalone | |||
| JVM_XMS: 256m | |||
| JVM_XMX: 512m | |||
| SPRING_DATASOURCE_PLATFORM: "" | |||
| NACOS_AUTH_ENABLE: ${NACOS_AUTH_ENABLE:-true} | |||
| NACOS_AUTH_IDENTITY_KEY: ${NACOS_AUTH_IDENTITY_KEY:-emp} | |||
| NACOS_AUTH_IDENTITY_VALUE: ${NACOS_AUTH_IDENTITY_VALUE:-emp2026} | |||
| NACOS_AUTH_TOKEN: ${NACOS_AUTH_TOKEN} | |||
| volumes: | |||
| - nacos_data:/home/nacos/data | |||
| - nacos_logs:/home/nacos/logs | |||
| healthcheck: | |||
| test: ["CMD-SHELL", "curl -sf http://127.0.0.1:8848/nacos/actuator/health || exit 1"] | |||
| interval: 10s | |||
| timeout: 5s | |||
| retries: 30 | |||
| networks: | |||
| - emp-net | |||
| emp-gateway: | |||
| <<: *app-env | |||
| image: ${IMAGE_NAMESPACE:-emp-test}/emp-gateway:${IMAGE_TAG:-latest} | |||
| depends_on: | |||
| <<: *java-depends | |||
| emp-auth: | |||
| <<: *app-env | |||
| image: ${IMAGE_NAMESPACE:-emp-test}/emp-auth:${IMAGE_TAG:-latest} | |||
| depends_on: | |||
| <<: *java-depends | |||
| emp-monitor: | |||
| <<: *app-env | |||
| image: ${IMAGE_NAMESPACE:-emp-test}/emp-monitor:${IMAGE_TAG:-latest} | |||
| depends_on: | |||
| <<: *java-depends | |||
| tdengine: | |||
| condition: service_healthy | |||
| emp-pdf: | |||
| condition: service_healthy | |||
| emp-data: | |||
| <<: *app-env | |||
| image: ${IMAGE_NAMESPACE:-emp-test}/emp-data:${IMAGE_TAG:-latest} | |||
| depends_on: | |||
| <<: *java-depends | |||
| tdengine: | |||
| condition: service_healthy | |||
| emp-ws: | |||
| condition: service_started | |||
| emp-pdf: | |||
| <<: *app-env | |||
| image: ${IMAGE_NAMESPACE:-emp-test}/emp-pdf:${IMAGE_TAG:-latest} | |||
| environment: | |||
| PORT: 3100 | |||
| healthcheck: | |||
| test: ["CMD-SHELL", "node -e \"require('http').get('http://127.0.0.1:3100/pdf', r => { r.resume(); process.exit(r.statusCode < 500 ? 0 : 1) }).on('error', () => process.exit(1))\""] | |||
| interval: 10s | |||
| timeout: 5s | |||
| retries: 30 | |||
| emp-ws: | |||
| <<: *app-env | |||
| image: ${IMAGE_NAMESPACE:-emp-test}/emp-ws:${IMAGE_TAG:-latest} | |||
| ports: | |||
| - "0.0.0.0:${WS_HOST_PORT:-37362}:3000" | |||
| depends_on: | |||
| mysql: | |||
| condition: service_healthy | |||
| redis: | |||
| condition: service_healthy | |||
| emp-admin: | |||
| image: ${IMAGE_NAMESPACE:-emp-test}/emp-admin:${IMAGE_TAG:-latest} | |||
| restart: unless-stopped | |||
| ports: | |||
| - "0.0.0.0:${ADMIN_HOST_PORT:-37361}:80" | |||
| depends_on: | |||
| emp-gateway: | |||
| condition: service_started | |||
| emp-ws: | |||
| condition: service_started | |||
| networks: | |||
| - emp-net | |||
| networks: | |||
| emp-net: | |||
| driver: bridge | |||
| volumes: | |||
| mysql_data: | |||
| redis_data: | |||
| kafka_data: | |||
| tdengine_data: | |||
| tdengine_log: | |||
| nacos_data: | |||
| nacos_logs: | |||
| @@ -0,0 +1,108 @@ | |||
| # EMP test runtime variables. | |||
| # Copy to .env on the target server and update passwords, PUBLIC_HOST and ports. | |||
| COMPOSE_PROJECT_NAME=emp-test | |||
| CONTAINER_PREFIX=emp-test | |||
| IMAGE_NAMESPACE=emp-test | |||
| IMAGE_TAG=latest | |||
| PUBLIC_HOST=127.0.0.1 | |||
| ADMIN_HOST_PORT=4750 | |||
| WS_HOST_PORT=4751 | |||
| MYSQL_HOST_PORT=4752 | |||
| KAFKA_HOST_PORT=4753 | |||
| TDENGINE_REST_HOST_PORT=4754 | |||
| REDIS_BIND_HOST=127.0.0.1 | |||
| # profiles/docker-compose.yml 不对外暴露 Gateway/PDF/Nacos/Redis/TDengine RPC。 | |||
| # 4760 预留备用,确需额外暴露服务时再单独分配。 | |||
| MYSQL_IMAGE=mysql:8.0 | |||
| REDIS_IMAGE=redis:7-alpine | |||
| KAFKA_IMAGE=bitnami/kafka:3.7.0 | |||
| TDENGINE_IMAGE=tdengine/tdengine:3.3.6.0 | |||
| NACOS_IMAGE=nacos/nacos-server:v2.3.2-slim | |||
| MYSQL_DATABASE=emp | |||
| MYSQL_ROOT_PASSWORD=change-me-mysql-root | |||
| DB_URL=jdbc:mysql://mysql:3306/emp?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai | |||
| DB_USER=root | |||
| DB_PWD=change-me-mysql-root | |||
| # DB_READ_URL=jdbc:mysql://mysql-read:3306/emp?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai | |||
| # DB_READ_USER=root | |||
| # DB_READ_PWD=change-me-mysql-root | |||
| SIMULATOR_DB_HOST=mysql | |||
| SIMULATOR_DB_PORT=3306 | |||
| SIMULATOR_DB_USER=root | |||
| SIMULATOR_DB_PASSWORD=change-me-mysql-root | |||
| SIMULATOR_DB_DATABASE=emp | |||
| SIMULATOR_DB_LIMIT=0 | |||
| REDIS_HOST=redis | |||
| REDIS_PORT=6379 | |||
| REDIS_PASSWORD=change-me-redis | |||
| REDIS_DB=0 | |||
| KAFKA_BROKERS=ip-cld.cn:29362 | |||
| KAFKA_GROUP_ID=emp-test-data-group | |||
| KAFKA_TOPIC=YuanJing-test-vehicle-mock-data | |||
| KAFKA_USER= | |||
| KAFKA_PWD= | |||
| SIMULATOR_KAFKA_BROKERS=ip-cld.cn:29362 | |||
| SIMULATOR_KAFKA_TOPIC=test-vehicle-real-data | |||
| SIMULATOR_KAFKA_USER= | |||
| SIMULATOR_KAFKA_PASSWORD= | |||
| SIMULATOR_KAFKA_CLIENT_ID=emp-test-simulator | |||
| SIMULATOR_KAFKA_BATCH_SIZE=500 | |||
| TDENGINE_DATABASE=emp | |||
| TDENGINE_USER=root | |||
| TDENGINE_PWD=taosdata | |||
| TDENGINE_URL=jdbc:TAOS-RS://tdengine:6041/emp | |||
| NACOS_ADDR=nacos:8848 | |||
| NACOS_USER=nacos | |||
| NACOS_PWD=nacos | |||
| NACOS_AUTH_ENABLE=true | |||
| NACOS_AUTH_IDENTITY_KEY=emp | |||
| NACOS_AUTH_IDENTITY_VALUE=emp2026 | |||
| NACOS_AUTH_TOKEN=ZW1wLXBsYXRmb3JtLW5hY29zLXNlY3JldC1rZXktMjAyNg== | |||
| SPRING_PROFILES_ACTIVE=prod | |||
| JWT_SECRET=change-me-jwt-secret | |||
| JWT_EXPIRATION=86400000 | |||
| SCHEDULER_ENABLED=true | |||
| EMP_WS_ENV=production | |||
| NODE_ENV=production | |||
| PORT=3000 | |||
| WS_INSTANCES=1 | |||
| WS_HOST=emp-ws | |||
| SERVER_API_BASE_URL=http://emp-gateway:9000/api | |||
| SIMULATOR_ADMIN_USERNAME=admin | |||
| SIMULATOR_LOGIN_AUTH=change-me-login-auth | |||
| SIMULATOR_JWT_SECRET=change-me-jwt-secret | |||
| PDF_SERVICE_URL=http://emp-pdf:3100 | |||
| PDF_FRONTEND_BASE_URL=http://127.0.0.1:4750 | |||
| AMAP_KEY= | |||
| COS_SECRET_ID=change-me | |||
| COS_SECRET_KEY=change-me | |||
| COS_REGION=ap-chengdu | |||
| COS_BUCKET=emp-example-bucket | |||
| COS_PUBLIC_BASE_URL= | |||
| SYNC_BASE_URL=https://example.com | |||
| SYNC_TK=change-me | |||
| SYNC_TENANT_ID=change-me | |||
| SYNC_REPORT_CRON=0 30 2 * * ? | |||
| SYNC_REPORT_SYNC_ENABLED=false | |||
| SYNC_REPORT_SYNC_CONCURRENCY=3 | |||
| SYNC_REPORT_SYNC_GROUP_NAMES= | |||
| SYNC_REPORT_CACHE_MISS_FETCH_ENABLED=false | |||
| GROUP_REPORT_CRON=0 30 4 ? * THU,SUN | |||
| @@ -0,0 +1,108 @@ | |||
| # EMP UAT runtime variables. | |||
| # Copy to .env on the target server and update passwords, PUBLIC_HOST and ports. | |||
| COMPOSE_PROJECT_NAME=emp-uat | |||
| CONTAINER_PREFIX=emp-uat | |||
| IMAGE_NAMESPACE=emp-uat | |||
| IMAGE_TAG=latest | |||
| PUBLIC_HOST=127.0.0.1 | |||
| ADMIN_HOST_PORT=4755 | |||
| WS_HOST_PORT=4756 | |||
| MYSQL_HOST_PORT=4757 | |||
| KAFKA_HOST_PORT=4758 | |||
| TDENGINE_REST_HOST_PORT=4759 | |||
| REDIS_BIND_HOST=127.0.0.1 | |||
| # profiles/docker-compose.yml 不对外暴露 Gateway/PDF/Nacos/Redis/TDengine RPC。 | |||
| # 4760 预留备用,确需额外暴露服务时再单独分配。 | |||
| MYSQL_IMAGE=mysql:8.0 | |||
| REDIS_IMAGE=redis:7-alpine | |||
| KAFKA_IMAGE=bitnami/kafka:3.7.0 | |||
| TDENGINE_IMAGE=tdengine/tdengine:3.3.6.0 | |||
| NACOS_IMAGE=nacos/nacos-server:v2.3.2-slim | |||
| MYSQL_DATABASE=emp | |||
| MYSQL_ROOT_PASSWORD=change-me-mysql-root | |||
| DB_URL=jdbc:mysql://mysql:3306/emp?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai | |||
| DB_USER=root | |||
| DB_PWD=change-me-mysql-root | |||
| # DB_READ_URL=jdbc:mysql://mysql-read:3306/emp?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai | |||
| # DB_READ_USER=root | |||
| # DB_READ_PWD=change-me-mysql-root | |||
| SIMULATOR_DB_HOST=mysql | |||
| SIMULATOR_DB_PORT=3306 | |||
| SIMULATOR_DB_USER=root | |||
| SIMULATOR_DB_PASSWORD=change-me-mysql-root | |||
| SIMULATOR_DB_DATABASE=emp | |||
| SIMULATOR_DB_LIMIT=0 | |||
| REDIS_HOST=redis | |||
| REDIS_PORT=6379 | |||
| REDIS_PASSWORD=change-me-redis | |||
| REDIS_DB=0 | |||
| KAFKA_BROKERS=ip-cld.cn:29362 | |||
| KAFKA_GROUP_ID=emp-uat-data-group | |||
| KAFKA_TOPIC=YuanJing-uat-vehicle-mock-data | |||
| KAFKA_USER= | |||
| KAFKA_PWD= | |||
| SIMULATOR_KAFKA_BROKERS=ip-cld.cn:29362 | |||
| SIMULATOR_KAFKA_TOPIC=uat-vehicle-real-data | |||
| SIMULATOR_KAFKA_USER= | |||
| SIMULATOR_KAFKA_PASSWORD= | |||
| SIMULATOR_KAFKA_CLIENT_ID=emp-uat-simulator | |||
| SIMULATOR_KAFKA_BATCH_SIZE=500 | |||
| TDENGINE_DATABASE=emp | |||
| TDENGINE_USER=root | |||
| TDENGINE_PWD=taosdata | |||
| TDENGINE_URL=jdbc:TAOS-RS://tdengine:6041/emp | |||
| NACOS_ADDR=nacos:8848 | |||
| NACOS_USER=nacos | |||
| NACOS_PWD=nacos | |||
| NACOS_AUTH_ENABLE=true | |||
| NACOS_AUTH_IDENTITY_KEY=emp | |||
| NACOS_AUTH_IDENTITY_VALUE=emp2026 | |||
| NACOS_AUTH_TOKEN=ZW1wLXBsYXRmb3JtLW5hY29zLXNlY3JldC1rZXktMjAyNg== | |||
| SPRING_PROFILES_ACTIVE=prod | |||
| JWT_SECRET=change-me-jwt-secret | |||
| JWT_EXPIRATION=86400000 | |||
| SCHEDULER_ENABLED=true | |||
| EMP_WS_ENV=production | |||
| NODE_ENV=production | |||
| PORT=3000 | |||
| WS_INSTANCES=1 | |||
| WS_HOST=emp-ws | |||
| SERVER_API_BASE_URL=http://emp-gateway:9000/api | |||
| SIMULATOR_ADMIN_USERNAME=admin | |||
| SIMULATOR_LOGIN_AUTH=change-me-login-auth | |||
| SIMULATOR_JWT_SECRET=change-me-jwt-secret | |||
| PDF_SERVICE_URL=http://emp-pdf:3100 | |||
| PDF_FRONTEND_BASE_URL=http://127.0.0.1:4755 | |||
| AMAP_KEY= | |||
| COS_SECRET_ID=change-me | |||
| COS_SECRET_KEY=change-me | |||
| COS_REGION=ap-chengdu | |||
| COS_BUCKET=emp-example-bucket | |||
| COS_PUBLIC_BASE_URL= | |||
| SYNC_BASE_URL=https://example.com | |||
| SYNC_TK=change-me | |||
| SYNC_TENANT_ID=change-me | |||
| SYNC_REPORT_CRON=0 30 2 * * ? | |||
| SYNC_REPORT_SYNC_ENABLED=false | |||
| SYNC_REPORT_SYNC_CONCURRENCY=3 | |||
| SYNC_REPORT_SYNC_GROUP_NAMES= | |||
| SYNC_REPORT_CACHE_MISS_FETCH_ENABLED=false | |||
| GROUP_REPORT_CRON=0 30 4 ? * THU,SUN | |||
| @@ -0,0 +1,120 @@ | |||
| #!/usr/bin/env bash | |||
| set -Eeuo pipefail | |||
| # Upload a runtime/update package to Tencent COS and print a deployable URL. | |||
| # Required when COSCLI is not preconfigured: | |||
| # COS_SECRET_ID, COS_SECRET_KEY, COS_REGION, COS_BUCKET | |||
| # | |||
| # Optional: | |||
| # DEPLOY_ENV=emp-uat | |||
| # PACKAGE_KIND=runtime|update | |||
| # COS_PREFIX=deploy/emp-uat/update/20260611120000 | |||
| # COS_KEY=deploy/emp-uat/update/package.tar.gz | |||
| # COS_SIGN_EXPIRE=604800 | |||
| # COS_CONFIG_PATH=/path/to/.cos.yaml | |||
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | |||
| DEPLOY_ENV="${DEPLOY_ENV:-emp-test}" | |||
| PACKAGE_KIND="${PACKAGE_KIND:-}" | |||
| COSCLI_BIN="${COSCLI_BIN:-coscli}" | |||
| COS_SIGN_EXPIRE="${COS_SIGN_EXPIRE:-604800}" | |||
| log() { | |||
| echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | |||
| } | |||
| die() { | |||
| echo "ERROR: $*" >&2 | |||
| exit 1 | |||
| } | |||
| need_cmd() { | |||
| command -v "$1" >/dev/null 2>&1 || die "Missing command: $1" | |||
| } | |||
| calc_sha256() { | |||
| local file="$1" | |||
| if command -v sha256sum >/dev/null 2>&1; then | |||
| sha256sum "$file" | awk '{print $1}' | |||
| elif command -v shasum >/dev/null 2>&1; then | |||
| shasum -a 256 "$file" | awk '{print $1}' | |||
| else | |||
| echo "" | |||
| fi | |||
| } | |||
| infer_package_kind() { | |||
| local name="$1" | |||
| if [[ -n "$PACKAGE_KIND" ]]; then | |||
| echo "$PACKAGE_KIND" | |||
| elif [[ "$name" == *"-update-"* ]]; then | |||
| echo "update" | |||
| else | |||
| echo "runtime" | |||
| fi | |||
| } | |||
| build_coscli_opts() { | |||
| COSCLI_OPTS=() | |||
| if [[ -n "${COS_CONFIG_PATH:-}" ]]; then | |||
| COSCLI_OPTS+=("-c" "$COS_CONFIG_PATH") | |||
| return | |||
| fi | |||
| [[ -n "${COS_SECRET_ID:-}" ]] || die "Missing COS_SECRET_ID or COS_CONFIG_PATH" | |||
| [[ -n "${COS_SECRET_KEY:-}" ]] || die "Missing COS_SECRET_KEY or COS_CONFIG_PATH" | |||
| [[ -n "${COS_REGION:-}" ]] || die "Missing COS_REGION" | |||
| COSCLI_OPTS+=( | |||
| "--init-skip=true" | |||
| "-i" "$COS_SECRET_ID" | |||
| "-k" "$COS_SECRET_KEY" | |||
| "-e" "cos.$COS_REGION.myqcloud.com" | |||
| ) | |||
| if [[ -n "${COS_TOKEN:-}" ]]; then | |||
| COSCLI_OPTS+=("--token" "$COS_TOKEN") | |||
| fi | |||
| } | |||
| [[ "$#" -eq 1 ]] || die "Usage: bash $SCRIPT_DIR/publish-cos.sh <package.tar.gz>" | |||
| PACKAGE_FILE="$1" | |||
| [[ -f "$PACKAGE_FILE" ]] || die "Package not found: $PACKAGE_FILE" | |||
| [[ -n "${COS_BUCKET:-}" ]] || die "Missing COS_BUCKET" | |||
| need_cmd "$COSCLI_BIN" | |||
| build_coscli_opts | |||
| PACKAGE_FILE="$(cd "$(dirname "$PACKAGE_FILE")" && pwd)/$(basename "$PACKAGE_FILE")" | |||
| PACKAGE_BASE="$(basename "$PACKAGE_FILE")" | |||
| PACKAGE_KIND="$(infer_package_kind "$PACKAGE_BASE")" | |||
| RUN_ID="$(date '+%Y%m%d%H%M%S')" | |||
| DEFAULT_PREFIX="deploy/$DEPLOY_ENV/$PACKAGE_KIND/$RUN_ID" | |||
| COS_PREFIX="${COS_PREFIX:-$DEFAULT_PREFIX}" | |||
| COS_PREFIX="${COS_PREFIX#/}" | |||
| COS_PREFIX="${COS_PREFIX%/}" | |||
| COS_KEY="${COS_KEY:-$COS_PREFIX/$PACKAGE_BASE}" | |||
| COS_KEY="${COS_KEY#/}" | |||
| COS_URI="cos://$COS_BUCKET/$COS_KEY" | |||
| SHA256="$(calc_sha256 "$PACKAGE_FILE")" | |||
| log "Upload package: $PACKAGE_FILE" | |||
| log "COS object: $COS_URI" | |||
| "$COSCLI_BIN" cp "$PACKAGE_FILE" "$COS_URI" "${COSCLI_OPTS[@]}" | |||
| log "Generate signed URL, expire seconds: $COS_SIGN_EXPIRE" | |||
| SIGNED_URL="$("$COSCLI_BIN" signurl "$COS_URI" --time "$COS_SIGN_EXPIRE" --simple-output "${COSCLI_OPTS[@]}")" | |||
| cat <<EOF | |||
| Package: $PACKAGE_FILE | |||
| COS Key: $COS_KEY | |||
| SHA256: $SHA256 | |||
| URL: $SIGNED_URL | |||
| Target deploy command: | |||
| DEPLOY_ENV=$DEPLOY_ENV PACKAGE_SHA256=$SHA256 bash deploy-from-url.sh "$SIGNED_URL" | |||
| EOF | |||
| @@ -1,207 +1,155 @@ | |||
| # 隔离测试环境增量更新说明 | |||
| # EMP 隔离环境增量更新说明 | |||
| 本文档用于后续只更新部分 EMP 服务镜像,不再每次全量打包中间件和所有服务。 | |||
| 增量包用于只更新指定应用服务镜像,不包含 MySQL、Redis、Kafka、TDengine、Nacos 等中间件镜像。 | |||
| ## 一、适用场景 | |||
| 默认环境为 `emp-test`;UAT 环境使用 `DEPLOY_ENV=emp-uat`。 | |||
| 适用于只更新以下应用服务中的一个或多个: | |||
| 甲方服务器系统重装后,`emp-test` 和 `emp-uat` 都需要先重新执行全量部署;全量部署完成并确认 `runtime/.env`、`runtime/docker-compose.yml` 存在后,后续版本再使用增量更新。 | |||
| | 简写 | Compose 服务 | 镜像 | | |||
| | --- | --- | --- | | |||
| | gateway | emp-gateway | emp-test/emp-gateway | | |||
| | auth | emp-auth | emp-test/emp-auth | | |||
| | monitor | emp-monitor | emp-test/emp-monitor | | |||
| | data | emp-data | emp-test/emp-data | | |||
| | pdf | emp-pdf | emp-test/emp-pdf | | |||
| | ws | emp-ws | emp-test/emp-ws | | |||
| | admin | emp-admin | emp-test/emp-admin | | |||
| Kafka 配置应保持为: | |||
| 中间件 MySQL、Redis、Kafka、TDengine、Nacos 不走增量更新包。 | |||
| - `emp-test` 模拟器推送 `ip-cld.cn:29362` / `test-vehicle-real-data`,后端消费 `ip-cld.cn:29362` / `YuanJing-test-vehicle-mock-data`。 | |||
| - `emp-uat` 模拟器推送 `ip-cld.cn:29362` / `uat-vehicle-real-data`,后端消费 `ip-cld.cn:29362` / `YuanJing-uat-vehicle-mock-data`。 | |||
| ## 二、构建机生成增量包 | |||
| 内部 Kafka 镜像仍会打进离线包,但默认不启动、不参与业务链路。 | |||
| 进入部署脚本目录: | |||
| ## 支持更新的服务 | |||
| ```bash | |||
| cd /home/git/emp_test_deploy/isolated | |||
| ``` | |||
| | 简写 | Compose 服务名 | 镜像 | | |||
| | --- | --- | --- | | |||
| | gateway | emp-gateway | `${IMAGE_NAMESPACE}/emp-gateway` | | |||
| | auth | emp-auth | `${IMAGE_NAMESPACE}/emp-auth` | | |||
| | monitor | emp-monitor | `${IMAGE_NAMESPACE}/emp-monitor` | | |||
| | data | emp-data | `${IMAGE_NAMESPACE}/emp-data` | | |||
| | pdf | emp-pdf | `${IMAGE_NAMESPACE}/emp-pdf` | | |||
| | ws | emp-ws | `${IMAGE_NAMESPACE}/emp-ws` | | |||
| | admin | emp-admin | `${IMAGE_NAMESPACE}/emp-admin` | | |||
| 只更新 `admin` 和 `monitor`: | |||
| ## 构建并上传 UAT 增量包 | |||
| ```bash | |||
| EMP_ROOT=/home/git/emp \ | |||
| IMAGE_NAMESPACE=emp-test \ | |||
| ./build-update.sh admin monitor | |||
| ``` | |||
| 生成文件在: | |||
| 打包服务器先设置 COS 配置: | |||
| ```bash | |||
| dist/emp-test-update-<时间戳>-admin-monitor.tar.gz | |||
| export COS_SECRET_ID=change-me | |||
| export COS_SECRET_KEY=change-me | |||
| export COS_REGION=ap-chengdu | |||
| export COS_BUCKET=emp-example-bucket | |||
| ``` | |||
| 也可以更新其他服务: | |||
| 构建指定服务,上传 COS,并输出签名 URL: | |||
| ```bash | |||
| # 只更新前端 | |||
| EMP_ROOT=/home/git/emp IMAGE_NAMESPACE=emp-test ./build-update.sh admin | |||
| # 只更新数据服务 | |||
| EMP_ROOT=/home/git/emp IMAGE_NAMESPACE=emp-test ./build-update.sh data | |||
| cd /home/git/emp/deploy/isolated | |||
| # 更新模拟器 / WebSocket | |||
| EMP_ROOT=/home/git/emp IMAGE_NAMESPACE=emp-test ./build-update.sh ws | |||
| # 更新 PDF 服务 | |||
| EMP_ROOT=/home/git/emp IMAGE_NAMESPACE=emp-test ./build-update.sh pdf | |||
| ``` | |||
| 如果镜像已经在本机构建好,只想重新打包已有镜像: | |||
| ```bash | |||
| DEPLOY_ENV=emp-uat \ | |||
| COS_UPLOAD=1 \ | |||
| EMP_ROOT=/home/git/emp \ | |||
| IMAGE_NAMESPACE=emp-test \ | |||
| SKIP_BUILD=1 \ | |||
| ./build-update.sh admin monitor | |||
| ``` | |||
| ## 三、传输到甲方服务器 | |||
| 将增量包传到甲方服务器任意目录,例如: | |||
| ```bash | |||
| /home/admin-x99/emp-test/update/emp-test-update-20260602153000-admin-monitor.tar.gz | |||
| ``` | |||
| ## 四、甲方服务器应用增量包 | |||
| 进入服务器: | |||
| ```bash | |||
| cd /home/admin-x99/emp-test | |||
| mkdir -p update-runtime | |||
| tar -xzf update/emp-test-update-20260602153000-admin-monitor.tar.gz \ | |||
| -C update-runtime \ | |||
| --strip-components=1 | |||
| cd update-runtime | |||
| ``` | |||
| 执行增量更新: | |||
| 输出示例: | |||
| ```bash | |||
| bash apply-update.sh | |||
| ```text | |||
| Package: .../dist/emp-uat-update-20260611123000-admin-monitor.tar.gz | |||
| COS Key: deploy/emp-uat/update/20260611123000/emp-uat-update-20260611123000-admin-monitor.tar.gz | |||
| SHA256: ... | |||
| URL: https://... | |||
| ``` | |||
| 脚本会优先使用当前目录下的 `.env`、`docker-compose.yml`;如果当前目录没有,会自动查找 `../runtime/` 和 `/home/admin-x99/emp-test/runtime/` 下的运行配置。 | |||
| 脚本会自动执行: | |||
| 如果本机镜像已经构建好,只需要重新打包并上传: | |||
| ```bash | |||
| docker load -i images.tar | |||
| docker compose --env-file .env -f docker-compose.yml -p emp-test \ | |||
| up -d --no-deps --force-recreate emp-admin emp-monitor | |||
| DEPLOY_ENV=emp-uat \ | |||
| COS_UPLOAD=1 \ | |||
| SKIP_BUILD=1 \ | |||
| EMP_ROOT=/home/git/emp \ | |||
| ./build-update.sh admin monitor | |||
| ``` | |||
| 如果当前目录没有 `.env` 和 `docker-compose.yml`,可以显式指定运行环境目录中的文件: | |||
| ```bash | |||
| PROJECT_NAME=emp-test \ | |||
| ENV_FILE=/home/admin-x99/emp-test/runtime/.env \ | |||
| COMPOSE_FILE=/home/admin-x99/emp-test/runtime/docker-compose.yml \ | |||
| bash apply-update.sh | |||
| ``` | |||
| ## 甲方服务器按 URL 应用增量包 | |||
| 也可以手动指定更新服务,覆盖包内 `services.txt`: | |||
| 先确认目标环境已完成全量部署,并存在: | |||
| ```bash | |||
| PROJECT_NAME=emp-test \ | |||
| ENV_FILE=/home/admin-x99/emp-test/runtime/.env \ | |||
| COMPOSE_FILE=/home/admin-x99/emp-test/runtime/docker-compose.yml \ | |||
| bash apply-update.sh emp-admin emp-monitor | |||
| ```text | |||
| /home/admin-x99/emp-uat/runtime/.env | |||
| /home/admin-x99/emp-uat/runtime/docker-compose.yml | |||
| ``` | |||
| ## 五、验证 | |||
| 查看服务状态: | |||
| 执行: | |||
| ```bash | |||
| cd /home/admin-x99/emp-test/runtime | |||
| docker compose --env-file .env -f docker-compose.yml -p emp-test ps emp-admin emp-monitor | |||
| DEPLOY_ENV=emp-uat \ | |||
| DEPLOY_HOME=/home/admin-x99/emp-uat \ | |||
| PACKAGE_SHA256=<打包输出的SHA256> \ | |||
| bash deploy-from-url.sh "<打包输出的URL>" | |||
| ``` | |||
| 查看后端日志: | |||
| 同一台服务器上更新不同环境时,必须使用对应环境参数: | |||
| ```bash | |||
| docker compose --env-file .env -f docker-compose.yml -p emp-test logs --tail=100 emp-monitor | |||
| ``` | |||
| 验证前端: | |||
| # 更新 test | |||
| DEPLOY_ENV=emp-test \ | |||
| DEPLOY_HOME=/home/admin-x99/emp-test \ | |||
| PACKAGE_SHA256=<打包输出的SHA256> \ | |||
| bash deploy-from-url.sh "<emp-test增量包URL>" | |||
| ```bash | |||
| curl -I http://127.0.0.1:37361 | |||
| # 更新 uat | |||
| DEPLOY_ENV=emp-uat \ | |||
| DEPLOY_HOME=/home/admin-x99/emp-uat \ | |||
| PACKAGE_SHA256=<打包输出的SHA256> \ | |||
| bash deploy-from-url.sh "<emp-uat增量包URL>" | |||
| ``` | |||
| ## 六、注意事项 | |||
| 1. 增量更新默认同时打 `latest` 和时间戳 tag。 | |||
| 2. 当前测试环境 `.env` 建议继续使用 `IMAGE_TAG=latest`,不要为了增量包修改成时间戳。 | |||
| 3. 如果把 `.env` 的 `IMAGE_TAG` 改成某个新时间戳,但只传了部分服务镜像,其他服务会因为缺少该 tag 而无法重建。 | |||
| 4. 如果修改了 `docker-compose.yml`、`.env.example`、中间件初始化逻辑,建议重新全量打包或单独同步配置文件。 | |||
| 5. 数据库结构变更不包含在镜像增量包中,需要单独执行 SQL 迁移。 | |||
| ## 七、PDF 导出故障排查 | |||
| 报错: | |||
| `deploy-from-url.sh` 会将包下载到: | |||
| ```text | |||
| PDF导出失败: I/O error on GET request for "http://emp-pdf:3100/pdf": Connection refused | |||
| $DEPLOY_HOME/packages/<时间戳>/ | |||
| ``` | |||
| 含义:`emp-monitor` 已经访问到 Docker 内网地址 `emp-pdf:3100`,但 PDF 服务端口没有进程监听,常见原因是 `emp-pdf` 容器未启动、启动后退出、正在重启,或 Node 服务未正常监听 3100。 | |||
| 然后调用包内的 `apply-update.sh`,并自动使用运行目录中的 `.env` 和 `docker-compose.yml`。 | |||
| 新版 `docker-compose.yml` 已给 `emp-pdf` 增加健康检查,并让 `emp-monitor` 等待 `emp-pdf` 可访问后再启动。健康检查访问 `/pdf`,400 也算通过,因为不带 url 参数时返回 400 代表服务已正常监听。若服务器仍使用旧 compose,需要先同步新的 `docker-compose.yml` 或按下面命令手动重启 PDF 服务。 | |||
| ## 手工应用增量包 | |||
| 先看容器状态: | |||
| 如果已经手工解压增量包,可以在解压目录执行: | |||
| ```bash | |||
| cd /home/admin-x99/emp-test/runtime | |||
| docker compose --env-file .env -f docker-compose.yml -p emp-test ps emp-pdf emp-monitor | |||
| ``` | |||
| 查看 PDF 服务日志: | |||
| cd /tmp/emp-update | |||
| ```bash | |||
| docker compose --env-file .env -f docker-compose.yml -p emp-test logs --tail=200 emp-pdf | |||
| DEPLOY_ENV=emp-uat \ | |||
| DEPLOY_HOME=/home/admin-x99/emp-uat \ | |||
| ENV_FILE=/home/admin-x99/emp-uat/runtime/.env \ | |||
| COMPOSE_FILE=/home/admin-x99/emp-uat/runtime/docker-compose.yml \ | |||
| bash apply-update.sh emp-admin emp-monitor | |||
| ``` | |||
| 在 PDF 容器内检查 PDF 服务是否监听: | |||
| 如果不传服务名,`apply-update.sh` 会读取增量包内的 `services.txt`。 | |||
| ```bash | |||
| docker compose --env-file .env -f docker-compose.yml -p emp-test exec emp-pdf \ | |||
| node -e "require('http').get('http://127.0.0.1:3100/pdf', r => { console.log(r.statusCode); r.resume(); process.exit(r.statusCode < 500 ? 0 : 1) }).on('error', e => { console.error(e.message); process.exit(1) })" | |||
| ``` | |||
| ## 验证 | |||
| 在同一个 Docker 网络里检查 `emp-monitor` 到 `emp-pdf` 的访问: | |||
| 查看 UAT 服务状态: | |||
| ```bash | |||
| docker compose --env-file .env -f docker-compose.yml -p emp-test exec emp-monitor \ | |||
| sh -lc "curl -i http://emp-pdf:3100/pdf || wget -S -O- http://emp-pdf:3100/pdf" | |||
| cd /home/admin-x99/emp-uat/runtime | |||
| docker compose --env-file .env -f docker-compose.yml -p emp-uat ps emp-admin emp-monitor | |||
| ``` | |||
| 如果 `emp-pdf` 未运行或健康检查失败,先重启 PDF 服务: | |||
| 查看日志: | |||
| ```bash | |||
| docker compose --env-file .env -f docker-compose.yml -p emp-test up -d --no-deps --force-recreate emp-pdf | |||
| docker compose --env-file .env -f docker-compose.yml -p emp-uat logs --tail=100 emp-monitor | |||
| ``` | |||
| 再重启 monitor,使其重新调用可用的 PDF 服务: | |||
| 查看 test 时把目录和项目名改为 `emp-test`: | |||
| ```bash | |||
| docker compose --env-file .env -f docker-compose.yml -p emp-test restart emp-monitor | |||
| cd /home/admin-x99/emp-test/runtime | |||
| docker compose --env-file .env -f docker-compose.yml -p emp-test ps emp-admin emp-monitor | |||
| ``` | |||
| 如果日志中出现 Chromium/Puppeteer 相关错误,重新构建并增量更新 `pdf`: | |||
| ## 注意事项 | |||
| ```bash | |||
| EMP_ROOT=/home/git/emp IMAGE_NAMESPACE=emp-test ./build-update.sh pdf | |||
| ``` | |||
| 1. 增量包默认同时包含 `latest` 和时间戳两个镜像 tag。 | |||
| 2. 目标服务器 `.env` 建议保持 `IMAGE_TAG=latest`,除非确定所有服务都已经按同一个时间戳 tag 部署。 | |||
| 3. 如果修改了 `docker-compose.yml`、`.env.example` 或中间件初始化逻辑,建议重新打全量包,或单独同步运行配置。 | |||
| 4. 数据库结构变更不包含在镜像增量包中,需要单独执行 SQL 迁移。 | |||
| 5. 同一台服务器同时部署 `emp-test` 和 `emp-uat` 时,更新命令里的 `DEPLOY_ENV`、`DEPLOY_HOME`、包 URL 必须匹配同一个环境。 | |||