<# .SYNOPSIS Docker Compose stacks management tool. .DESCRIPTION Manage all running Docker Compose stacks from one command. Auto-discovers stacks via Docker labels (no hardcoded paths). .LINK https://github.com/Faginux/docker-stacks #> function Get-Stacks { $stacks = @{} docker ps --format "{{.Names}}" | ForEach-Object { $project = docker inspect $_ --format '{{index .Config.Labels "com.docker.compose.project"}}' 2>$null $dir = docker inspect $_ --format '{{index .Config.Labels "com.docker.compose.project.working_dir"}}' 2>$null if ($project -and $dir -and -not $stacks.ContainsKey($project)) { $stacks[$project] = $dir } } return $stacks } function Invoke-OnStack { param([string]$Project, [string]$Path, [scriptblock]$Action) Write-Host "" Write-Host "=== $Project ($Path) ===" -ForegroundColor Cyan Push-Location $Path try { & $Action } finally { Pop-Location } } function Show-Help { Write-Host @" stacks - Docker Compose stacks management USAGE: stacks [stack_name] [flags] COMMANDS: list List all running stacks and their paths status Show health/state of all containers grouped by stack recreate [stack] Force-recreate all stacks or a specific one pull [stack] Pull latest images (no restart) update [stack] Pull + up -d (recreates if image changed) restart [stack] Restart containers (no recreate) stop [stack] Stop containers without removing them logs [-n N] Follow logs of a stack (default last 100 lines) config Show merged docker-compose configuration config --check Validate compose file (silent if OK) disk Show disk usage summary per stack logconfig Show logging config for all containers edit Open docker-compose.yml in editor (notepad) Override editor with `$env:STACKS_EDITOR help Show this help EXAMPLES: stacks list stacks status stacks recreate paperless stacks logs caddy -n 50 stacks config caddy --check stacks stop overseerr stacks disk "@ } # === Main === $command = $args[0] $stackName = $args[1] $flag = $args[2] $flagValue = $args[3] # Help / no args if (-not $command -or $command -in @("help", "-h", "--help")) { Show-Help exit 0 } $stacks = Get-Stacks if ($stacks.Count -eq 0) { Write-Host "No running Compose stacks found." -ForegroundColor Yellow exit 0 } # Stack name validation logic: # - Commands with optional stack filter (recreate/pull/update/restart/stop): validate if given # - Commands that require stack (logs/config/edit): validate internally # - Commands that ignore stack (list/status/disk/logconfig): no validation $cmdsOptionalStack = @("recreate", "pull", "update", "restart", "stop") $targetStacks = $stacks if ($stackName -and ($command -in $cmdsOptionalStack)) { if (-not $stacks.ContainsKey($stackName)) { Write-Host "Error: stack '$stackName' not found." -ForegroundColor Red Write-Host "Available: $($stacks.Keys -join ', ')" -ForegroundColor Yellow exit 1 } $targetStacks = @{ $stackName = $stacks[$stackName] } } switch ($command) { "list" { Write-Host "" Write-Host "Running stacks:" -ForegroundColor Green $stacks.GetEnumerator() | Sort-Object Name | ForEach-Object { "{0,-20} -> {1}" -f $_.Key, $_.Value } } "status" { $stacks.GetEnumerator() | Sort-Object Name | ForEach-Object { $project = $_.Key Write-Host "" Write-Host "=== $project ===" -ForegroundColor Cyan docker ps -a --format "{{.Names}}" | ForEach-Object { $name = $_ $p = docker inspect $name --format '{{index .Config.Labels "com.docker.compose.project"}}' 2>$null if ($p -eq $project) { $state = docker inspect $name --format '{{.State.Status}}' 2>$null $health = docker inspect $name --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' 2>$null " {0,-25} {1,-12} {2}" -f $name, $state, $health } } } } "recreate" { $targetStacks.GetEnumerator() | ForEach-Object { Invoke-OnStack -Project $_.Key -Path $_.Value -Action { docker compose up -d --force-recreate } } } "pull" { $targetStacks.GetEnumerator() | ForEach-Object { Invoke-OnStack -Project $_.Key -Path $_.Value -Action { docker compose pull } } } "update" { $targetStacks.GetEnumerator() | ForEach-Object { Invoke-OnStack -Project $_.Key -Path $_.Value -Action { docker compose pull docker compose up -d } } } "restart" { $targetStacks.GetEnumerator() | ForEach-Object { Invoke-OnStack -Project $_.Key -Path $_.Value -Action { docker compose restart } } } "stop" { $targetStacks.GetEnumerator() | ForEach-Object { Invoke-OnStack -Project $_.Key -Path $_.Value -Action { docker compose stop } } } "logs" { if (-not $stackName) { Write-Host "Error: 'logs' requires a stack name." -ForegroundColor Red Write-Host "Usage: stacks logs [-n N]" -ForegroundColor Yellow exit 1 } if (-not $stacks.ContainsKey($stackName)) { Write-Host "Error: stack '$stackName' not found." -ForegroundColor Red Write-Host "Available: $($stacks.Keys -join ', ')" -ForegroundColor Yellow exit 1 } $tailLines = 100 if ($flag -eq "-n" -and $flagValue) { $tailLines = $flagValue } Push-Location $stacks[$stackName] try { docker compose logs -f --tail=$tailLines } finally { Pop-Location } } "config" { if (-not $stackName) { Write-Host "Error: 'config' requires a stack name." -ForegroundColor Red Write-Host "Usage: stacks config [--check]" -ForegroundColor Yellow exit 1 } if (-not $stacks.ContainsKey($stackName)) { Write-Host "Error: stack '$stackName' not found." -ForegroundColor Red Write-Host "Available: $($stacks.Keys -join ', ')" -ForegroundColor Yellow exit 1 } Push-Location $stacks[$stackName] try { if ($flag -eq "--check") { docker compose config -q if ($LASTEXITCODE -eq 0) { Write-Host "Stack '$stackName' config is valid." -ForegroundColor Green } else { Write-Host "Stack '$stackName' config has errors." -ForegroundColor Red exit 1 } } else { docker compose config } } finally { Pop-Location } } "disk" { Write-Host "" Write-Host "=== System disk usage ===" -ForegroundColor Cyan docker system df Write-Host "" Write-Host "=== Volumes per stack ===" -ForegroundColor Cyan $stacks.GetEnumerator() | Sort-Object Name | ForEach-Object { $project = $_.Key $vols = @(docker volume ls --filter "label=com.docker.compose.project=$project" --format "{{.Name}}") if ($vols -and $vols[0]) { $volList = $vols -join ", " " {0,-20} {1}" -f $project, $volList } else { " {0,-20} (no labeled volumes)" -f $project } } Write-Host "" Write-Host "Tip: 'docker system df -v' shows per-volume/per-image sizes." -ForegroundColor DarkGray } "logconfig" { docker ps --format "{{.Names}}" | ForEach-Object { $config = docker inspect $_ --format '{{.HostConfig.LogConfig.Type}} {{.HostConfig.LogConfig.Config}}' Write-Host "$_ : $config" } } "edit" { if (-not $stackName) { Write-Host "Error: 'edit' requires a stack name." -ForegroundColor Red Write-Host "Available: $($stacks.Keys -join ', ')" -ForegroundColor Yellow exit 1 } if (-not $stacks.ContainsKey($stackName)) { Write-Host "Error: stack '$stackName' not found." -ForegroundColor Red Write-Host "Available: $($stacks.Keys -join ', ')" -ForegroundColor Yellow exit 1 } $composeFile = Join-Path $stacks[$stackName] "docker-compose.yml" if (-not (Test-Path $composeFile)) { $composeFile = Join-Path $stacks[$stackName] "docker-compose.yaml" if (-not (Test-Path $composeFile)) { Write-Host "Error: docker-compose.y(a)ml not found in $($stacks[$stackName])" -ForegroundColor Red exit 1 } } $editor = if ($env:STACKS_EDITOR) { $env:STACKS_EDITOR } else { "notepad" } Write-Host "Opening $composeFile with $editor..." -ForegroundColor Cyan & $editor $composeFile } default { Write-Host "Unknown command: $command" -ForegroundColor Red Show-Help exit 1 } }