184 lines
7.3 KiB
PowerShell
184 lines
7.3 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Deploy the Lean 101 Clients app to a Digital Ocean droplet over SSH.
|
|
|
|
.DESCRIPTION
|
|
Tars the local source tree, uploads it to the droplet, and runs
|
|
docker compose up --build. No git required on the server.
|
|
|
|
The same script handles first-time setup and subsequent updates.
|
|
|
|
.PARAMETER RemoteHost
|
|
Hostname or IP of the Digital Ocean droplet. Required.
|
|
|
|
.PARAMETER RemoteUser
|
|
SSH user. Defaults to 'root'.
|
|
|
|
.PARAMETER RemotePath
|
|
Absolute path on the droplet. Defaults to '/srv/lean101-clients'.
|
|
|
|
.PARAMETER EnvFile
|
|
Local path to the production env file. Defaults to '.env.production'.
|
|
|
|
.PARAMETER SshKey
|
|
Optional path to an SSH private key.
|
|
|
|
.PARAMETER ComposeFile
|
|
Compose file name on the remote host. Defaults to 'docker-compose.production.yml'.
|
|
|
|
.PARAMETER Seed
|
|
Run 'python -m app.seed' inside the backend container after the stack is up.
|
|
|
|
.PARAMETER Logs
|
|
Tail logs for ~60 lines after deploy to verify the stack came up.
|
|
|
|
.PARAMETER SkipBuild
|
|
Pass --no-build to docker compose (use when only env changed).
|
|
|
|
.EXAMPLE
|
|
./deploy/Deploy.ps1 -RemoteHost 209.38.24.231
|
|
|
|
.EXAMPLE
|
|
./deploy/Deploy.ps1 -RemoteHost 209.38.24.231 -Seed -Logs
|
|
#>
|
|
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory = $true)] [string] $RemoteHost,
|
|
[string] $RemoteUser = "root",
|
|
[string] $RemotePath = "/srv/lean101-clients",
|
|
[string] $EnvFile = ".env.production",
|
|
[string] $SshKey,
|
|
[string] $ComposeFile = "docker-compose.production.yml",
|
|
[switch] $Seed,
|
|
[switch] $Logs,
|
|
[switch] $SkipBuild
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
Set-StrictMode -Version Latest
|
|
|
|
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
function Write-Step($msg) { Write-Host "==> $msg" -ForegroundColor Cyan }
|
|
function Write-Warn($msg) { Write-Host "!! $msg" -ForegroundColor Yellow }
|
|
|
|
function Get-RepoRoot {
|
|
$dir = Split-Path -Parent $PSScriptRoot
|
|
if (-not $dir) { $dir = (Get-Location).Path }
|
|
return $dir
|
|
}
|
|
|
|
$RepoRoot = Get-RepoRoot
|
|
$SshTarget = "$RemoteUser@$RemoteHost"
|
|
$SshOpts = @("-o", "StrictHostKeyChecking=accept-new", "-o", "BatchMode=no")
|
|
if ($SshKey) { $SshOpts += @("-i", $SshKey) }
|
|
|
|
function Invoke-Ssh([string] $cmd) {
|
|
& ssh @SshOpts $SshTarget $cmd
|
|
if ($LASTEXITCODE -ne 0) { throw "Remote command failed (exit $LASTEXITCODE): $cmd" }
|
|
}
|
|
|
|
function Invoke-Scp([string] $local, [string] $remote) {
|
|
& scp @SshOpts $local "${SshTarget}:${remote}"
|
|
if ($LASTEXITCODE -ne 0) { throw "scp failed: $local -> $remote" }
|
|
}
|
|
|
|
# ── Resolve paths ─────────────────────────────────────────────────────────────
|
|
Push-Location $RepoRoot
|
|
try {
|
|
$EnvPath = if ([System.IO.Path]::IsPathRooted($EnvFile)) { $EnvFile } else { Join-Path $RepoRoot $EnvFile }
|
|
if (-not (Test-Path $EnvPath)) {
|
|
throw "Env file not found at '$EnvPath'. Copy .env.production.example and fill in secrets."
|
|
}
|
|
|
|
# ── Connectivity check ──────────────────────────────────────────────────────
|
|
Write-Step "Checking SSH connectivity to $SshTarget"
|
|
Invoke-Ssh "echo connected as `$(whoami) on `$(hostname)"
|
|
|
|
# ── Package source files ────────────────────────────────────────────────────
|
|
Write-Step "Packaging source files (excluding node_modules, caches, etc.)"
|
|
|
|
$TarFile = Join-Path $env:TEMP "lean101-deploy-$(Get-Date -Format 'yyyyMMdd-HHmmss').tar.gz"
|
|
|
|
$excludes = @(
|
|
"--exclude=./node_modules",
|
|
"--exclude=./frontend/node_modules",
|
|
"--exclude=./frontend/.svelte-kit",
|
|
"--exclude=./frontend/build",
|
|
"--exclude=./.git",
|
|
"--exclude=./__pycache__",
|
|
"--exclude=./backend/__pycache__",
|
|
"--exclude=./backend/app/__pycache__",
|
|
"--exclude=./**/__pycache__",
|
|
"--exclude=./*.pyc",
|
|
"--exclude=./.env",
|
|
"--exclude=./.env.production",
|
|
"--exclude=./.env.alpha",
|
|
"--exclude=./data_entry_app.db",
|
|
"--exclude=./*.db"
|
|
)
|
|
|
|
& tar -czf $TarFile @excludes -C $RepoRoot .
|
|
if ($LASTEXITCODE -ne 0) { throw "tar failed" }
|
|
|
|
$TarSize = [math]::Round((Get-Item $TarFile).Length / 1MB, 1)
|
|
Write-Host " Archive: $TarFile ($TarSize MB)"
|
|
|
|
# ── Upload env file ─────────────────────────────────────────────────────────
|
|
Write-Step "Uploading env file"
|
|
Invoke-Scp $EnvPath "$RemotePath/.env.production"
|
|
Invoke-Ssh "chmod 600 '$RemotePath/.env.production'"
|
|
|
|
# ── Upload and extract source ────────────────────────────────────────────────
|
|
Write-Step "Uploading source archive"
|
|
Invoke-Scp $TarFile "/tmp/lean101-deploy.tar.gz"
|
|
Remove-Item $TarFile -Force
|
|
|
|
Write-Step "Extracting on server"
|
|
Invoke-Ssh "mkdir -p '$RemotePath' && tar -xzf /tmp/lean101-deploy.tar.gz -C '$RemotePath' && rm /tmp/lean101-deploy.tar.gz"
|
|
|
|
# ── Docker compose up ───────────────────────────────────────────────────────
|
|
$ComposeArgs = "--env-file .env.production -f $ComposeFile"
|
|
$BuildFlag = if ($SkipBuild) { "--no-build" } else { "--build" }
|
|
|
|
Write-Step "Bringing stack up (build=$(-not $SkipBuild))"
|
|
Invoke-Ssh "cd '$RemotePath' && docker compose $ComposeArgs up -d $BuildFlag --remove-orphans"
|
|
|
|
# ── Health check ────────────────────────────────────────────────────────────
|
|
Write-Step "Waiting for backend health check"
|
|
$healthScript = @"
|
|
set -e
|
|
cd '$RemotePath'
|
|
for i in `$(seq 1 30); do
|
|
status=`$(docker inspect --format='{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' lean101-clients-backend 2>/dev/null || echo missing)
|
|
case "`$status" in
|
|
healthy|running) echo "backend is `$status"; exit 0 ;;
|
|
*) printf '.'; sleep 4 ;;
|
|
esac
|
|
done
|
|
echo; echo 'backend did not become healthy in time' >&2; exit 1
|
|
"@
|
|
Invoke-Ssh $healthScript
|
|
|
|
# ── Optional seed ───────────────────────────────────────────────────────────
|
|
if ($Seed) {
|
|
Write-Step "Seeding reference data"
|
|
Invoke-Ssh "cd '$RemotePath' && docker compose $ComposeArgs exec -T backend python -m app.seed"
|
|
}
|
|
|
|
# ── Final status ────────────────────────────────────────────────────────────
|
|
Write-Step "Stack status"
|
|
Invoke-Ssh "cd '$RemotePath' && docker compose $ComposeArgs ps"
|
|
|
|
if ($Logs) {
|
|
Write-Step "Recent logs (last 60 lines)"
|
|
Invoke-Ssh "cd '$RemotePath' && docker compose $ComposeArgs logs --tail=60"
|
|
}
|
|
|
|
Write-Host ""
|
|
Write-Host "Deployment complete -> https://clients.lean-101.com.au" -ForegroundColor Green
|
|
}
|
|
finally {
|
|
Pop-Location
|
|
}
|