#requires -Version 5.1 <# .SYNOPSIS Set up, start and stop the entire TravelHub stack on Docker. A new developer needs only Docker Desktop -- no local Go, Node, or PostgreSQL. .DESCRIPTION Wraps docker compose for the three services (PostgreSQL + Go API + Angular via nginx). Database credentials are read from backend/.env, so there is one source of truth and no secret is duplicated. URLs once up: App -> http://localhost:4200 (nginx serves the SPA and proxies /api) API -> http://localhost:8080 Demo sign-in: any seeded account, password demo1234 (e.g. agent@skyline.test, agency "skyline"). .PARAMETER Action setup Build images and bring everything up, waiting until healthy. The database is created and seeded on this first run. One command to onboard. start Bring the stack up (images already built). stop Stop and remove the containers; the database volume is kept. restart stop then start. reset Stop and DELETE the database volume, then start fresh (re-seeds). rebuild Rebuild images (use after pulling code changes) and start. status Show container state. logs Tail logs (-Follow to stream; -Service to pick one). .PARAMETER Service logs: which service (postgres | api | web). Default: all. .PARAMETER Follow logs: stream instead of printing the tail once. .EXAMPLE .\docker.ps1 setup .EXAMPLE .\docker.ps1 logs -Service api -Follow .EXAMPLE .\docker.ps1 reset # wipe data and re-seed with fresh flight dates #> [CmdletBinding()] param( [Parameter(Position = 0)] [ValidateSet('setup', 'start', 'stop', 'restart', 'reset', 'rebuild', 'status', 'logs', 'help')] [string]$Action = 'help', [ValidateSet('postgres', 'api', 'web')] [string]$Service, [switch]$Follow ) $ErrorActionPreference = 'Stop' $Root = $PSScriptRoot function Write-Step($m) { Write-Host "==> $m" -ForegroundColor Cyan } function Write-Ok($m) { Write-Host " + $m" -ForegroundColor Green } function Write-Warn2($m){ Write-Host " ! $m" -ForegroundColor Yellow } # --- docker availability --------------------------------------------------- function Require-Docker { if (-not (Get-Command docker -ErrorAction SilentlyContinue)) { throw "Docker not found. Install Docker Desktop and ensure it is running: https://www.docker.com/products/docker-desktop/" } # `docker info` fails fast if the engine/daemon is not running. docker info *> $null if ($LASTEXITCODE -ne 0) { throw 'Docker is installed but the engine is not running. Start Docker Desktop and retry.' } # Compose v2 (the `docker compose` subcommand) is required. docker compose version *> $null if ($LASTEXITCODE -ne 0) { throw "This script needs Docker Compose v2 (the 'docker compose' subcommand). Update Docker Desktop." } } # --- credentials from backend/.env ----------------------------------------- # Load DB_USER / DB_PASSWORD / DB_NAME from backend/.env into this process so # docker compose can substitute them. Nothing is printed or copied to disk. function Import-DbEnv { $envFile = Join-Path $Root 'backend\.env' if (-not (Test-Path $envFile)) { throw "backend\.env not found. Copy backend\.env.example to backend\.env and set DB_PASSWORD first." } foreach ($line in Get-Content $envFile) { $t = $line.Trim() if ($t -eq '' -or $t.StartsWith('#')) { continue } $i = $t.IndexOf('=') if ($i -lt 1) { continue } $key = $t.Substring(0, $i).Trim() $val = $t.Substring($i + 1).Trim().Trim('"').Trim("'") if ($key -in @('DB_USER', 'DB_PASSWORD', 'DB_NAME')) { Set-Item -Path "Env:$key" -Value $val } } if (-not $env:DB_PASSWORD) { throw 'DB_PASSWORD is not set in backend\.env.' } if (-not $env:DB_USER) { $env:DB_USER = 'postgres' } if (-not $env:DB_NAME) { $env:DB_NAME = 'travelhub' } } # --- compose helpers ------------------------------------------------------- function Compose { param([Parameter(ValueFromRemainingArguments = $true)] $ComposeArgs) Push-Location $Root try { & docker compose @ComposeArgs } finally { Pop-Location } } function Wait-ForUrl($url, $timeoutSec) { $deadline = (Get-Date).AddSeconds($timeoutSec) while ((Get-Date) -lt $deadline) { try { Invoke-WebRequest $url -UseBasicParsing -TimeoutSec 3 | Out-Null; return $true } catch { Start-Sleep -Seconds 2 } } return $false } function Show-Ready { Write-Host '' if (Wait-ForUrl 'http://localhost:8080/api/health' 90) { Write-Ok 'API healthy -> http://localhost:8080' } else { Write-Warn2 'API not healthy yet -- check: .\docker.ps1 logs -Service api' } if (Wait-ForUrl 'http://localhost:4200/' 60) { Write-Ok 'App ready -> http://localhost:4200' } else { Write-Warn2 'Web not ready yet -- check: .\docker.ps1 logs -Service web' } Write-Host '' Write-Host 'Sign in with any demo account, password demo1234 (e.g. agent@skyline.test / skyline).' Write-Host 'Note: stop any native .\app.ps1 first -- it uses the same ports.' -ForegroundColor DarkGray } # --- actions --------------------------------------------------------------- switch ($Action) { 'setup' { Require-Docker; Import-DbEnv Write-Step 'Building images (first run downloads base images)' Compose build Write-Step 'Starting stack and seeding the database' Compose up -d Show-Ready Write-Ok 'Setup complete.' } 'start' { Require-Docker; Import-DbEnv Write-Step 'Starting stack' Compose up -d Show-Ready } 'stop' { Require-Docker; Import-DbEnv Write-Step 'Stopping stack (database volume kept)' Compose down Write-Ok 'Stopped.' } 'restart' { Require-Docker; Import-DbEnv Compose down Compose up -d Show-Ready } 'reset' { Require-Docker; Import-DbEnv Write-Warn2 'This DELETES the database volume. All data will be lost.' Write-Step 'Removing containers and the data volume' Compose down -v Write-Step 'Starting fresh (re-seeds with current flight dates)' Compose up -d Show-Ready Write-Ok 'Reset complete.' } 'rebuild' { Require-Docker; Import-DbEnv Write-Step 'Rebuilding images' Compose build --no-cache Compose up -d Show-Ready } 'status' { Require-Docker; Import-DbEnv Compose ps } 'logs' { Require-Docker; Import-DbEnv $logArgs = @('logs') if ($Follow) { $logArgs += '-f' } else { $logArgs += @('--tail', '80') } if ($Service) { $logArgs += $Service } Compose @logArgs } default { Get-Help $PSCommandPath -Detailed } }