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.
This commit is contained in:
108
scripts/build-offline-bundle.ps1
Normal file
108
scripts/build-offline-bundle.ps1
Normal file
@@ -0,0 +1,108 @@
|
||||
<#
|
||||
.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
|
||||
}
|
||||
Reference in New Issue
Block a user