windows-vm

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Headless Windows 11 VM

无头Windows 11 VM

Manage a headless Windows 11 VM running via dockur/windows in Docker with KVM acceleration. The VM is accessible via SSH only — no RDP or GUI required.
管理通过Docker中的dockur/windows运行、支持KVM加速的无头Windows 11虚拟机。该虚拟机仅支持通过SSH访问,无需RDP或GUI。

Host prerequisites

宿主机前置要求

  • Docker
  • KVM support (
    /dev/kvm
    must exist — check with
    ls /dev/kvm
    )
  • sshpass
    (
    sudo apt install sshpass
    )
  • imagemagick
    (optional, for screenshot debugging:
    sudo apt install imagemagick
    )
  • Docker
  • 支持KVM(必须存在
    /dev/kvm
    ,可通过
    ls /dev/kvm
    命令检查)
  • sshpass
    (可通过
    sudo apt install sshpass
    安装)
  • imagemagick
    (可选,用于截图调试:
    sudo apt install imagemagick

Configuration

配置信息

  • Container name:
    windows11
  • VM directory:
    $HOME/windows-vm/
    • storage/
      — VM disk image (managed by dockur, wiped on recreate)
    • iso/win11x64.iso
      — cached Windows ISO (7.3GB, persists across recreates)
    • oem/install.bat
      — post-install script (installs OpenSSH Server)
  • Credentials: user / password
  • SSH:
    localhost:2222
    (bound to 127.0.0.1 only)
  • RDP:
    localhost:3389
    (bound to 127.0.0.1 only, fallback)
  • Web console:
    localhost:8006
    (VNC in browser, for debugging)
  • Resources: 8GB RAM, 4 CPU cores, 64GB disk
  • 容器名称
    windows11
  • 虚拟机目录
    $HOME/windows-vm/
    • storage/
      — 虚拟机磁盘镜像(由dockur管理,重建容器时会被清空)
    • iso/win11x64.iso
      — 缓存的Windows ISO镜像(大小7.3GB,容器重建后仍保留)
    • oem/install.bat
      — 后置安装脚本(用于安装OpenSSH Server)
  • 登录凭证:user / password
  • SSH访问
    localhost:2222
    (仅绑定到127.0.0.1)
  • RDP访问
    localhost:3389
    (仅绑定到127.0.0.1,备用)
  • Web控制台
    localhost:8006
    (浏览器端VNC,用于调试)
  • 资源配置:8GB内存、4核CPU、64GB磁盘

Actions

操作指南

create — First-time setup or full recreate

create — 首次设置或完整重建

  1. Ensure directories exist:
    bash
    mkdir -p "$HOME/windows-vm/oem" "$HOME/windows-vm/storage" "$HOME/windows-vm/iso"
  2. Ensure
    $HOME/windows-vm/oem/install.bat
    exists with OpenSSH setup:
    bat
    @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.
  3. If recreating, remove the old container and disk:
    bash
    docker stop windows11 && docker rm windows11
    rm -f "$HOME/windows-vm/storage/data.img"
  4. Launch the container. There are two cases:
    If cached ISO exists (
    $HOME/windows-vm/iso/win11x64.iso
    ):
    bash
    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
    :
    bash
    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):
    bash
    cp "$HOME/windows-vm/storage/win11x64.iso" "$HOME/windows-vm/iso/win11x64.iso"
  5. 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:
    bash
    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:
    bash
    sshpass -p 'password' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -p 2222 user@localhost "whoami"
  6. Once SSH is responding, install Node.js and Claude Code by piping a setup script via stdin (avoids PowerShell escaping hell over SSH):
    bash
    cat << 'PS' | sshpass -p 'password' ssh -o StrictHostKeyChecking=no -p 2222 user@localhost "powershell -ExecutionPolicy Bypass -Command -"
    # Download and install Node.js silently
    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"
    
    # Install Claude Code globally
    & 'C:\Program Files\nodejs\npm.cmd' install -g @anthropic-ai/claude-code
    Write-Host "Claude Code installed"
    
    # Add npm global bin to SYSTEM PATH (user PATH is not read by sshd)
    $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 execution policy machine-wide (required for claude.ps1)
    Set-ExecutionPolicy RemoteSigned -Scope LocalMachine -Force -ErrorAction SilentlyContinue
    
    # Create system-wide PowerShell profile that rebuilds PATH from registry on login.
    # Without this, interactive SSH sessions don't pick up the full system PATH.
    $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 sshd so it picks up the new PATH
    Restart-Service sshd -Force
    PS
    Note: the connection will drop when sshd restarts — that's expected.
  7. Clear the stale host key (new VM = new host key) and verify:
    bash
    ssh-keygen -f ~/.ssh/known_hosts -R '[localhost]:2222'
    sshpass -p 'password' ssh -o StrictHostKeyChecking=no -p 2222 user@localhost "claude --version"
  1. 确保目录存在:
    bash
    mkdir -p "$HOME/windows-vm/oem" "$HOME/windows-vm/storage" "$HOME/windows-vm/iso"
  2. 确保
    $HOME/windows-vm/oem/install.bat
    存在,包含OpenSSH配置:
    bat
    @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.
  3. 如果是重建场景,先删除旧容器和磁盘:
    bash
    docker stop windows11 && docker rm windows11
    rm -f "$HOME/windows-vm/storage/data.img"
  4. 启动容器,分两种场景:
    如果已存在缓存ISO
    $HOME/windows-vm/iso/win11x64.iso
    ):
    bash
    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
    首次运行(无缓存ISO) — 去掉
    /boot.iso
    挂载并添加
    VERSION
    参数:
    bash
    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
    ISO下载完成且Windows启动后,在容器首次停止前立即复制ISO文件(dockur在重建容器时会清空
    /storage
    目录):
    bash
    cp "$HOME/windows-vm/storage/win11x64.iso" "$HOME/windows-vm/iso/win11x64.iso"
  5. 等待Windows安装和OpenSSH配置完成,全新安装需要20-30分钟(OEM的install.bat会在Windows OOBE流程结束后运行,从微软下载OpenSSH速度较慢),可通过以下命令监控进度:
    bash
    docker logs -f windows11
    也可以通过Web控制台
    http://localhost:8006
    查看虚拟机屏幕。
    检查SSH是否可用:
    bash
    sshpass -p 'password' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -p 2222 user@localhost "whoami"
  6. SSH正常响应后,通过标准输入传入安装脚本安装Node.js和Claude Code(避免SSH传输PowerShell命令时的转义问题):
    bash
    cat << 'PS' | sshpass -p 'password' ssh -o StrictHostKeyChecking=no -p 2222 user@localhost "powershell -ExecutionPolicy Bypass -Command -"
    # Download and install Node.js silently
    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"
    
    # Install Claude Code globally
    & 'C:\Program Files\nodejs\npm.cmd' install -g @anthropic-ai/claude-code
    Write-Host "Claude Code installed"
    
    # Add npm global bin to SYSTEM PATH (user PATH is not read by sshd)
    $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 execution policy machine-wide (required for claude.ps1)
    Set-ExecutionPolicy RemoteSigned -Scope LocalMachine -Force -ErrorAction SilentlyContinue
    
    # Create system-wide PowerShell profile that rebuilds PATH from registry on login.
    # Without this, interactive SSH sessions don't pick up the full system PATH.
    $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 sshd so it picks up the new PATH
    Restart-Service sshd -Force
    PS
    注意:sshd重启时SSH连接会断开,属于正常现象。
  7. 清除旧的主机密钥(新虚拟机对应新的主机密钥)并验证安装:
    bash
    ssh-keygen -f ~/.ssh/known_hosts -R '[localhost]:2222'
    sshpass -p 'password' ssh -o StrictHostKeyChecking=no -p 2222 user@localhost "claude --version"

start — Start a stopped VM

start — 启动已停止的虚拟机

bash
docker start windows11
bash
docker start windows11

stop — Stop the VM

stop — 停止虚拟机

bash
docker stop windows11
bash
docker stop windows11

restart — Restart the VM

restart — 重启虚拟机

bash
docker restart windows11
bash
docker restart windows11

status — Check VM status

status — 查看虚拟机状态

bash
docker ps -f name=windows11 --format "table {{.Status}}\t{{.Ports}}"
docker logs windows11 2>&1 | tail -5
bash
docker ps -f name=windows11 --format "table {{.Status}}\t{{.Ports}}"
docker logs windows11 2>&1 | tail -5

ssh — Connect to the VM

ssh — 连接到虚拟机

bash
ssh -p 2222 user@localhost
bash
ssh -p 2222 user@localhost

screenshot — See what's on the VM screen (for debugging)

screenshot — 查看虚拟机屏幕内容(用于调试)

bash
docker exec windows11 bash -c "echo 'screendump /tmp/screen.ppm' | nc -w 2 localhost 7100" > /dev/null 2>&1
sleep 1
docker cp windows11:/tmp/screen.ppm /tmp/screen.ppm
convert /tmp/screen.ppm /tmp/screen.png
bash
docker exec windows11 bash -c "echo 'screendump /tmp/screen.ppm' | nc -w 2 localhost 7100" > /dev/null 2>&1
sleep 1
docker cp windows11:/tmp/screen.ppm /tmp/screen.ppm
convert /tmp/screen.ppm /tmp/screen.png

Important Notes

重要说明

  • ISO caching: The
    /storage
    volume is managed by dockur and gets wiped on recreate. Store the ISO separately in
    $HOME/windows-vm/iso/
    and mount it as
    /boot.iso
    to skip the 7.3GB download.
  • --cap-add NET_ADMIN
    is required for port forwarding to work. Without it, QEMU falls back to user-mode networking and port forwarding silently fails.
  • --device /dev/kvm
    is required for hardware acceleration.
  • Boot time: Fresh install takes 20-30 min (Windows install + OpenSSH download from Microsoft). Subsequent boots from existing
    data.img
    are fast (~2 min).
  • Ports are bound to
    127.0.0.1
    only — not exposed to the network.
  • Do NOT use
    -e VERSION="win11"
    when mounting
    /boot.iso
    — the version is auto-detected from the ISO.
  • ISO缓存
    /storage
    卷由dockur管理,重建容器时会被清空。将ISO单独存放在
    $HOME/windows-vm/iso/
    目录并挂载为
    /boot.iso
    可以跳过7.3GB的下载流程。
  • 必须添加
    --cap-add NET_ADMIN
    参数才能正常使用端口转发,否则QEMU会 fallback 到用户模式网络,端口转发会静默失败。
  • 必须添加
    --device /dev/kvm
    参数才能启用硬件加速。
  • 启动时长:全新安装需要20-30分钟(Windows安装+从微软下载OpenSSH),后续从已有
    data.img
    启动速度很快(约2分钟)。
  • 所有端口仅绑定到
    127.0.0.1
    ,不会暴露到公网。
  • 挂载
    /boot.iso
    时不要使用
    -e VERSION="win11"
    参数,版本会自动从ISO中识别。

Post-install gotchas

安装后注意事项

  • Node.js is not pre-installed — the Claude Code install script (
    irm https://claude.ai/install.ps1 | iex
    ) will report success but
    claude
    won't work without Node. Install Node.js via MSI first.
  • npm global bin not in PATH — Node's MSI adds
    C:\Program Files\nodejs
    to PATH but not
    C:\Users\user\AppData\Roaming\npm
    (where
    npm install -g
    puts binaries). Must add it to the system PATH (not user PATH) because OpenSSH's sshd only reads system PATH. After changing system PATH, restart sshd.
  • PowerShell execution policy — Default policy is
    Restricted
    , which blocks
    claude.ps1
    . Must set to
    RemoteSigned
    at LocalMachine scope (not CurrentUser) for it to take effect in SSH sessions.
  • Escaping hell — Running PowerShell commands over SSH with nested quotes is unreliable. Pipe scripts via stdin using
    powershell -ExecutionPolicy Bypass -Command -
    instead.
  • Interactive SSH sessions don't get full PATH — Windows OpenSSH sshd doesn't properly propagate the system PATH to interactive PowerShell sessions. Fix: create a system-wide PowerShell profile (
    $PROFILE.AllUsersAllHosts
    ) that rebuilds
    $env:Path
    from the registry on every login.
  • winget may not work — The Microsoft Store certificate can fail in a VM. Use direct MSI/installer downloads instead.
  • Host key changes — Each recreated VM gets new SSH host keys. Run
    ssh-keygen -R '[localhost]:2222'
    to clear the old one.
  • 未预装Node.js — Claude Code安装脚本(
    irm https://claude.ai/install.ps1 | iex
    )会提示安装成功,但缺少Node.js时
    claude
    命令无法运行,需要先通过MSI安装Node.js。
  • npm全局bin目录不在PATH中 — Node.js的MSI安装包会将
    C:\Program Files\nodejs
    添加到PATH,但不会添加
    C:\Users\user\AppData\Roaming\npm
    npm install -g
    安装的二进制文件存放路径)。必须将其添加到系统PATH而非用户PATH,因为OpenSSH的sshd仅读取系统PATH。修改系统PATH后需要重启sshd。
  • PowerShell执行策略 — 默认策略是
    Restricted
    ,会拦截
    claude.ps1
    脚本运行。必须将LocalMachine作用域的执行策略设置为
    RemoteSigned
    (而非CurrentUser),才能在SSH会话中生效。
  • 转义问题 — 通过SSH运行包含嵌套引号的PowerShell命令可靠性很低,建议使用
    powershell -ExecutionPolicy Bypass -Command -
    通过标准输入传入脚本。
  • 交互式SSH会话无法获取完整PATH — Windows版OpenSSH的sshd不会将完整系统PATH传递给交互式PowerShell会话,解决方法是创建全局PowerShell配置文件(
    $PROFILE.AllUsersAllHosts
    ),每次登录时从注册表重新构建
    $env:Path
  • winget可能无法使用 — 虚拟机中可能出现Microsoft Store证书验证失败的问题,建议直接下载MSI/安装包进行安装。
  • 主机密钥变更 — 每次重建虚拟机都会生成新的SSH主机密钥,需要运行
    ssh-keygen -R '[localhost]:2222'
    清除旧密钥。