Files
shopdb-flask/scripts/build-offline-bundle.ps1
cproudlock 6534590fca
Some checks failed
CI / backend (push) Successful in 1m51s
CI / naming (push) Successful in 1s
CI / frontend (push) Successful in 9s
CI / migrations-mysql (push) Failing after 9s
docker: air-gapped deploy kit (image bundle + offline compose + runbook)
Air-gapped sites cannot pip install / npm ci / docker pull, so a build-at-site
compose (build: .) fails and reports 'service api is not running'. Add a
build-once-ship-image path:

- scripts/build-offline-bundle.ps1: on a connected box, build shopdb-flask +
  pull mysql:8.0, docker save both into one gzipped tarball with a sha256.
- docker-compose.airgap.yml: runs pre-loaded images (image:, never build:),
  drops the ./plugins bind mount (which would mask the image's baked-in plugins
  with an empty host dir and load zero plugins at an image-only site), and adds
  a one-shot migrate service (db upgrade + plugin upgrade-all + seed) that api
  waits on via service_completed_successfully, so 'up -d' brings a working site.
- docs/DEPLOY-AIRGAP.md: full runbook (build, transfer+verify, load+run, admin,
  verify, upgrade, troubleshooting), incl the Zscaler in-build cert caveat.
- .env.example: IMAGE_TAG for the air-gap compose to pin the loaded image tag.
2026-07-23 14:15:34 -04:00

109 lines
3.9 KiB
PowerShell

<#
.SYNOPSIS
Build the shopdb-flask image on a CONNECTED box and bundle it (plus the MySQL
image) into one tarball for transfer to an air-gapped site.
.DESCRIPTION
Air-gapped sites cannot pip install / npm ci / docker pull. This builds the
fully self-contained application image where connectivity exists, pulls the
MySQL image the stack needs, and `docker save`s both into a single gzipped
tarball with a SHA-256 checksum. Carry the .tar.gz to the site and follow
docs/DEPLOY-AIRGAP.md (docker load + docker-compose.airgap.yml).
Run from the repo root. Requires Docker Desktop. The connected build box needs
clean access to Docker Hub + PyPI + npm. If it is behind GE Zscaler and the
build fails on pip/npm SSL (CERTIFICATE_VERIFY_FAILED), build from a box with
clean internet, or trust the corp root CA in the build - see
docs/DEPLOY-AIRGAP.md "Building behind Zscaler".
.PARAMETER Version
Image tag for shopdb-flask (default 0.7.0). This is the IMAGE_TAG the site
sets in its .env so docker-compose.airgap.yml runs the matching image.
.PARAMETER MysqlImage
MySQL image the stack runs (default mysql:8.0). Must match db.image in
docker-compose.airgap.yml.
.PARAMETER OutDir
Where to write the tarball + checksum (default the repo root).
.PARAMETER SkipBuild
Reuse an already-built shopdb-flask:<Version> image; only pull + save.
.EXAMPLE
pwsh scripts/build-offline-bundle.ps1 -Version 0.7.0
#>
[CmdletBinding()]
param(
[string]$Version = '0.7.0',
[string]$MysqlImage = 'mysql:8.0',
[string]$OutDir = '.',
[switch]$SkipBuild
)
$ErrorActionPreference = 'Stop'
function Assert-LastExit([string]$What) {
if ($LASTEXITCODE -ne 0) { throw "$What failed (exit $LASTEXITCODE)" }
}
# Fail early if docker is missing rather than midway through a long build.
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
throw 'docker not found on PATH. Install Docker Desktop and retry.'
}
$appImage = "shopdb-flask:$Version"
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
Push-Location $repoRoot
try {
if (-not $SkipBuild) {
Write-Host "==> Building $appImage" -ForegroundColor Cyan
docker build -t $appImage .
Assert-LastExit 'docker build'
} else {
Write-Host "==> Skipping build; reusing $appImage" -ForegroundColor Yellow
}
Write-Host "==> Pulling $MysqlImage" -ForegroundColor Cyan
docker pull $MysqlImage
Assert-LastExit 'docker pull'
if (-not (Test-Path $OutDir)) { New-Item -ItemType Directory -Path $OutDir | Out-Null }
$stem = "shopdb-stack-$Version"
$tar = Join-Path $OutDir "$stem.tar"
$gz = Join-Path $OutDir "$stem.tar.gz"
Write-Host "==> Saving $appImage + $MysqlImage" -ForegroundColor Cyan
docker save -o $tar $appImage $MysqlImage
Assert-LastExit 'docker save'
Write-Host "==> Compressing to $gz" -ForegroundColor Cyan
$inStream = [System.IO.File]::OpenRead($tar)
$outStream = [System.IO.File]::Create($gz)
$gzip = New-Object System.IO.Compression.GzipStream($outStream, [System.IO.Compression.CompressionMode]::Compress)
try {
$inStream.CopyTo($gzip)
} finally {
$gzip.Dispose(); $outStream.Dispose(); $inStream.Dispose()
}
Remove-Item $tar
$hash = (Get-FileHash $gz -Algorithm SHA256).Hash.ToLower()
$sumFile = "$gz.sha256"
"$hash $stem.tar.gz" | Set-Content -Path $sumFile -NoNewline
$sizeMb = [math]::Round((Get-Item $gz).Length / 1MB, 1)
Write-Host ''
Write-Host "Bundle ready: $gz ($sizeMb MB)" -ForegroundColor Green
Write-Host "SHA-256: $hash"
Write-Host "Checksum: $sumFile"
Write-Host ''
Write-Host 'At the air-gapped site (verify the checksum first):'
Write-Host " docker load -i $stem.tar.gz"
Write-Host " # set IMAGE_TAG=$Version in .env, then:"
Write-Host ' docker compose -f docker-compose.airgap.yml up -d'
}
finally {
Pop-Location
}