run.ps1
This page is part of the documentation for Orleans.Lattice 9.9.0 (release line 9.9), built 2026-10-04. It is also published as markdown, with every table and list, at run-ps1.md, and llms.txt lists every page.Part of MultiSiteManufacturing source.
<#
.SYNOPSIS
Launches the Multi-Site Manufacturing sample (M14: Docker Compose + Traefik).
.DESCRIPTION
The sample runs as two independent Orleans clusters ("us" +
"eu"), each with two silos and its own Azurite, all inside
Docker Compose. A per-cluster Traefik reverse proxy provides sticky-
session load balancing across the cluster's two silos. Three host
ports are published:
http://localhost:5001 traefik-us -> silo-us-a | silo-us-b
http://localhost:5002 traefik-eu -> silo-eu-a | silo-eu-b
http://localhost:3000 grafana -> observability dashboards
Individual silo HTTP ports (:8080), Orleans silo (:11111) and gateway
(:30000) ports, and the per-cluster Azurite endpoints live on internal
Compose networks only. The shared backup Azurite account is reachable from
both clusters, and Prometheus is multi-homed only so it can scrape both.
See docker-compose.yml for the topology and its rationale.
.PARAMETER Down
Stop and remove all containers, networks, and named volumes.
.PARAMETER Clean
Wipe any pre-existing containers, networks, and named volumes before
starting the stack. Use this when you want a fresh run with no seeded
state carried over from a previous ./run.ps1 invocation. Ignored when
combined with -Down or -Logs.
.PARAMETER Logs
Tail logs from all silos (follow mode). Ctrl+C detaches without
stopping the containers.
.PARAMETER NoBuild
Skip "docker compose build" - reuse the cached msmfg-host:dev image.
.PARAMETER Service
Restrict -Logs to a single compose service, e.g. silo-us-a.
.PARAMETER Username
Enable state-API authentication for the Orleans.Lattice.Explorer (issue
#886). When supplied together with -Password, the stack comes up with the
read-only state API requiring a Basic credential: the salted PBKDF2 hash is
generated by tools/New-LatticeStateCredential.ps1 and delivered to every silo
container as LATTICE_STATE_USER_<Username> via a git-ignored .env file. The
plaintext password never reaches a container env, a command line, or the
compose file. The web explorer does not seed a browser sign-in from these
values by default; launch it, then enter the matching credentials in the
sign-in dialog. Omit both to run anonymously (the default, unchanged
behaviour).
.PARAMETER Password
The plaintext password paired with -Username. Validated against the helper's
policy (minimum 8 characters with an uppercase letter, a lowercase letter, and
a digit). Read in-process and hashed; never written anywhere in plaintext.
.PARAMETER Backup
Enable the backup / restore subsystem so the Orleans.Lattice.Explorer backup
UI can drive capture and restore against this stack (issue #1131). Sets
LATTICE_BACKUP_ENABLED=true in the git-ignored .env; every silo then registers
the shared external backup sink, the control API, and its gRPC binding. The
binding runs with authorization turned OFF - this is a demo-grade, insecure
posture (anyone who can reach the cluster endpoint can back up or restore),
matching how the state API runs anonymously by default. Omit to leave the
backup subsystem out entirely (the default, unchanged behaviour). Independent
of -Username/-Password: the two toggles can be combined or used separately.
.EXAMPLE
./run.ps1
Build the image (if needed), start all services, wait until the two
Traefik entrypoints accept TCP, then print the two cluster URLs.
.EXAMPLE
./run.ps1 -Logs
Tail logs from every silo until Ctrl+C.
.EXAMPLE
./run.ps1 -Username alice -Password 'Sup3rSecret'
Start the stack with state-API authentication enabled. Then browse it:
./run-explorer.ps1 -Username alice -Password 'Sup3rSecret'
.EXAMPLE
./run.ps1 -Backup
Start the stack with the backup / restore subsystem enabled so the explorer
backup UI can capture and restore. Demo-grade: the backup gRPC binding runs
with authorization off.
.EXAMPLE
./run.ps1 -Down
Tear everything down including the Azurite volumes (seeded state
is deleted - next ./run.ps1 will re-seed). Also removes the generated
.env credential file.
.EXAMPLE
./run.ps1 -Clean
Wipe any previous state, then build and start the stack fresh.
#>
param(
[switch]$Down,
[switch]$Logs,
[switch]$NoBuild,
[switch]$Clean,
[string]$Service,
[string]$Username,
[string]$Password,
[switch]$Backup
)
$ErrorActionPreference = "Stop"
# Path to the git-ignored credential .env loaded by docker-compose (issue #886).
$EnvFilePath = Join-Path $PSScriptRoot ".env"
# Validate the -Username / -Password pairing up front: both or neither.
$authRequested = -not [string]::IsNullOrWhiteSpace($Username) -or -not [string]::IsNullOrWhiteSpace($Password)
$authEnabled = -not [string]::IsNullOrWhiteSpace($Username) -and -not [string]::IsNullOrWhiteSpace($Password)
if ($authRequested -and -not $authEnabled) {
throw "Supply BOTH -Username and -Password to enable state-API authentication, or neither to run anonymously."
}
function Remove-CredentialEnvFile {
if (Test-Path $EnvFilePath) {
Remove-Item $EnvFilePath -Force -ErrorAction SilentlyContinue
Write-Host "Removed generated credential .env" -ForegroundColor DarkGray
}
}
function Write-CredentialEnvFile {
# Always (re)write .env on the up path so a stale toggle from a previous run
# never lingers into a later run. Both the state-API auth flag and the backup
# flag are written every time (each defaulting to false) so .env reflects
# exactly the switches passed to THIS invocation. The two flags are
# independent; env_file injects whichever lines are present into every silo.
$backupLine = if ($Backup) { "LATTICE_BACKUP_ENABLED=true" } else { "LATTICE_BACKUP_ENABLED=false" }
if (-not $authEnabled) {
Set-Content -Path $EnvFilePath -Value @("EXPLORER_STATE_AUTH=false", $backupLine) -Encoding ascii
if ($Backup) {
Write-Host "Backup subsystem enabled (LATTICE_BACKUP_ENABLED=true, .env)" -ForegroundColor Green
}
return
}
$helper = Join-Path $PSScriptRoot "..\..\tools\New-LatticeStateCredential.ps1"
if (-not (Test-Path $helper)) {
throw "Credential helper not found at $helper"
}
# Hand the plaintext password to the helper through an environment variable
# (never on the command line) so it cannot be observed via the process table.
# The helper hashes it in-process and writes only the salted hash to stdout.
$env:MSMFG_STATE_PW = $Password
try {
$credLine = & pwsh -NoProfile -File $helper -Username $Username -PasswordEnv MSMFG_STATE_PW -Format env
$helperExit = $LASTEXITCODE
}
finally {
Remove-Item Env:MSMFG_STATE_PW -ErrorAction SilentlyContinue
}
if ($helperExit -eq 3) {
throw "Password rejected by policy: minimum 8 characters with at least one uppercase letter, one lowercase letter, and one digit."
}
if ($helperExit -ne 0 -or [string]::IsNullOrWhiteSpace($credLine)) {
throw "Credential generation failed (helper exit $helperExit)."
}
# $credLine is "LATTICE_STATE_USER_<user>=pbkdf2-sha256$...". Pair it with the
# enable flag the host reads to turn RequireAuthorization on, plus the backup
# toggle so the two subsystems can be enabled together.
Set-Content -Path $EnvFilePath -Value @("EXPLORER_STATE_AUTH=true", $credLine, $backupLine) -Encoding ascii
Write-Host "Generated state-API credential for '$Username' (.env, git-ignored)" -ForegroundColor Green
if ($Backup) {
Write-Host "Backup subsystem enabled (LATTICE_BACKUP_ENABLED=true, .env)" -ForegroundColor Green
}
}
# Always run compose from this script's directory so docker-compose.yml,
# Dockerfile, and the relative build context (../../) resolve correctly.
Push-Location $PSScriptRoot
try {
# Sanity-check: docker must be on PATH and the daemon reachable.
$dockerCmd = Get-Command docker -ErrorAction SilentlyContinue
if (-not $dockerCmd) {
throw "docker is not on PATH. Install Docker Desktop (Windows) or the Docker engine."
}
& docker info --format "{{.ServerVersion}}" 2>$null | Out-Null
if ($LASTEXITCODE -ne 0) {
throw "docker daemon is not reachable. Start Docker Desktop and retry."
}
if ($Down) {
Write-Host "Stopping msmfg stack and removing volumes..." -ForegroundColor Yellow
& docker compose down --volumes --remove-orphans
Remove-CredentialEnvFile
return
}
if ($Logs) {
if ($Service) {
& docker compose logs -f $Service
} else {
& docker compose logs -f
}
return
}
if ($Clean) {
Write-Host "Clearing pre-existing msmfg state (containers, networks, volumes)..." -ForegroundColor Yellow
& docker compose down --volumes --remove-orphans
if ($LASTEXITCODE -ne 0) { throw "docker compose down failed (exit $LASTEXITCODE)." }
Remove-CredentialEnvFile
}
# Write (or refresh) the credential .env before bringing the stack up so
# docker-compose's optional env_file picks it up. Anonymous runs reset it to
# EXPLORER_STATE_AUTH=false; authenticated runs generate the salted hash.
Write-CredentialEnvFile
if (-not $NoBuild) {
Write-Host "Building msmfg-host:dev image (docker compose build)..." -ForegroundColor Cyan
# All four silos share the identical msmfg-host:dev image, so build it
# once (silo-us-a) instead of letting compose run four identical builds.
# BuildKit is required for the Dockerfile's NuGet cache mount, which keeps
# the global-packages folder warm between builds so restore only hits
# nuget.org on the very first build - the rest resolve locally.
$env:DOCKER_BUILDKIT = "1"
$env:COMPOSE_DOCKER_CLI_BUILD = "1"
& docker compose build silo-us-a
if ($LASTEXITCODE -ne 0) { throw "docker compose build failed (exit $LASTEXITCODE)." }
}
Write-Host "Starting msmfg stack (docker compose up -d)..." -ForegroundColor Cyan
& docker compose up -d
if ($LASTEXITCODE -ne 0) { throw "docker compose up failed (exit $LASTEXITCODE)." }
# Poll the published ports on the host side until they accept TCP.
# Each silo opens its HTTP listener well before the Orleans cluster
# membership settles, so this is a minimal "process is alive" probe,
# not a readiness gate. Full cluster bootstrap (replication reminders,
# seed run) can take another 10-30 seconds and is visible in logs.
function Wait-ForTcpPort {
param([string]$HostName, [int]$Port, [int]$TimeoutSeconds = 60)
$sw = [System.Diagnostics.Stopwatch]::StartNew()
while ($sw.Elapsed.TotalSeconds -lt $TimeoutSeconds) {
try {
$client = [System.Net.Sockets.TcpClient]::new()
$iar = $client.BeginConnect($HostName, $Port, $null, $null)
if ($iar.AsyncWaitHandle.WaitOne(1000)) {
$client.EndConnect($iar)
$client.Close()
return $true
}
$client.Close()
} catch { Start-Sleep -Milliseconds 500 }
}
return $false
}
$urls = @(
@{ Name = "traefik-us (US cluster)"; Port = 5001 }
@{ Name = "traefik-eu (EU cluster)"; Port = 5002 }
)
foreach ($u in $urls) {
Write-Host -NoNewline "Waiting for $($u.Name) on :$($u.Port)... "
if (Wait-ForTcpPort -HostName "localhost" -Port $u.Port -TimeoutSeconds 90) {
Write-Host "ready" -ForegroundColor Green
} else {
Write-Host "TIMEOUT" -ForegroundColor Red
# Traefik itself comes up in ~1s; a timeout here usually means
# no healthy backend silo, so point the operator at silo logs.
Write-Host " Check: docker compose logs traefik-us traefik-eu"
Write-Host " Check: docker compose logs silo-us-a silo-us-b silo-eu-a silo-eu-b"
}
}
Write-Host ""
Write-Host "Cluster URLs (sticky-LB via Traefik):" -ForegroundColor Cyan
Write-Host " http://localhost:5001 US cluster (routes to silo-us-a | silo-us-b)"
Write-Host " http://localhost:5002 EU cluster (routes to silo-eu-a | silo-eu-b)"
Write-Host ""
Write-Host "Explore the cluster with Orleans.Lattice.Explorer:" -ForegroundColor Cyan
if ($authEnabled) {
Write-Host " State-API authentication is ENABLED (user '$Username')."
Write-Host " ./run-explorer.ps1 -Username $Username -Password <password> browse US (signed in)"
Write-Host " ./run-explorer.ps1 -Cluster eu -Username $Username -Password <password> browse EU"
} else {
Write-Host " State-API authentication is DISABLED (anonymous, default)."
Write-Host " ./run-explorer.ps1 browse US in the Blazor explorer"
Write-Host " ./run-explorer.ps1 -Cluster eu browse EU"
Write-Host " ./run-explorer.ps1 -Client windows browse in the Windows desktop explorer"
}
if ($Backup) {
Write-Host " Backup subsystem is ENABLED (demo-grade, authorization off)."
Write-Host " Drive capture / restore from the explorer's backup UI."
}
Write-Host ""
Write-Host "Useful commands:" -ForegroundColor Cyan
Write-Host " ./run.ps1 -Logs tail all silo logs"
Write-Host " ./run.ps1 -Logs -Service silo-us-a tail one silo"
Write-Host " docker compose ps show container state"
Write-Host " docker network disconnect msmfg_us-net msmfg-traefik-eu"
Write-Host " simulate a cross-cluster partition (sever US -> EU)"
Write-Host " ./run.ps1 -Username u -Password p start with state-API auth enabled"
Write-Host " ./run.ps1 -Clean wipe state, then start fresh"
Write-Host " ./run.ps1 -Backup start with the backup subsystem enabled"
Write-Host " ./run.ps1 -Down stop + wipe volumes"
}
finally {
Pop-Location
}