# 构建 api / player / admin 生产镜像并导出为 tar # 用法(在项目根目录或本目录执行均可): # .\docs\docker\build-and-export-images.ps1 # .\docs\docker\build-and-export-images.ps1 -Tag v1.2.3 # .\build-and-export-images.ps1 -UseCache # .\build-and-export-images.ps1 -ExportOnly param( [string]$Tag = $env:IMAGE_TAG, [switch]$UseCache, [switch]$ExportOnly, [string]$Output = "" ) $ErrorActionPreference = "Stop" $Root = (Resolve-Path (Join-Path $PSScriptRoot "../..")).Path Set-Location $Root $ComposeFile = "docker-compose.prod.yml" $EnvFile = ".env.docker" $Services = @("api", "player", "admin") function Get-DefaultTag { try { $short = (& git rev-parse --short HEAD 2>$null).Trim() if ($short) { return $short } } catch {} return Get-Date -Format "yyyyMMdd-HHmmss" } function Assert-ImageTag { param([string]$Value) if ($Value -notmatch '^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$') { throw "镜像 tag 不合法: $Value" } } if (-not (Test-Path $ComposeFile)) { throw "未找到 $ComposeFile(当前目录: $Root)" } if (-not (Test-Path $EnvFile)) { if (Test-Path ".env.docker.example") { Write-Warning "未找到 .env.docker,使用 .env.docker.example(生产请复制并修改密钥)" $EnvFile = ".env.docker.example" } else { throw "未找到 .env.docker 或 .env.docker.example" } } if ([string]::IsNullOrWhiteSpace($Tag)) { $Tag = Get-DefaultTag } Assert-ImageTag $Tag if ([string]::IsNullOrWhiteSpace($Output)) { $Output = "thebet365-images-$Tag.tar" } if (-not $ExportOnly) { Write-Host "==> 构建镜像: $($Services -join ', ') (tag: $Tag)" $buildArgs = @( "compose", "-f", $ComposeFile, "--env-file", $EnvFile, "build" ) if (-not $UseCache) { $buildArgs += "--no-cache" } $buildArgs += $Services $oldImageTag = $env:IMAGE_TAG try { $env:IMAGE_TAG = $Tag & docker @buildArgs if ($LASTEXITCODE -ne 0) { throw "docker build 失败,退出码 $LASTEXITCODE" } } finally { $env:IMAGE_TAG = $oldImageTag } } $OutputPath = if ([System.IO.Path]::IsPathRooted($Output)) { $Output } else { Join-Path $Root $Output } Write-Host "==> 导出镜像 -> $OutputPath" & docker save ` "thebet365-api:${Tag}" ` "thebet365-player:${Tag}" ` "thebet365-admin:${Tag}" ` -o $OutputPath if ($LASTEXITCODE -ne 0) { throw "docker save 失败,退出码 $LASTEXITCODE" } $manifestPath = if ($OutputPath.EndsWith(".tar")) { $OutputPath.Substring(0, $OutputPath.Length - 4) + ".manifest.txt" } else { "$OutputPath.manifest.txt" } $gitCommit = "unknown" $gitDirty = "unknown" try { $gitCommit = (& git rev-parse HEAD 2>$null).Trim() & git diff --quiet $diffExit = $LASTEXITCODE & git diff --cached --quiet $cachedExit = $LASTEXITCODE $gitDirty = if ($diffExit -eq 0 -and $cachedExit -eq 0) { "false" } else { "true" } } catch {} $tarHash = (Get-FileHash -Algorithm SHA256 $OutputPath).Hash.ToLowerInvariant() $apiImageId = (& docker image inspect "thebet365-api:${Tag}" --format "{{.Id}}").Trim() $playerImageId = (& docker image inspect "thebet365-player:${Tag}" --format "{{.Id}}").Trim() $adminImageId = (& docker image inspect "thebet365-admin:${Tag}" --format "{{.Id}}").Trim() @( "tag=$Tag" "built_at=$((Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ"))" "git_commit=$gitCommit" "git_dirty=$gitDirty" "tar=$([System.IO.Path]::GetFileName($OutputPath))" "tar_sha256=$tarHash" "api_image=thebet365-api:${Tag}" "api_image_id=$apiImageId" "player_image=thebet365-player:${Tag}" "player_image_id=$playerImageId" "admin_image=thebet365-admin:${Tag}" "admin_image_id=$adminImageId" ) | Set-Content -Encoding UTF8 $manifestPath $sizeMb = [math]::Round((Get-Item $OutputPath).Length / 1MB, 2) Write-Host "完成: $OutputPath (${sizeMb} MB)" Write-Host "Manifest: $manifestPath" Write-Host @" 服务器首次部署: ./scripts/deploy-first.sh --images $([System.IO.Path]::GetFileName($OutputPath)) --tag $Tag 服务器后续更新: ./scripts/deploy-update.sh --images $([System.IO.Path]::GetFileName($OutputPath)) --tag $Tag "@