#Requires -RunAsAdministrator <# .SYNOPSIS Force-installs Docker Desktop on Windows 10 LTSC / IoT LTSC editions. .DESCRIPTION Docker Desktop's installer rejects non-Enterprise/Pro editions at launch. This script works around that using three layered techniques: Technique A -- Registry EditionID spoof (all builds) Temporarily sets HKLM\...\CurrentVersion\EditionID to "Enterprise" so the installer's edition check passes, then restores the original value after installation. Technique B -- WSL2 backend (build 19041+, recommended) Installs WSL2 manually (wsl --install or the MSI path for LTSC), then launches Docker Desktop with --backend=wsl2. Gives you full Linux container support without Hyper-V. Technique C -- Hyper-V feature injection (build 17763+) Enables Hyper-V on SKUs where the GUI won't show it (IoT Enterprise, IoT Enterprise LTSC) by directly enabling the optional features and writing the required BCD entry. Used as fallback for the Windows containers backend or when WSL2 is unavailable. All three techniques are applied in sequence; you can disable any of them with the switches below. .PARAMETER DockerDesktopVersion Version of Docker Desktop to install, e.g. "4.30.0". Defaults to "latest" which queries the Docker Hub release API. .PARAMETER Backend "wsl2" -- Linux containers via WSL2 (default, recommended) "hyper-v" -- Windows/Linux containers via Hyper-V "windows" -- Windows containers only, no Hyper-V / WSL2 needed .PARAMETER SkipEditionSpoof Skip the temporary EditionID registry spoof. Use only if your SKU already passes Docker Desktop's check. .PARAMETER SkipWSL2 Skip WSL2 installation (ignored when -Backend is not wsl2). .PARAMETER SkipHyperV Skip Hyper-V feature enablement. .PARAMETER KeepDownloads Do not delete downloaded installers after use. .EXAMPLE # Fully automatic, WSL2 backend (recommended for LTSC 2021) .\Install-DockerDesktop-LTSC-Force.ps1 # Hyper-V backend on LTSC 2019 without WSL2 .\Install-DockerDesktop-LTSC-Force.ps1 -Backend hyper-v -SkipWSL2 # Windows-containers-only, no virtualisation layer at all .\Install-DockerDesktop-LTSC-Force.ps1 -Backend windows -SkipWSL2 -SkipHyperV .NOTES [!] This script modifies system registry keys and Windows features. Take a snapshot / backup before running on production machines. [!] A reboot is almost certainly required after the first run. Tested on: Windows 10 IoT Enterprise LTSC 2019 (build 17763) Windows 10 IoT Enterprise LTSC 2021 (build 19044) Windows 10 Enterprise LTSC 2021 (build 19044) #> [CmdletBinding()] param( [string] $DockerDesktopVersion = "latest", [ValidateSet("wsl2","hyper-v","windows")] [string] $Backend = "wsl2", [switch] $SkipEditionSpoof, [switch] $SkipWSL2, [switch] $SkipHyperV, [switch] $KeepDownloads ) Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 # -- Helpers ------------------------------------------------------------------- function Write-Step { param($m) Write-Host "`n==> $m" -ForegroundColor Cyan } function Write-OK { param($m) Write-Host " [OK] $m" -ForegroundColor Green } function Write-Warn { param($m) Write-Host " [WARN] $m" -ForegroundColor Yellow } function Write-Fail { param($m) Write-Host "`n[FAIL] $m" -ForegroundColor Red; exit 1 } function Invoke-WithRetry { param([scriptblock]$Action, [int]$Retries = 3, [int]$DelaySeconds = 5) for ($i = 1; $i -le $Retries; $i++) { try { return & $Action } catch { if ($i -eq $Retries) { throw } Write-Warn "Attempt $i failed -- retrying in $DelaySeconds s..." Start-Sleep $DelaySeconds } } } function Enable-WindowsFeatureSafe { param([string]$Name) $f = Get-WindowsOptionalFeature -Online -FeatureName $Name -ErrorAction SilentlyContinue if ($null -eq $f) { Write-Warn "Feature '$Name' not found on this SKU -- skipping"; return } if ($f.State -eq "Enabled") { Write-OK "'$Name' already enabled"; return } Write-Host " Enabling '$Name'..." Enable-WindowsOptionalFeature -Online -FeatureName $Name -All -NoRestart | Out-Null Write-OK "'$Name' enabled (reboot required)" $script:RebootRequired = $true } $script:RebootRequired = $false $script:OriginalEdition = $null # used by spoof/restore pair # -- 1. OS check --------------------------------------------------------------- Write-Step "Validating operating system" $regCV = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" $build = [int](Get-ItemProperty $regCV).CurrentBuildNumber $curEd = (Get-ItemProperty $regCV).EditionID $os = Get-WmiObject Win32_OperatingSystem Write-OK "Caption : $($os.Caption)" Write-OK "Build : $build" Write-OK "EditionID: $curEd" Write-OK "SKU : $($os.OperatingSystemSKU)" if ($build -lt 17763) { Write-Fail "Minimum supported build is 17763 (LTSC 2019). Found $build." } if ($Backend -eq "wsl2" -and $build -lt 19041) { Write-Warn "WSL2 requires build 19041+. Your build is $build -- switching backend to 'hyper-v'." $Backend = "hyper-v" } Write-OK "Selected backend: $Backend" # -- 2. Resolve Docker Desktop download URL ------------------------------------ Write-Step "Resolving Docker Desktop installer" if ($DockerDesktopVersion -eq "latest") { try { $rel = Invoke-RestMethod "https://desktop.docker.com/win/main/amd64/appcast.xml" ` -UseBasicParsing -ErrorAction Stop # appcast is XML; the url contains the version $url = ($rel.rss.channel.item.enclosure | Select-Object -First 1).url if (-not $url) { throw "empty" } $DockerDesktopVersion = [regex]::Match($url, 'Docker%20Desktop%20(\d+\.\d+\.\d+)').Groups[1].Value Write-OK "Latest Docker Desktop: $DockerDesktopVersion" } catch { # Fallback: direct stable URL (always points to current release) $url = "https://desktop.docker.com/win/main/amd64/Docker%20Desktop%20Installer.exe" Write-Warn "Could not determine latest version -- using rolling latest URL" } } if (-not $url) { # Build versioned URL $escaped = "Docker%20Desktop%20$DockerDesktopVersion.exe" $url = "https://desktop.docker.com/win/main/amd64/$escaped" } Write-OK "Installer URL: $url" $tmpDir = Join-Path $Env:TEMP "docker-desktop-install" New-Item -ItemType Directory -Force $tmpDir | Out-Null $installer = Join-Path $tmpDir "DockerDesktopInstaller.exe" # -- 3. TECHNIQUE A -- EditionID registry spoof -------------------------------- function Set-EditionSpoof { $cv = Get-ItemProperty $regCV # Edition fields $script:OriginalEdition = $cv.EditionID $script:OriginalProductName = $cv.ProductName # Build / version fields (Docker Desktop requires build 19045 / 22H2) $script:OriginalCurrentBuild = $cv.CurrentBuild $script:OriginalCurrentBuildNumber = $cv.CurrentBuildNumber $script:OriginalDisplayVersion = $cv.DisplayVersion $script:OriginalReleaseId = $cv.ReleaseId $script:OriginalUBR = $cv.UBR $needsEditionSpoof = $script:OriginalEdition -notmatch "Enterprise|Pro|Home" $needsBuildSpoof = ([int]$script:OriginalCurrentBuildNumber) -lt 19045 if (-not $needsEditionSpoof -and -not $needsBuildSpoof) { Write-OK "Edition and build already pass Docker Desktop checks -- spoof not needed" $script:OriginalEdition = $null # signal: nothing to restore return } if ($needsEditionSpoof) { Write-Host " Spoofing EditionID: '$script:OriginalEdition' -> 'Enterprise' ..." Set-ItemProperty -Path $regCV -Name "EditionID" -Value "Enterprise" Set-ItemProperty -Path $regCV -Name "ProductName" -Value "Windows 10 Enterprise" } if ($needsBuildSpoof) { # Spoof to Windows 10 22H2 (build 19045) -- minimum accepted by Docker Desktop Write-Host " Spoofing build: '$script:OriginalCurrentBuildNumber' -> '19045' (22H2) ..." Set-ItemProperty -Path $regCV -Name "CurrentBuild" -Value "19045" Set-ItemProperty -Path $regCV -Name "CurrentBuildNumber" -Value "19045" Set-ItemProperty -Path $regCV -Name "DisplayVersion" -Value "22H2" Set-ItemProperty -Path $regCV -Name "ReleaseId" -Value "2009" Set-ItemProperty -Path $regCV -Name "UBR" -Value 4170 } Write-OK "Registry spoofed (will be fully restored after install)" } function Restore-EditionSpoof { if ($null -eq $script:OriginalEdition) { return } Write-Host " Restoring original registry values ..." Set-ItemProperty -Path $regCV -Name "EditionID" -Value $script:OriginalEdition Set-ItemProperty -Path $regCV -Name "ProductName" -Value $script:OriginalProductName Set-ItemProperty -Path $regCV -Name "CurrentBuild" -Value $script:OriginalCurrentBuild Set-ItemProperty -Path $regCV -Name "CurrentBuildNumber" -Value $script:OriginalCurrentBuildNumber Set-ItemProperty -Path $regCV -Name "DisplayVersion" -Value $script:OriginalDisplayVersion Set-ItemProperty -Path $regCV -Name "ReleaseId" -Value $script:OriginalReleaseId Set-ItemProperty -Path $regCV -Name "UBR" -Value $script:OriginalUBR Write-OK "Registry restored to original values" } # Register restore as a finally-block safeguard at the top level $spoofApplied = $false if (-not $SkipEditionSpoof) { Write-Step "Technique A: EditionID spoof" Set-EditionSpoof $spoofApplied = $true } # -- 4. TECHNIQUE B -- WSL2 ----------------------------------------------------- if ($Backend -eq "wsl2" -and -not $SkipWSL2) { Write-Step "Technique B: WSL2 installation" # Enable required features Enable-WindowsFeatureSafe "Microsoft-Windows-Subsystem-Linux" Enable-WindowsFeatureSafe "VirtualMachinePlatform" # Check if WSL2 kernel is already present $wslVer = wsl --status 2>$null | Select-String "Kernel" if ($wslVer) { Write-OK "WSL2 kernel already installed: $wslVer" } else { # On LTSC, 'wsl --install' may not work -- download the kernel MSI directly try { Write-Host " Trying 'wsl --install --no-distribution' ..." wsl --install --no-distribution 2>&1 | Out-Null Write-OK "WSL2 installed via wsl --install" } catch { Write-Warn "wsl --install failed -- downloading WSL2 kernel MSI manually ..." $wslMsi = Join-Path $tmpDir "wsl_update_x64.msi" Invoke-WithRetry { Invoke-WebRequest ` -Uri "https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi" ` -OutFile $wslMsi -UseBasicParsing } Start-Process msiexec.exe -ArgumentList "/i `"$wslMsi`" /quiet /norestart" -Wait Write-OK "WSL2 kernel MSI installed" } } # Set WSL default version try { wsl --set-default-version 2 2>&1 | Out-Null Write-OK "WSL default version set to 2" } catch { Write-Warn "Could not set WSL default version (may need reboot first)" $script:RebootRequired = $true } } # -- 5. TECHNIQUE C -- Hyper-V feature injection -------------------------------- if (($Backend -eq "hyper-v" -or $Backend -eq "wsl2") -and -not $SkipHyperV) { Write-Step "Technique C: Hyper-V feature injection" $hvFeatures = @( "Microsoft-Hyper-V-All", "Microsoft-Hyper-V", "Microsoft-Hyper-V-Tools-All", "Microsoft-Hyper-V-Management-PowerShell", "Microsoft-Hyper-V-Hypervisor", "Microsoft-Hyper-V-Services" ) foreach ($f in $hvFeatures) { Enable-WindowsFeatureSafe $f } # On IoT SKUs Hyper-V may need a BCD entry to actually activate Write-Host " Verifying BCD hypervisor launch type ..." $bcdOut = bcdedit /enum "{current}" 2>&1 if ($bcdOut -match "hypervisorlaunchtype\s+Auto") { Write-OK "BCD hypervisorlaunchtype already set to Auto" } else { bcdedit /set hypervisorlaunchtype Auto | Out-Null Write-OK "BCD hypervisorlaunchtype set to Auto" $script:RebootRequired = $true } } # -- 6. Download Docker Desktop installer -------------------------------------- Write-Step "Downloading Docker Desktop installer" if (Test-Path $installer) { Write-OK "Installer already in temp folder -- skipping download" } else { Write-Host " Downloading from $url ..." Invoke-WithRetry { Invoke-WebRequest -Uri $url -OutFile $installer -UseBasicParsing } Write-OK "Download complete ($('{0:N1}' -f (([math]::Round((Get-Item $installer).Length / 1048576, 1)))) MB)" } # -- 7. Install Docker Desktop ------------------------------------------------- Write-Step "Installing Docker Desktop (backend: $Backend)" # Build installer argument list $installArgs = @( "install", "--quiet", "--accept-license", "--no-windows-containers" # default off; we enable selectively below ) switch ($Backend) { "wsl2" { $installArgs += "--backend=wsl-2" } "hyper-v" { $installArgs += "--backend=hyper-v"; $installArgs = $installArgs | Where-Object { $_ -ne "--no-windows-containers" } } "windows" { $installArgs += "--backend=windows"; $installArgs = $installArgs | Where-Object { $_ -ne "--no-windows-containers" } } } Write-Host " Running: $installer $($installArgs -join ' ')" try { $proc = Start-Process -FilePath $installer -ArgumentList $installArgs ` -Wait -PassThru -NoNewWindow if ($proc.ExitCode -notin @(0, 3010)) { # 3010 = success, reboot required throw "Installer exited with code $($proc.ExitCode)" } if ($proc.ExitCode -eq 3010) { $script:RebootRequired = $true } Write-OK "Docker Desktop installer finished (exit code $($proc.ExitCode))" } finally { # Always restore edition spoof, even on installer failure if ($spoofApplied) { Restore-EditionSpoof } } # -- 8. Post-install configuration --------------------------------------------- Write-Step "Post-install configuration" # Force Docker Desktop to start without the update nag / welcome screen $ddConfigDir = "$Env:APPDATA\Docker" $ddConfigFile = "$ddConfigDir\settings.json" New-Item -ItemType Directory -Force $ddConfigDir | Out-Null if (-not (Test-Path $ddConfigFile)) { $backendValue = if ($Backend -eq "windows") { "windows" } else { "wsl2" } @" { "licenseTermsVersion": 2, "analyticsEnabled": false, "checkForUpdates": false, "autoStart": false, "exposeDockerAPIOnTCP2375": false, "wslEngineEnabled": $( if ($Backend -eq "wsl2") { "true" } else { "false" } ), "displayedTutorial": true, "backend": "$backendValue" } "@ | Set-Content $ddConfigFile -Encoding UTF8 Write-OK "Created Docker Desktop settings.json" } else { Write-OK "settings.json already exists -- not overwriting" } # Add Docker CLI to PATH if not already there (Desktop installs to a version-stamped dir) $dockerCli = "$Env:ProgramFiles\Docker\Docker\resources\bin" $sysPath = [Environment]::GetEnvironmentVariable("Path","Machine") if ($sysPath -notlike "*$dockerCli*") { [Environment]::SetEnvironmentVariable("Path","$sysPath;$dockerCli","Machine") $Env:Path += ";$dockerCli" Write-OK "Added Docker CLI to system PATH" } # -- 9. Cleanup ---------------------------------------------------------------- if (-not $KeepDownloads) { Remove-Item $tmpDir -Recurse -Force -ErrorAction SilentlyContinue Write-OK "Temporary files removed" } # -- 10. Summary --------------------------------------------------------------- Write-Host "`n================================================" -ForegroundColor Green Write-Host " Docker Desktop installation complete!" -ForegroundColor Green Write-Host "================================================" -ForegroundColor Green if ($script:RebootRequired) { Write-Host @" *** A REBOOT IS REQUIRED *** Windows features were enabled or WSL2 was installed. After rebooting, launch Docker Desktop from the Start menu and wait for the engine to start (~30 s), then run: docker run hello-world "@ -ForegroundColor Yellow } else { Write-Host @" Launch Docker Desktop from the Start menu, wait for the engine icon to turn green, then run: docker run hello-world "@ -ForegroundColor White } Write-Host " Techniques applied:" -ForegroundColor White Write-Host " A -- EditionID spoof : $(if ($SkipEditionSpoof) {'skipped'} else {'applied + restored'})" -ForegroundColor Gray Write-Host " B -- WSL2 : $(if ($SkipWSL2 -or $Backend -ne 'wsl2') {'skipped'} else {'applied'})" -ForegroundColor Gray Write-Host " C -- Hyper-V inject : $(if ($SkipHyperV) {'skipped'} else {'applied'})" -ForegroundColor Gray Write-Host " Backend selected : $Backend" -ForegroundColor Gray Write-Host ""