改用镜像层增量发布
This commit is contained in:
parent
4a201f8480
commit
b4b80f2eab
@ -84,7 +84,8 @@
|
||||
- 当前服务器只有 2GB 内存,只负责运行容器、健康检查和镜像切换。
|
||||
- 严禁在服务器执行 `docker build`、`docker compose build`、`npm install`、`npm run production`、`composer install` 或其他源码构建命令。
|
||||
- `compose.jiangling.yml` 不得出现 `build:`;服务器只能运行开发机已经构建并上传的 `caiwu_akaunting:<commit>` 镜像。
|
||||
- `mariadb:11.4` 运行镜像也由开发机缓存并随发布包上传,服务器不得在发布时临时访问 Docker Hub。
|
||||
- `mariadb:11.4` 运行镜像由开发机缓存并仅随首次初始化全量包上传,服务器不得在发布时临时访问 Docker Hub。
|
||||
- 日常发布必须通过只监听 `127.0.0.1` 的服务器本机 Registry 和 SSH 隧道传输,仅上传 Registry 中不存在的镜像层;禁止每次上传完整 `docker save` 压缩包。
|
||||
- 服务器保留当前镜像和上一个可回滚镜像,其他本项目旧镜像在发布成功后清理。
|
||||
- 服务器发布必须使用互斥锁;同一时间只允许一个人切换版本,禁止并发覆盖。
|
||||
|
||||
@ -96,12 +97,13 @@
|
||||
- Dockerfile 必须先复制锁文件再安装依赖:只有 `composer.lock`、`package-lock.json` 或基础镜像定义变化时,才允许对应依赖层失效。
|
||||
- 前端资源必须由开发机宿主系统运行 `scripts/build-frontend.ps1` 构建;脚本固定并复用项目专用 Node 20,不得在 Docker 虚拟机或服务器内执行前端打包。
|
||||
- 基础镜像、Composer 下载缓存、npm 下载缓存和未变化的构建层应直接复用,不得每次全量下载。
|
||||
- `-BootstrapServer` 仅用于新服务器首次全量初始化;已有服务器的日常发布自动走增量镜像层传输。
|
||||
|
||||
### 8.4 磁盘和缓存上限
|
||||
|
||||
- Docker Desktop 虚拟磁盘上限建议设为 60GB;宿主机至少保留 15GB 可用空间。前端打包使用宿主机内存,不要求为了本项目调高 Docker 虚拟机内存。
|
||||
- 发布脚本使用独立缓存并将 BuildKit 缓存控制在 8GB 内,只清理 7 天前的本项目缓存。
|
||||
- 开发机和服务器默认各保留 2 个本项目镜像;上传用临时 `image.tar` 无论成功失败都必须删除。
|
||||
- 开发机、服务器 Docker 和服务器本机 Registry 默认各保留 2 个本项目版本;上传用临时 `image.tar` 无论成功失败都必须删除。
|
||||
- 禁止脚本自动执行全局 `docker system prune -a`、删除其他项目镜像、删除卷或重置 Docker Desktop。
|
||||
- Docker 虚拟磁盘文件不会因为普通清理立即缩小;日常目标是控制已用空间不再增长,不得让新人自行删除或重建虚拟磁盘。
|
||||
|
||||
|
||||
@ -11,7 +11,10 @@ class Console
|
||||
{
|
||||
public static function run($string, $timeout = 0)
|
||||
{
|
||||
$command = sprintf('%s %s %s', escapeshellarg(PHP_BINARY), Application::artisanBinary(), $string);
|
||||
// PHP_BINARY is empty under some web SAPIs (including Apache mod_php).
|
||||
// PHP_BINDIR still points to the directory containing the active PHP binary.
|
||||
$phpBinary = PHP_BINARY ?: PHP_BINDIR . DIRECTORY_SEPARATOR . 'php';
|
||||
$command = sprintf('%s %s %s', escapeshellarg($phpBinary), Application::artisanBinary(), $string);
|
||||
|
||||
logger('Console command:: ' . $command);
|
||||
|
||||
|
||||
122
deploy/activate-incremental-release.sh
Normal file
122
deploy/activate-incremental-release.sh
Normal file
@ -0,0 +1,122 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
REGISTRY_IMAGE="${1:?REGISTRY_IMAGE is required}"
|
||||
IMAGE_TAG="${2:?IMAGE_TAG is required}"
|
||||
REMOTE_DIR="${3:-/opt/caiwuxitong}"
|
||||
DEPLOY_SHA="${4:?DEPLOY_SHA is required}"
|
||||
RUN_MIGRATIONS="${5:-0}"
|
||||
KEEP_IMAGES="${6:-2}"
|
||||
IMAGE_NAME="caiwu_akaunting"
|
||||
LOCK_FILE="$REMOTE_DIR/.deploy.lock"
|
||||
REGISTRY_API="http://127.0.0.1:5000/v2/caiwuxitong/app"
|
||||
TAG_HISTORY="$REMOTE_DIR/.registry-tags"
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "activate-incremental-release.sh must run as root" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$REMOTE_DIR/runtime/.env" ]; then
|
||||
echo "Missing $REMOTE_DIR/runtime/.env; run bootstrap-server.sh first" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec 9>"$LOCK_FILE"
|
||||
if ! flock -n 9; then
|
||||
echo "Another CaiWuXiTong deployment is already running" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PREVIOUS_IMAGE="$(docker inspect caiwu_app --format '{{.Config.Image}}' 2>/dev/null || true)"
|
||||
PREVIOUS_SHA="$(cat "$REMOTE_DIR/.deployed-sha" 2>/dev/null || true)"
|
||||
|
||||
docker pull "$REGISTRY_IMAGE"
|
||||
docker tag "$REGISTRY_IMAGE" "$IMAGE_NAME:$IMAGE_TAG"
|
||||
|
||||
LOADED_SHA="$(docker image inspect "$IMAGE_NAME:$IMAGE_TAG" --format '{{index .Config.Labels "org.opencontainers.image.revision"}}')"
|
||||
if [ "$LOADED_SHA" != "$DEPLOY_SHA" ]; then
|
||||
echo "Image revision mismatch: expected $DEPLOY_SHA, got $LOADED_SHA" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$REMOTE_DIR"
|
||||
export IMAGE_TAG RUN_MIGRATIONS
|
||||
docker compose -f compose.jiangling.yml up -d --remove-orphans
|
||||
|
||||
healthy=0
|
||||
attempt=1
|
||||
while [ "$attempt" -le 30 ]; do
|
||||
if curl -fsS -o /dev/null http://127.0.0.1/; then
|
||||
healthy=1
|
||||
break
|
||||
fi
|
||||
|
||||
sleep 5
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
if [ "$healthy" -ne 1 ]; then
|
||||
docker compose -f compose.jiangling.yml logs --tail 120 app >&2 || true
|
||||
|
||||
case "$PREVIOUS_IMAGE" in
|
||||
"$IMAGE_NAME":*)
|
||||
IMAGE_TAG="${PREVIOUS_IMAGE#*:}"
|
||||
RUN_MIGRATIONS=0
|
||||
export IMAGE_TAG RUN_MIGRATIONS
|
||||
docker compose -f compose.jiangling.yml up -d app || true
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Release failed health check; previous image was restored when available" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '%s\n' "$DEPLOY_SHA" > "$REMOTE_DIR/.deployed-sha.tmp"
|
||||
mv "$REMOTE_DIR/.deployed-sha.tmp" "$REMOTE_DIR/.deployed-sha"
|
||||
|
||||
docker image ls "$IMAGE_NAME" --format '{{.Repository}}:{{.Tag}}' \
|
||||
| awk '!seen[$0]++' \
|
||||
| tail -n "+$((KEEP_IMAGES + 1))" \
|
||||
| while IFS= read -r old_image; do
|
||||
[ -n "$old_image" ] && docker image rm "$old_image" >/dev/null 2>&1 || true
|
||||
done
|
||||
|
||||
docker image prune -f --filter 'label=com.caiwu.project=CaiWuXiTong' >/dev/null || true
|
||||
|
||||
# Track tags by successful deployment order, retain only current + previous,
|
||||
# delete older manifests, and reclaim their unreferenced blobs.
|
||||
touch "$TAG_HISTORY"
|
||||
awk -v tag="$IMAGE_TAG" '$0 != tag && NF { print }' "$TAG_HISTORY" > "$TAG_HISTORY.tmp"
|
||||
printf '%s\n' "$IMAGE_TAG" >> "$TAG_HISTORY.tmp"
|
||||
mv "$TAG_HISTORY.tmp" "$TAG_HISTORY"
|
||||
|
||||
deleted_registry_tag=0
|
||||
while [ "$(wc -l < "$TAG_HISTORY")" -gt "$KEEP_IMAGES" ]; do
|
||||
OLD_TAG="$(sed -n '1p' "$TAG_HISTORY")"
|
||||
DIGEST="$(curl -fsSI \
|
||||
-H 'Accept: application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json' \
|
||||
"$REGISTRY_API/manifests/$OLD_TAG" \
|
||||
| awk -F ': ' 'tolower($1) == "docker-content-digest" { gsub("\r", "", $2); print $2 }')"
|
||||
|
||||
if [ -n "$DIGEST" ]; then
|
||||
curl -fsS -X DELETE "$REGISTRY_API/manifests/$DIGEST" >/dev/null
|
||||
deleted_registry_tag=1
|
||||
fi
|
||||
docker image rm "127.0.0.1:5000/caiwuxitong/app:$OLD_TAG" >/dev/null 2>&1 || true
|
||||
sed '1d' "$TAG_HISTORY" > "$TAG_HISTORY.tmp"
|
||||
mv "$TAG_HISTORY.tmp" "$TAG_HISTORY"
|
||||
done
|
||||
|
||||
if [ "$deleted_registry_tag" -eq 1 ]; then
|
||||
systemctl stop docker-registry
|
||||
if ! docker-registry garbage-collect --delete-untagged /etc/docker/registry/config.yml; then
|
||||
echo "Registry garbage collection failed; the service will still be restarted" >&2
|
||||
fi
|
||||
systemctl start docker-registry
|
||||
fi
|
||||
|
||||
docker compose -f compose.jiangling.yml ps
|
||||
echo "DEPLOYED_SHA=$DEPLOY_SHA"
|
||||
|
||||
: "$PREVIOUS_SHA"
|
||||
24
deploy/registry-config.yml
Normal file
24
deploy/registry-config.yml
Normal file
@ -0,0 +1,24 @@
|
||||
version: 0.1
|
||||
|
||||
log:
|
||||
fields:
|
||||
service: caiwuxitong-registry
|
||||
|
||||
storage:
|
||||
cache:
|
||||
blobdescriptor: inmemory
|
||||
filesystem:
|
||||
rootdirectory: /var/lib/docker-registry
|
||||
delete:
|
||||
enabled: true
|
||||
|
||||
http:
|
||||
addr: 127.0.0.1:5000
|
||||
headers:
|
||||
X-Content-Type-Options: [nosniff]
|
||||
|
||||
health:
|
||||
storagedriver:
|
||||
enabled: true
|
||||
interval: 10s
|
||||
threshold: 3
|
||||
56
deploy/setup-incremental-registry.sh
Normal file
56
deploy/setup-incremental-registry.sh
Normal file
@ -0,0 +1,56 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
REMOTE_DIR="${1:-/opt/caiwuxitong}"
|
||||
CONFIG_SOURCE="${2:-$REMOTE_DIR/registry-config.yml}"
|
||||
REGISTRY_CONFIG="/etc/docker/registry/config.yml"
|
||||
REGISTRY_REPOSITORY="127.0.0.1:5000/caiwuxitong/app"
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "setup-incremental-registry.sh must run as root" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$CONFIG_SOURCE" ]; then
|
||||
echo "Missing registry config: $CONFIG_SOURCE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v docker-registry >/dev/null 2>&1; then
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends docker-registry
|
||||
fi
|
||||
|
||||
install -d -m 755 "$(dirname "$REGISTRY_CONFIG")"
|
||||
install -m 644 "$CONFIG_SOURCE" "$REGISTRY_CONFIG"
|
||||
systemctl enable docker-registry >/dev/null
|
||||
systemctl restart docker-registry
|
||||
|
||||
attempt=1
|
||||
while [ "$attempt" -le 20 ]; do
|
||||
if curl -fsS -o /dev/null http://127.0.0.1:5000/v2/; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
curl -fsS -o /dev/null http://127.0.0.1:5000/v2/
|
||||
|
||||
# Seed the registry from the image already on this server. This makes the first
|
||||
# incremental release reuse existing layers instead of uploading them again.
|
||||
CURRENT_IMAGE="$(docker inspect caiwu_app --format '{{.Config.Image}}' 2>/dev/null || true)"
|
||||
case "$CURRENT_IMAGE" in
|
||||
caiwu_akaunting:*)
|
||||
CURRENT_TAG="${CURRENT_IMAGE#*:}"
|
||||
docker tag "$CURRENT_IMAGE" "$REGISTRY_REPOSITORY:$CURRENT_TAG"
|
||||
docker push "$REGISTRY_REPOSITORY:$CURRENT_TAG"
|
||||
touch "$REMOTE_DIR/.registry-tags"
|
||||
if ! grep -Fxq "$CURRENT_TAG" "$REMOTE_DIR/.registry-tags"; then
|
||||
printf '%s\n' "$CURRENT_TAG" >> "$REMOTE_DIR/.registry-tags"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "REGISTRY_READY=127.0.0.1:5000"
|
||||
@ -9,8 +9,8 @@
|
||||
```text
|
||||
任务分支开发 -> 本机验证 -> PR -> 合并 main
|
||||
-> 开发机复用缓存构建镜像
|
||||
-> 临时导出并上传镜像
|
||||
-> 2GB 服务器只加载和切换镜像
|
||||
-> 经 SSH 隧道只上传变化的镜像层
|
||||
-> 2GB 服务器只拉取本机 Registry 并切换镜像
|
||||
-> 真实页面健康检查
|
||||
-> 保留当前与上一个镜像,清理其余本项目产物
|
||||
```
|
||||
@ -28,7 +28,7 @@
|
||||
|
||||
说明:Docker 虚拟磁盘清理后,Windows 上看到的 `.vhdx` 文件可能不会立刻变小,但内部可用空间已经释放。不要为了让文件数字变小而删除虚拟磁盘。
|
||||
|
||||
## 3. 为什么后续不会反复全量下载
|
||||
## 3. 为什么后续不会反复全量下载和上传
|
||||
|
||||
项目使用专属构建器 `caiwu-builder`,缓存与其他项目分开:
|
||||
|
||||
@ -39,8 +39,12 @@
|
||||
- npm 优先使用本机下载缓存,`node_modules` 原位刷新,不会逐版本堆积;
|
||||
- 业务代码变化只重新执行前端资源构建和后面的代码复制;
|
||||
- 日常命令不带 `--pull` 和 `--no-cache`。
|
||||
- 首次初始化才上传应用和 MariaDB 的全量包;日常发布通过 SSH 隧道连接服务器本机 Registry。
|
||||
- Registry 按内容哈希判断镜像层是否已经存在,基础环境、PHP 依赖和未变化的前端层不会重复上传。
|
||||
- Registry 只监听服务器 `127.0.0.1:5000`,公网无法直接访问;发布脚本退出时会关闭 SSH 隧道。
|
||||
- 每次发布后只保留当前和上一个 Registry 版本,并回收旧版本不再引用的镜像层。
|
||||
|
||||
首次构建必然较慢;后续构建应大量显示 `CACHED`。
|
||||
首次构建和首次上传必然较慢;后续构建应大量显示 `CACHED`,上传量由本次实际变化的镜像层决定。
|
||||
|
||||
## 4. 首次初始化服务器
|
||||
|
||||
@ -60,6 +64,7 @@
|
||||
- 创建 `/opt/caiwuxitong/runtime/.env`;
|
||||
- 在服务器本地生成数据库密码和 Laravel `APP_KEY`,不会打印出来;
|
||||
- 创建持久化数据库卷和上传文件卷。
|
||||
- 上传应用与 MariaDB 首次全量包,并初始化仅限本机访问的增量 Registry。
|
||||
|
||||
首次打开 `http://118.196.84.79` 后,由负责人或指定人员在网页安装向导中设置管理员账号和密码。
|
||||
|
||||
@ -81,13 +86,13 @@ git pull --ff-only
|
||||
1. 检查分支和未提交改动;
|
||||
2. 使用项目专用 Node 20 和本机 npm 缓存构建前端资源;
|
||||
3. 使用提交 SHA 构建镜像并复用项目专属缓存;
|
||||
4. 将应用镜像和本机缓存的 MariaDB 运行镜像导出为 gzip 临时包并上传;
|
||||
5. 服务器加载镜像并启动;
|
||||
4. 将应用镜像导出到临时文件,通过 SSH 隧道只上传 Registry 缺少的镜像层;
|
||||
5. 服务器从本机 Registry 拉取新版本并启动;
|
||||
6. 检查真实 HTTP 页面;
|
||||
7. 失败时恢复上一个镜像;
|
||||
8. 使用服务器互斥锁阻止两个人同时切换版本;
|
||||
9. 删除本地和服务器临时镜像包;
|
||||
10. 两端仅保留当前和上一个本项目镜像;
|
||||
9. 删除本地临时镜像包并关闭 SSH 隧道;
|
||||
10. 开发机、服务器 Docker 和服务器 Registry 均仅保留当前和上一个本项目版本;
|
||||
11. 清理 7 天前的本项目构建缓存,并将总量压到最多 8GB。
|
||||
|
||||
## 6. 数据库迁移
|
||||
@ -120,6 +125,7 @@ git pull --ff-only
|
||||
- 禁止登录服务器运行 `docker build` 或 `docker compose build`;
|
||||
- 禁止在服务器运行 Composer、npm 或前端打包;
|
||||
- 禁止把 `.pem`、`.env`、密码、数据库文件或 `image.tar` 提交到 Git;
|
||||
- 禁止把服务器 Registry 的 `5000` 端口开放到公网;
|
||||
- 禁止为了省空间删除 Docker 卷;
|
||||
- 禁止全局清空 Docker,因为同一台电脑可能还有其他项目;
|
||||
- 禁止跳过健康检查后口头说“部署完成”。
|
||||
@ -133,4 +139,5 @@ git pull --ff-only
|
||||
- 是否执行数据库迁移;
|
||||
- 本地临时包是否删除;
|
||||
- 本地/服务器保留了几个本项目镜像;
|
||||
- 本次增量新增镜像层大小和传输耗时;
|
||||
- BuildKit 缓存当前占用。
|
||||
|
||||
@ -29,7 +29,10 @@ $localTar = Join-Path $tempRoot 'image.tar'
|
||||
$localArchive = Join-Path $tempRoot 'image.tar.gz'
|
||||
$remoteArchive = $null
|
||||
$bootstrapRemote = $null
|
||||
$sshTunnel = $null
|
||||
$buildkitConfig = Join-Path $repoRoot 'deploy/buildkitd.toml'
|
||||
$regctlVersion = 'v0.11.5'
|
||||
$regctlSha256 = '9b83975d464e4ee5a9f899a3ead26072ed67e9a8ed0f582897bfad8141b45938'
|
||||
|
||||
function Invoke-Native {
|
||||
param(
|
||||
@ -94,6 +97,91 @@ function Compress-Gzip {
|
||||
}
|
||||
}
|
||||
|
||||
function Get-RegctlPath {
|
||||
$toolRoot = Join-Path $env:LOCALAPPDATA 'CaiWuXiTong\tools'
|
||||
$regctlPath = Join-Path $toolRoot "regctl-$regctlVersion.exe"
|
||||
|
||||
if (Test-Path -LiteralPath $regctlPath) {
|
||||
$actualHash = (Get-FileHash -LiteralPath $regctlPath -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
if ($actualHash -eq $regctlSha256) {
|
||||
return $regctlPath
|
||||
}
|
||||
Remove-Item -LiteralPath $regctlPath -Force
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Path $toolRoot -Force | Out-Null
|
||||
$downloadPath = "$regctlPath.download"
|
||||
try {
|
||||
Invoke-WebRequest `
|
||||
-Uri "https://github.com/regclient/regclient/releases/download/$regctlVersion/regctl-windows-amd64.exe" `
|
||||
-OutFile $downloadPath
|
||||
$actualHash = (Get-FileHash -LiteralPath $downloadPath -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
if ($actualHash -ne $regctlSha256) {
|
||||
throw "regctl 校验失败:预期 $regctlSha256,实际 $actualHash。"
|
||||
}
|
||||
Move-Item -LiteralPath $downloadPath -Destination $regctlPath -Force
|
||||
}
|
||||
finally {
|
||||
if (Test-Path -LiteralPath $downloadPath) {
|
||||
Remove-Item -LiteralPath $downloadPath -Force
|
||||
}
|
||||
}
|
||||
|
||||
return $regctlPath
|
||||
}
|
||||
|
||||
function Get-AvailableLocalPort {
|
||||
$listener = [Net.Sockets.TcpListener]::new([Net.IPAddress]::Loopback, 0)
|
||||
try {
|
||||
$listener.Start()
|
||||
return ([Net.IPEndPoint]$listener.LocalEndpoint).Port
|
||||
}
|
||||
finally {
|
||||
$listener.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
function Start-RegistryTunnel {
|
||||
param([int]$LocalPort)
|
||||
|
||||
$sshPath = (Get-Command ssh -ErrorAction Stop).Source
|
||||
$startInfo = [Diagnostics.ProcessStartInfo]::new()
|
||||
$startInfo.FileName = $sshPath
|
||||
$startInfo.UseShellExecute = $false
|
||||
$startInfo.CreateNoWindow = $true
|
||||
foreach ($argument in @(
|
||||
'-N',
|
||||
'-i', $keyFullPath,
|
||||
'-o', 'BatchMode=yes',
|
||||
'-o', 'ExitOnForwardFailure=yes',
|
||||
'-o', 'ServerAliveInterval=15',
|
||||
'-L', "127.0.0.1:${LocalPort}:127.0.0.1:5000",
|
||||
$sshTarget
|
||||
)) {
|
||||
$startInfo.ArgumentList.Add($argument)
|
||||
}
|
||||
|
||||
$process = [Diagnostics.Process]::Start($startInfo)
|
||||
$attempt = 1
|
||||
while ($attempt -le 20) {
|
||||
if ($process.HasExited) {
|
||||
throw 'SSH 增量传输隧道启动失败。'
|
||||
}
|
||||
try {
|
||||
Invoke-WebRequest -Uri "http://127.0.0.1:$LocalPort/v2/" -TimeoutSec 1 | Out-Null
|
||||
return $process
|
||||
}
|
||||
catch {
|
||||
Start-Sleep -Milliseconds 500
|
||||
$attempt++
|
||||
}
|
||||
}
|
||||
|
||||
$process.Kill()
|
||||
$process.Dispose()
|
||||
throw 'SSH 隧道已建立,但无法访问服务器本机 Registry。'
|
||||
}
|
||||
|
||||
Push-Location $repoRoot
|
||||
try {
|
||||
$branch = (git branch --show-current).Trim()
|
||||
@ -186,29 +274,69 @@ try {
|
||||
ssh -i $keyFullPath -o BatchMode=yes $sshTarget "docker version >/dev/null && docker compose version >/dev/null && install -d -m 700 '$RemotePath'"
|
||||
} '服务器 Docker 未就绪;首次发布请添加 -BootstrapServer。'
|
||||
|
||||
docker image inspect $DatabaseImage 2>$null | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
docker image inspect $DatabaseMirrorImage 2>$null | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Invoke-Native { docker pull $DatabaseMirrorImage } '下载数据库运行镜像失败。'
|
||||
}
|
||||
Invoke-Native { docker tag $DatabaseMirrorImage $DatabaseImage } '标记数据库运行镜像失败。'
|
||||
}
|
||||
|
||||
Invoke-Native {
|
||||
docker save --output $localTar $fullImage $DatabaseImage
|
||||
} '导出应用与数据库运行镜像失败。'
|
||||
Compress-Gzip -Source $localTar -Destination $localArchive
|
||||
Remove-Item -LiteralPath $localTar -Force
|
||||
$archiveSizeMB = [math]::Round((Get-Item -LiteralPath $localArchive).Length / 1MB, 1)
|
||||
Write-Host "压缩传输包:$archiveSizeMB MiB"
|
||||
Invoke-Native { scp -i $keyFullPath -o BatchMode=yes $localArchive "${sshTarget}:$remoteArchive" } '上传镜像失败。'
|
||||
Invoke-Native { scp -i $keyFullPath -o BatchMode=yes compose.jiangling.yml deploy/activate-release.sh "${sshTarget}:$RemotePath/" } '上传运行配置失败。'
|
||||
|
||||
$migrationFlag = if ($AllowMigrations) { '1' } else { '0' }
|
||||
Invoke-Native {
|
||||
ssh -i $keyFullPath -o BatchMode=yes $sshTarget "chmod 700 '$RemotePath/activate-release.sh' && '$RemotePath/activate-release.sh' '$remoteArchive' '$imageTag' '$RemotePath' '$deploySha' '$migrationFlag' '$KeepImages'"
|
||||
} '服务器激活或健康检查失败。'
|
||||
scp -i $keyFullPath -o BatchMode=yes `
|
||||
compose.jiangling.yml `
|
||||
deploy/activate-release.sh `
|
||||
deploy/activate-incremental-release.sh `
|
||||
deploy/setup-incremental-registry.sh `
|
||||
deploy/registry-config.yml `
|
||||
"${sshTarget}:$RemotePath/"
|
||||
} '上传运行配置失败。'
|
||||
|
||||
if ($BootstrapServer) {
|
||||
docker image inspect $DatabaseImage 2>$null | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
docker image inspect $DatabaseMirrorImage 2>$null | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Invoke-Native { docker pull $DatabaseMirrorImage } '下载数据库运行镜像失败。'
|
||||
}
|
||||
Invoke-Native { docker tag $DatabaseMirrorImage $DatabaseImage } '标记数据库运行镜像失败。'
|
||||
}
|
||||
|
||||
Invoke-Native {
|
||||
docker save --output $localTar $fullImage $DatabaseImage
|
||||
} '导出应用与数据库运行镜像失败。'
|
||||
Compress-Gzip -Source $localTar -Destination $localArchive
|
||||
Remove-Item -LiteralPath $localTar -Force
|
||||
$archiveSizeMB = [math]::Round((Get-Item -LiteralPath $localArchive).Length / 1MB, 1)
|
||||
Write-Host "首次初始化全量包:$archiveSizeMB MiB"
|
||||
Invoke-Native { scp -i $keyFullPath -o BatchMode=yes $localArchive "${sshTarget}:$remoteArchive" } '上传镜像失败。'
|
||||
Invoke-Native {
|
||||
ssh -i $keyFullPath -o BatchMode=yes $sshTarget "chmod 700 '$RemotePath/'*.sh && '$RemotePath/activate-release.sh' '$remoteArchive' '$imageTag' '$RemotePath' '$deploySha' '$migrationFlag' '$KeepImages' && '$RemotePath/setup-incremental-registry.sh' '$RemotePath' '$RemotePath/registry-config.yml'"
|
||||
} '服务器激活、健康检查或增量仓库初始化失败。'
|
||||
}
|
||||
else {
|
||||
Invoke-Native {
|
||||
ssh -i $keyFullPath -o BatchMode=yes $sshTarget "chmod 700 '$RemotePath/'*.sh && (curl -fsS -o /dev/null http://127.0.0.1:5000/v2/ || '$RemotePath/setup-incremental-registry.sh' '$RemotePath' '$RemotePath/registry-config.yml')"
|
||||
} '服务器增量镜像仓库初始化失败。'
|
||||
|
||||
Invoke-Native { docker save --output $localTar $fullImage } '导出本次应用镜像失败。'
|
||||
$regctlPath = Get-RegctlPath
|
||||
$localRegistryPort = Get-AvailableLocalPort
|
||||
$sshTunnel = Start-RegistryTunnel -LocalPort $localRegistryPort
|
||||
$registryImage = "127.0.0.1:${localRegistryPort}/caiwuxitong/app:$imageTag"
|
||||
$serverRegistryImage = "127.0.0.1:5000/caiwuxitong/app:$imageTag"
|
||||
|
||||
$beforeBytesOutput = @(& ssh -i $keyFullPath -o BatchMode=yes $sshTarget "du -sb /var/lib/docker-registry 2>/dev/null | cut -f1")
|
||||
$beforeBytes = [long](($beforeBytesOutput -join '').Trim())
|
||||
$uploadTimer = [Diagnostics.Stopwatch]::StartNew()
|
||||
Invoke-Native {
|
||||
& $regctlPath `
|
||||
--host "reg=127.0.0.1:${localRegistryPort},tls=disabled" `
|
||||
image import $registryImage $localTar
|
||||
} '增量镜像层上传失败。'
|
||||
$uploadTimer.Stop()
|
||||
$afterBytesOutput = @(& ssh -i $keyFullPath -o BatchMode=yes $sshTarget "du -sb /var/lib/docker-registry 2>/dev/null | cut -f1")
|
||||
$afterBytes = [long](($afterBytesOutput -join '').Trim())
|
||||
$incrementalMiB = [math]::Round([math]::Max(0, $afterBytes - $beforeBytes) / 1MB, 2)
|
||||
Write-Host "增量新增镜像层:$incrementalMiB MiB;传输耗时:$([math]::Round($uploadTimer.Elapsed.TotalSeconds, 1)) 秒"
|
||||
|
||||
Invoke-Native {
|
||||
ssh -i $keyFullPath -o BatchMode=yes $sshTarget "'$RemotePath/activate-incremental-release.sh' '$serverRegistryImage' '$imageTag' '$RemotePath' '$deploySha' '$migrationFlag' '$KeepImages'"
|
||||
} '服务器增量激活或健康检查失败。'
|
||||
}
|
||||
|
||||
Remove-OldProjectImages -CurrentImage $fullImage
|
||||
Write-Host "发布完成:$fullImage -> $AppUrl"
|
||||
@ -220,6 +348,13 @@ finally {
|
||||
Remove-Item -LiteralPath $tempRoot -Recurse -Force
|
||||
}
|
||||
|
||||
if ($sshTunnel) {
|
||||
if (-not $sshTunnel.HasExited) {
|
||||
$sshTunnel.Kill()
|
||||
}
|
||||
$sshTunnel.Dispose()
|
||||
}
|
||||
|
||||
if ($remoteArchive) {
|
||||
ssh -i $keyFullPath -o BatchMode=yes -o ConnectTimeout=5 $sshTarget "rm -f '$remoteArchive'" 2>$null | Out-Null
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user