Просмотр исходного кода

feat: 支持双环境COS部署

master
leiyun 1 неделю назад
Родитель
Сommit
4f361f7cae
12 измененных файлов: 1075 добавлений и 292 удалений
  1. +108
    -126
      isolated/README.md
  2. +7
    -3
      isolated/apply-update.sh
  3. +91
    -4
      isolated/build-package.ps1
  4. +31
    -4
      isolated/build-package.sh
  5. +20
    -3
      isolated/build-update.sh
  6. +142
    -0
      isolated/deploy-from-url.sh
  7. +14
    -12
      isolated/install.sh
  8. +238
    -0
      isolated/profiles/docker-compose.yml
  9. +108
    -0
      isolated/profiles/emp-test/.env.example
  10. +108
    -0
      isolated/profiles/emp-uat/.env.example
  11. +120
    -0
      isolated/publish-cos.sh
  12. +88
    -140
      isolated/update.MD

+ 108
- 126
isolated/README.md Просмотреть файл

@@ -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
```

+ 7
- 3
isolated/apply-update.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


+ 91
- 4
isolated/build-package.ps1 Просмотреть файл

@@ -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

+ 31
- 4
isolated/build-package.sh Просмотреть файл

@@ -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

+ 20
- 3
isolated/build-update.sh Просмотреть файл

@@ -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


+ 142
- 0
isolated/deploy-from-url.sh Просмотреть файл

@@ -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"

+ 14
- 12
isolated/install.sh Просмотреть файл

@@ -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."

+ 238
- 0
isolated/profiles/docker-compose.yml Просмотреть файл

@@ -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:

+ 108
- 0
isolated/profiles/emp-test/.env.example Просмотреть файл

@@ -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

+ 108
- 0
isolated/profiles/emp-uat/.env.example Просмотреть файл

@@ -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

+ 120
- 0
isolated/publish-cos.sh Просмотреть файл

@@ -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

+ 88
- 140
isolated/update.MD Просмотреть файл

@@ -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 必须匹配同一个环境。

Загрузка…
Отмена
Сохранить