# Boot Persistence on Windows Process Compose has no built-in `service install` (unlike portless). On Windows, register a Task Scheduler entry. ## Key Constraints 1. Task Scheduler runs with a **minimal PATH** — Python, uv, Git tools, custom binaries won't be found unless we set PATH explicitly 2. Tasks running at boot-before-logon need **LogonType S4U** (no stored password, no interactive logon) 3. Hidden window style avoids console flash on login ## Two-File Pattern Use a wrapper script that sets the environment, then have Task Scheduler launch the wrapper. Keeps task definition simple and lets you tweak env without re-registering. ### File 1 — `boot-start.ps1` (wrapper) ```powershell <# .SYNOPSIS Boot-time launcher for Process Compose. Sets PATH and launches headless. #> [CmdletBinding()] param() $ErrorActionPreference = 'Continue' $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $root = (Resolve-Path (Join-Path $scriptDir '..')).Path $pcExe = Join-Path $root 'bin\process-compose.exe' $pcYaml = Join-Path $root 'process-compose.yaml' $logFile = Join-Path $root 'logs\process-compose.log' $bootLog = Join-Path $root 'logs\boot-start.log' New-Item -ItemType Directory -Force -Path (Join-Path $root 'logs') | Out-Null "[$(Get-Date -Format 'yyyy-MM-ddTHH:mm:ssK')] boot-start invoked. User: $env:USERNAME" | Out-File -FilePath $bootLog -Append # Build PATH explicitly. Tune for your machine. $pathParts = @( "$root\bin" # PC + any committed binaries "C:\Program Files\Git\usr\bin" # openssl, bash, coreutils "C:\Program Files\Git\bin" # git "C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python313" # python, pythonw "C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python313\Scripts" # uv, pip, etc. "C:\Program Files (x86)\cloudflared" # optional: cloudflared "C:\Windows\System32" "C:\Windows" $env:PATH ) $env:PATH = ($pathParts -join ';') # Optional: load secrets from gitignored .env (e.g. API keys) $envFile = Join-Path $root '.env' if (Test-Path $envFile) { Get-Content $envFile | ForEach-Object { if ($_ -match '^\s*([A-Z_]+)\s*=\s*(.+?)\s*$') { [Environment]::SetEnvironmentVariable($matches[1], $matches[2], 'Process') } } } # Ensure incompatible env vars are unset (example: OAuth-only services that # refuse to start with stale API keys) # [Environment]::SetEnvironmentVariable('SOME_API_KEY', $null, 'Process') "[$(Get-Date -Format 'yyyy-MM-ddTHH:mm:ssK')] Starting process-compose..." | Out-File -FilePath $bootLog -Append # -p 8888 API port (pick something free, avoid 8080 if you have other tools there) # -t=false no TUI (headless daemon mode) # -L PC's own log file & $pcExe -p 8888 -t=false -L $logFile up -f $pcYaml "[$(Get-Date -Format 'yyyy-MM-ddTHH:mm:ssK')] process-compose exited code $LASTEXITCODE" | Out-File -FilePath $bootLog -Append ``` ### File 2 — `boot-task-install.ps1` (registers the task) ```powershell [CmdletBinding()] param() $ErrorActionPreference = 'Stop' # Must be admin to create scheduled tasks $currentUser = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = New-Object Security.Principal.WindowsPrincipal($currentUser) if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { throw "Run as Administrator." } $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $root = (Resolve-Path (Join-Path $scriptDir '..')).Path $bootScript = Join-Path $scriptDir 'boot-start.ps1' $taskName = "ProcessCompose-MyStack" # rename per project # Idempotent: remove existing if present Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue | Unregister-ScheduledTask -Confirm:$false $action = New-ScheduledTaskAction ` -Execute "powershell.exe" ` -Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$bootScript`"" ` -WorkingDirectory $root $trigger = New-ScheduledTaskTrigger -AtStartup $settings = New-ScheduledTaskSettingsSet ` -ExecutionTimeLimit (New-TimeSpan -Seconds 0) ` -AllowStartIfOnBatteries ` -DontStopIfGoingOnBatteries ` -RestartCount 3 ` -RestartInterval (New-TimeSpan -Minutes 1) # S4U: run at boot as user without interactive logon or stored password $taskPrincipal = New-ScheduledTaskPrincipal ` -UserId $env:USERNAME ` -LogonType S4U ` -RunLevel Highest Register-ScheduledTask -TaskName $taskName ` -Action $action -Trigger $trigger -Settings $settings -Principal $taskPrincipal ` -Description "Starts Process Compose at boot." ``` ## LogonType Trade-offs | LogonType | Runs at boot before logon? | Needs password? | Capability | |---|---|---|---| | `Interactive` | No — waits for user logon | No | Full user context (UI, network shares) | | `S4U` | Yes | No | User context but no UI, no network shares | | `Password` | Yes | Yes (stored encrypted) | Full user context | | `ServiceAccount` | Yes (as Local System / Network Service) | No | Limited to service account perms — typically can't read user files | For Process Compose managing user-scoped dev services, **S4U** is usually the right choice: services run as the user (can read `C:\Users\\...`) without requiring an interactive logon. ## Verify After Registration ```powershell # Check task exists Get-ScheduledTask -TaskName "ProcessCompose-MyStack" | Format-List TaskName, State, Triggers, Principal # Manually run the task to test before reboot Start-ScheduledTask -TaskName "ProcessCompose-MyStack" # Wait, then check PC is up Start-Sleep -Seconds 10 process-compose -p 8888 process list ``` ## Troubleshooting Boot Failures After a reboot, if services don't come up: 1. **Check the boot log:** `/logs/boot-start.log` — confirm the wrapper actually ran 2. **Check PC's log:** `/logs/process-compose.log` — confirm PC started and look for process-spawn errors 3. **Check Task Scheduler history:** Right-click the task → History tab. Look for failure reasons. 4. **Reproduce manually:** open elevated PS, run `.\scripts\boot-start.ps1` and watch what happens. Common failures: - PATH missing a tool → add to `pathParts` array - Working dir not absolute → ensure all paths in `process-compose.yaml` are absolute - Secrets not loaded → `.env` file not in expected location - Port collision (PC API port 8888 occupied) → check `netstat -ano | findstr :8888` ## Pair with portless service install portless has its own boot task. The two are independent — register both: ```powershell portless service install # registers portless's task .\scripts\boot-task-install.ps1 # registers PC's task # Verify both Get-ScheduledTask | Where-Object { $_.TaskName -like "*ortless*" -or $_.TaskName -like "*ompose*" } ``` ## Uninstall ```powershell # In the same script: Get-ScheduledTask -TaskName "ProcessCompose-MyStack" -ErrorAction SilentlyContinue | Unregister-ScheduledTask -Confirm:$false portless service uninstall ```