feat: 添加 sw2 服务器 Docker 部署与回退流程
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
# KRA CI/CD 部署
|
||||
|
||||
sw2.fuhua91.com 在 226 服务器的 Docker 部署见 [Docker 部署说明](docker/README.md)。下面保留的是通用 Kubernetes/Jenkins 模板说明。
|
||||
|
||||
deploy/ 目录是一套与项目名解耦的部署模板,基于 kra 框架的新项目可以整体复制后只改一个参数块。
|
||||
|
||||
> 完整的环境配置、凭据来源与排障说明见 [DEPLOYMENT.md](DEPLOYMENT.md)。
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# sw2 Docker 部署
|
||||
|
||||
沿用 226 服务器 `/www/wwwroot/live.fuhua91.com` 的 Docker 网络、持久化目录和宝塔反向代理结构。使用独立的 `sw2-backend`、`sw2-frontend`、`sw2-net`,兼容服务器现有 Docker 18.06,不依赖 Compose。
|
||||
|
||||
## 目录与端口
|
||||
|
||||
部署根目录为 `/www/wwwroot/sw2.fuhua91.com`:
|
||||
|
||||
- `config/config.yaml`:后端运行配置,挂载到 `/data/conf`,UID 1000 可写。
|
||||
- `runtime.env`:SDK 兼容参数,通过 Docker `--env-file` 加载,权限 600。
|
||||
- `log/`、`uploads/`:日志与文件持久化目录。
|
||||
- `releases/<commit>/`:已校验的发布包;`current-release` 记录当前版本。
|
||||
- 后端仅绑定 `127.0.0.1:9602`,前端仅绑定 `127.0.0.1:9601`。
|
||||
- 宝塔 `sw2.fuhua91.com` 代理至 9601,前端容器将 `/api/` 转发至后端并去除前缀,支持 WebSocket、SSE 和文件上传。
|
||||
|
||||
数据库沿用已迁移并验证的 `sw9`,平台别名保留 `lx/hw/yq/oa`,Redis 沿用当前关闭状态。生产配置使用 `admin.app.env: production`,关闭开发 Swagger。五条常规商务任务启用,两个历史回填停用。**使用同一 sw9 时只运行一个调度实例,切换到服务器前停止本机的后端服务。**
|
||||
|
||||
## 构建与发布
|
||||
|
||||
Windows 本机安装 Go、Node、pnpm、tar,先提交源代码,再执行:
|
||||
|
||||
```powershell
|
||||
.\deploy\docker\build-package.ps1
|
||||
```
|
||||
|
||||
输出位于 `.cache/deploy/sw2-<commit>.tar.gz` 及对应 SHA-256 文件。后端交叉编译为 Linux amd64,前端执行生产构建;包内只含二进制、前端产物、Dockerfile 和部署配置,真实运行配置及环境变量不进入镜像或 Git。
|
||||
|
||||
通过 SSH 将包上传到 226,校验 SHA-256 后解压到 `releases/<commit>/`。首次部署另行准备上述私有配置文件,保持 `admin.router_prefix` 为空,并确认主库为 sw9。SDK 环境变量名称为 `SW_SDK_AUTH_CODE`、`SW_SDK_MIGRATE_AUTH_CODE`、`SW_SDK_TABLE_PREFIX`,值从已有私有运行配置取得。
|
||||
|
||||
```bash
|
||||
bash /www/wwwroot/sw2.fuhua91.com/releases/<commit>/deploy.sh <commit> --build-only
|
||||
# 构建和容器配置检查通过后,停止连接同一 sw9 的本机后端。
|
||||
bash /www/wwwroot/sw2.fuhua91.com/releases/<commit>/deploy.sh <commit>
|
||||
```
|
||||
|
||||
脚本先构建和检查镜像,再停止当前受管容器,保留一份停止的 `-previous` 容器。健康检查失败时恢复前一组容器。配置、上传文件和数据库不随镜像替换;索引迁移由后端启动按版本执行。
|
||||
|
||||
首次接入域名时,备份宝塔原站点配置,再安装 `bt-nginx-sw2.fuhua91.com.conf` 至 `/www/server/panel/vhost/nginx/`,执行 `/www/server/nginx/sbin/nginx -t` 通过后重载。模板保留证书验证目录,当前监听 HTTP;后续由宝塔为 sw2 单独配置匹配域名的证书。
|
||||
|
||||
验证首页、静态资源、`/api/health`、登录状态、商务接口和任务启停/执行日志。回退版本可执行 `bash /www/wwwroot/sw2.fuhua91.com/deploy.sh <旧commit>`;数据库迁移及业务数据不由镜像回退脚本撤销。
|
||||
@@ -0,0 +1,19 @@
|
||||
FROM docker.m.daocloud.io/alpine:3.22@sha256:2e0b81d60597f449baa56dc25d355367869d2dca4cd57e866f1e2b52af3eb55f
|
||||
|
||||
RUN apk add --no-cache ca-certificates tzdata \
|
||||
&& addgroup -S -g 1000 sw2 \
|
||||
&& adduser -S -D -H -u 1000 -G sw2 sw2 \
|
||||
&& mkdir -p /app/logs /app/uploads/file /app/uploads/chunks /data/conf \
|
||||
&& chown -R sw2:sw2 /app /data/conf
|
||||
|
||||
ARG VERSION
|
||||
LABEL org.opencontainers.image.revision=$VERSION
|
||||
ENV TZ=Asia/Shanghai
|
||||
COPY --chown=sw2:sw2 kratos-admin /app/kratos-admin
|
||||
RUN chmod 0755 /app/kratos-admin
|
||||
WORKDIR /app
|
||||
USER sw2
|
||||
EXPOSE 8000
|
||||
HEALTHCHECK --interval=15s --timeout=5s --start-period=30s --retries=3 \
|
||||
CMD wget -q -O /dev/null http://127.0.0.1:8000/health || exit 1
|
||||
CMD ["/app/kratos-admin", "-conf", "/data/conf"]
|
||||
@@ -0,0 +1,41 @@
|
||||
map $http_upgrade $sw2_site_connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name sw2.fuhua91.com;
|
||||
root /www/wwwroot/sw2.fuhua91.com;
|
||||
server_tokens off;
|
||||
client_max_body_size 1024m;
|
||||
|
||||
# 保留宝塔证书验证目录,配置、日志和发布包不作为静态站点提供。
|
||||
include /www/server/panel/vhost/nginx/well-known/sw2.fuhua91.com.conf;
|
||||
include /www/server/panel/vhost/nginx/extension/sw2.fuhua91.com/*.conf;
|
||||
location ^~ /.well-known/ {
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location ~ /\. {
|
||||
return 404;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:9601;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $sw2_site_connection_upgrade;
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
proxy_read_timeout 600s;
|
||||
proxy_send_timeout 600s;
|
||||
}
|
||||
|
||||
access_log /www/wwwlogs/sw2.fuhua91.com.log;
|
||||
error_log /www/wwwlogs/sw2.fuhua91.com.error.log;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
param([string]$Tag = '')
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
|
||||
Push-Location $repoRoot
|
||||
try {
|
||||
if (-not $Tag) { $Tag = (& git rev-parse --short=12 HEAD).Trim() }
|
||||
if ($Tag -notmatch '^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,100}$') { throw 'Invalid release tag' }
|
||||
& git diff --quiet -- . ':!configs/config.yaml'
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Commit source changes before packaging' }
|
||||
& git diff --cached --quiet
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Commit staged changes before packaging' }
|
||||
|
||||
$outputRoot = Join-Path $repoRoot '.cache\deploy'
|
||||
$stage = Join-Path $outputRoot $Tag
|
||||
$archive = Join-Path $outputRoot "sw2-$Tag.tar.gz"
|
||||
if (Test-Path -LiteralPath $archive) { throw "Release archive already exists: $archive" }
|
||||
New-Item -ItemType Directory -Force -Path (Join-Path $stage 'backend'), (Join-Path $stage 'frontend') | Out-Null
|
||||
$previous = @{}
|
||||
try {
|
||||
foreach ($name in @('GOOS', 'GOARCH', 'CGO_ENABLED')) {
|
||||
$previous[$name] = [Environment]::GetEnvironmentVariable($name, 'Process')
|
||||
}
|
||||
$env:GOOS = 'linux'
|
||||
$env:GOARCH = 'amd64'
|
||||
$env:CGO_ENABLED = '0'
|
||||
& go build -trimpath -ldflags "-s -w -X main.Version=$Tag" -o (Join-Path $stage 'backend\kratos-admin') ./cmd
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Linux backend build failed' }
|
||||
} finally {
|
||||
foreach ($name in $previous.Keys) { [Environment]::SetEnvironmentVariable($name, $previous[$name], 'Process') }
|
||||
}
|
||||
|
||||
Push-Location (Join-Path $repoRoot 'web')
|
||||
try {
|
||||
& pnpm build
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Frontend build failed' }
|
||||
} finally { Pop-Location }
|
||||
|
||||
Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'backend.Dockerfile') -Destination (Join-Path $stage 'backend\Dockerfile')
|
||||
Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'frontend.Dockerfile') -Destination (Join-Path $stage 'frontend\Dockerfile')
|
||||
Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'nginx.conf') -Destination (Join-Path $stage 'frontend\nginx.conf')
|
||||
$frontendDist = Join-Path $stage 'frontend\dist'
|
||||
New-Item -ItemType Directory -Force -Path $frontendDist | Out-Null
|
||||
Get-ChildItem -LiteralPath (Join-Path $repoRoot 'web\dist') | ForEach-Object {
|
||||
Copy-Item -LiteralPath $_.FullName -Destination $frontendDist -Recurse -Force
|
||||
}
|
||||
foreach ($name in @('deploy.sh', 'bt-nginx-sw2.fuhua91.com.conf')) {
|
||||
Copy-Item -LiteralPath (Join-Path $PSScriptRoot $name) -Destination (Join-Path $stage $name)
|
||||
}
|
||||
& tar -czf $archive -C $stage .
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Release archive failed' }
|
||||
$hash = (Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
[IO.File]::WriteAllText("$archive.sha256", "$hash $([IO.Path]::GetFileName($archive))`n", [Text.UTF8Encoding]::new($false))
|
||||
Write-Output "Release: $archive"
|
||||
Write-Output "SHA256: $hash"
|
||||
} finally { Pop-Location }
|
||||
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
TAG="${1:?Usage: deploy.sh TAG [--build-only]}"
|
||||
MODE="${2:-}"
|
||||
[[ "$TAG" =~ ^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,100}$ ]] || { echo 'Invalid release tag' >&2; exit 1; }
|
||||
[[ -z "$MODE" || "$MODE" == '--build-only' ]] || { echo 'Unknown mode' >&2; exit 1; }
|
||||
BASE_DIR=/www/wwwroot/sw2.fuhua91.com
|
||||
RELEASE="$BASE_DIR/releases/$TAG"
|
||||
BACKEND_IMAGE="sw2/backend:$TAG"
|
||||
FRONTEND_IMAGE="sw2/frontend:$TAG"
|
||||
NETWORK=sw2-net
|
||||
NAMES=(sw2-backend sw2-frontend)
|
||||
|
||||
command -v docker >/dev/null
|
||||
command -v curl >/dev/null
|
||||
test -f "$RELEASE/backend/Dockerfile"
|
||||
test -f "$RELEASE/frontend/Dockerfile"
|
||||
|
||||
docker build --build-arg "VERSION=$TAG" -t "$BACKEND_IMAGE" "$RELEASE/backend"
|
||||
docker build --build-arg "VERSION=$TAG" -t "$FRONTEND_IMAGE" "$RELEASE/frontend"
|
||||
docker run --rm "$BACKEND_IMAGE" /app/kratos-admin -h >/dev/null
|
||||
docker run --rm --add-host sw2-backend:127.0.0.1 "$FRONTEND_IMAGE" nginx -t
|
||||
if [[ "$MODE" == '--build-only' ]]; then exit 0; fi
|
||||
|
||||
test -s "$BASE_DIR/config/config.yaml"
|
||||
test -f "$BASE_DIR/runtime.env"
|
||||
install -d -o 1000 -g 1000 -m 0750 "$BASE_DIR/config" "$BASE_DIR/log" "$BASE_DIR/uploads"
|
||||
chown 1000:1000 "$BASE_DIR/config/config.yaml"
|
||||
chmod 0600 "$BASE_DIR/config/config.yaml" "$BASE_DIR/runtime.env"
|
||||
if ! docker network inspect "$NETWORK" >/dev/null 2>&1; then
|
||||
docker network create --label com.sw2.application=sw2 "$NETWORK"
|
||||
fi
|
||||
|
||||
managed() {
|
||||
[[ "$(docker inspect --format '{{index .Config.Labels "com.sw2.application"}}' "$1")" == sw2 ]]
|
||||
}
|
||||
exists() { docker inspect "$1" >/dev/null 2>&1; }
|
||||
for name in "${NAMES[@]}"; do
|
||||
for candidate in "$name" "$name-previous"; do
|
||||
if exists "$candidate"; then
|
||||
managed "$candidate" || { echo "Refusing to replace unmanaged container: $candidate" >&2; exit 1; }
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
moved=()
|
||||
created=()
|
||||
rollback() {
|
||||
result=$?
|
||||
trap - ERR
|
||||
set +eu
|
||||
for name in "${created[@]}"; do docker rm -f "$name" >/dev/null 2>&1; done
|
||||
for name in "${moved[@]}"; do
|
||||
if exists "$name-previous"; then docker rename "$name-previous" "$name"; fi
|
||||
docker start "$name"
|
||||
done
|
||||
echo 'Deployment failed; previous containers restored where present.' >&2
|
||||
exit "$result"
|
||||
}
|
||||
trap rollback ERR
|
||||
for name in "${NAMES[@]}"; do
|
||||
if exists "$name-previous"; then docker rm "$name-previous" >/dev/null; fi
|
||||
if exists "$name"; then
|
||||
moved+=("$name")
|
||||
docker stop -t 30 "$name" >/dev/null
|
||||
docker rename "$name" "$name-previous"
|
||||
fi
|
||||
done
|
||||
|
||||
created+=(sw2-backend)
|
||||
docker run -d --name sw2-backend --label com.sw2.application=sw2 \
|
||||
--restart unless-stopped --network "$NETWORK" \
|
||||
--log-opt max-size=20m --log-opt max-file=3 \
|
||||
-p 127.0.0.1:9602:8000 --env-file "$BASE_DIR/runtime.env" \
|
||||
-v "$BASE_DIR/config:/data/conf" -v "$BASE_DIR/log:/app/logs" -v "$BASE_DIR/uploads:/app/uploads" \
|
||||
"$BACKEND_IMAGE"
|
||||
|
||||
ready=false
|
||||
for attempt in $(seq 1 45); do
|
||||
if curl -fsS --max-time 3 http://127.0.0.1:9602/health >/dev/null 2>&1; then ready=true; break; fi
|
||||
sleep 2
|
||||
done
|
||||
[[ "$ready" == true ]] || { echo 'Backend health check failed' >&2; false; }
|
||||
|
||||
created+=(sw2-frontend)
|
||||
docker run -d --name sw2-frontend --label com.sw2.application=sw2 \
|
||||
--restart unless-stopped --network "$NETWORK" \
|
||||
--log-opt max-size=20m --log-opt max-file=3 \
|
||||
-p 127.0.0.1:9601:80 "$FRONTEND_IMAGE"
|
||||
|
||||
ready=false
|
||||
for attempt in $(seq 1 15); do
|
||||
if curl -fsS --max-time 3 http://127.0.0.1:9601/api/health >/dev/null 2>&1; then ready=true; break; fi
|
||||
sleep 2
|
||||
done
|
||||
[[ "$ready" == true ]] || { echo 'Frontend API proxy health check failed' >&2; false; }
|
||||
trap - ERR
|
||||
printf '%s\n' "$TAG" > "$BASE_DIR/current-release"
|
||||
install -m 0750 "$RELEASE/deploy.sh" "$BASE_DIR/deploy.sh"
|
||||
docker ps --filter label=com.sw2.application=sw2 --format '{{.Names}} {{.Image}} {{.Status}} {{.Ports}}'
|
||||
@@ -0,0 +1,8 @@
|
||||
FROM docker.m.daocloud.io/nginx:1.28-alpine@sha256:a8b39bd9cf0f83869a2162827a0caf6137ddf759d50a171451b335cecc87d236
|
||||
|
||||
ARG VERSION
|
||||
LABEL org.opencontainers.image.revision=$VERSION
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY dist/ /usr/share/nginx/html/
|
||||
HEALTHCHECK --interval=15s --timeout=5s --start-period=10s --retries=3 \
|
||||
CMD wget -q -O /dev/null http://127.0.0.1/healthz || exit 1
|
||||
@@ -0,0 +1,53 @@
|
||||
map $http_upgrade $sw2_connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
map $http_x_forwarded_proto $sw2_forwarded_proto {
|
||||
default $scheme;
|
||||
http http;
|
||||
https https;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
server_tokens off;
|
||||
client_max_body_size 1024m;
|
||||
|
||||
location = /healthz {
|
||||
access_log off;
|
||||
default_type text/plain;
|
||||
return 200 'ok';
|
||||
}
|
||||
|
||||
location = /api {
|
||||
return 308 /api/;
|
||||
}
|
||||
|
||||
location ^~ /api/ {
|
||||
proxy_pass http://sw2-backend:8000/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $http_x_real_ip;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $sw2_forwarded_proto;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $sw2_connection_upgrade;
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
proxy_read_timeout 600s;
|
||||
proxy_send_timeout 600s;
|
||||
}
|
||||
|
||||
location ~ /\. {
|
||||
return 404;
|
||||
}
|
||||
|
||||
location / {
|
||||
add_header Cache-Control 'no-cache';
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user