-
Ensure directories exist:
mkdir -p "$HOME/windows-vm/oem" "$HOME/windows-vm/storage" "$HOME/windows-vm/iso"
-
Ensure $HOME/windows-vm/oem/install.bat exists with OpenSSH setup:
@echo off
echo Installing OpenSSH Server...
powershell -Command "Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0" 2>nul
powershell -Command "Get-WindowsCapability -Online -Name OpenSSH.Server* | Add-WindowsCapability -Online" 2>nul
dism /Online /Add-Capability /CapabilityName:OpenSSH.Server~~~~0.0.1.0 2>nul
powershell -Command "Start-Service sshd" 2>nul
powershell -Command "Set-Service -Name sshd -StartupType Automatic"
powershell -Command "New-ItemProperty -Path 'HKLM:\SOFTWARE\OpenSSH' -Name DefaultShell -Value 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe' -PropertyType String -Force"
powershell -Command "New-NetFirewallRule -Name 'OpenSSH-Server' -DisplayName 'OpenSSH Server' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22"
powershell -Command "Get-Service sshd" 2>nul
echo Done.
-
If recreating, remove the old container and disk:
docker stop windows11 && docker rm windows11
rm -f "$HOME/windows-vm/storage/data.img"
-
Launch the container. There are two cases:
If cached ISO exists ($HOME/windows-vm/iso/win11x64.iso):
docker run -d \
--name windows11 \
-p 127.0.0.1:3389:3389 \
-p 127.0.0.1:2222:22 \
-p 127.0.0.1:8006:8006 \
-e RAM_SIZE="8G" \
-e CPU_CORES="4" \
-e DISK_SIZE="64G" \
-e USERNAME="user" \
-e PASSWORD="password" \
--cap-add NET_ADMIN \
--device /dev/kvm \
-v "$HOME/windows-vm/storage:/storage" \
-v "$HOME/windows-vm/oem:/oem" \
-v "$HOME/windows-vm/iso/win11x64.iso:/boot.iso" \
dockurr/windows
First time (no cached ISO) — omit the /boot.iso mount and add VERSION:
docker run -d \
--name windows11 \
-p 127.0.0.1:3389:3389 \
-p 127.0.0.1:2222:22 \
-p 127.0.0.1:8006:8006 \
-e RAM_SIZE="8G" \
-e CPU_CORES="4" \
-e DISK_SIZE="64G" \
-e VERSION="win11" \
-e USERNAME="user" \
-e PASSWORD="password" \
--cap-add NET_ADMIN \
--device /dev/kvm \
-v "$HOME/windows-vm/storage:/storage" \
-v "$HOME/windows-vm/oem:/oem" \
dockurr/windows
After the ISO downloads and Windows boots, immediately copy the ISO out before
the container is ever stopped (dockur wipes /storage on recreate):
cp "$HOME/windows-vm/storage/win11x64.iso" "$HOME/windows-vm/iso/win11x64.iso"
-
Wait for Windows install + OpenSSH setup to complete. This takes 20-30 minutes for a
fresh install (the OEM install.bat runs at the end of Windows OOBE and downloads OpenSSH
from Microsoft, which is slow). Monitor with:
docker logs -f windows11
You can also watch the VM screen via the web console at http://localhost:8006.
To check if SSH is up:
sshpass -p 'password' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -p 2222 user@localhost "whoami"
-
Once SSH is responding, install Node.js and Claude Code by piping a setup script via stdin
(avoids PowerShell escaping hell over SSH):
cat << 'PS' | sshpass -p 'password' ssh -o StrictHostKeyChecking=no -p 2222 user@localhost "powershell -ExecutionPolicy Bypass -Command -"
Invoke-WebRequest -Uri 'https://nodejs.org/dist/v22.14.0/node-v22.14.0-x64.msi' -OutFile 'C:\Users\user\node-install.msi'
Start-Process msiexec.exe -ArgumentList '/i C:\Users\user\node-install.msi /qn /norestart' -Wait -Verb RunAs
Write-Host "Node.js installed"
& 'C:\Program Files\nodejs\npm.cmd' install -g @anthropic-ai/claude-code
Write-Host "Claude Code installed"
$systemPath = [Environment]::GetEnvironmentVariable('Path', 'Machine')
$additions = @()
if ($systemPath -notlike '*AppData*npm*') { $additions += 'C:\Users\user\AppData\Roaming\npm' }
if ($systemPath -notlike '*Git\cmd*') { $additions += 'C:\Program Files\Git\cmd' }
if ($additions.Count -gt 0) {
[Environment]::SetEnvironmentVariable('Path', $systemPath + ';' + ($additions -join ';'), 'Machine')
Write-Host "Added to system PATH: $($additions -join ', ')"
}
Set-ExecutionPolicy RemoteSigned -Scope LocalMachine -Force -ErrorAction SilentlyContinue
$profileDir = Split-Path $PROFILE.AllUsersAllHosts
if (-not (Test-Path $profileDir)) { New-Item -ItemType Directory -Path $profileDir -Force }
@'
$machinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine')
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
$env:Path = "$machinePath;$userPath"
'@ | Set-Content -Path $PROFILE.AllUsersAllHosts -Force
Write-Host "PowerShell profile created"
Restart-Service sshd -Force
PS
Note: the connection will drop when sshd restarts — that's expected.
-
Clear the stale host key (new VM = new host key) and verify:
ssh-keygen -f ~/.ssh/known_hosts -R '[localhost]:2222'
sshpass -p 'password' ssh -o StrictHostKeyChecking=no -p 2222 user@localhost "claude --version"