# Windows in a Docker Container — Admin Guide

This guide documents the architecture, internals, configuration surface, and operational concerns of `dockur/windows` for administrators, DevOps engineers, and developers who deploy or extend it. It is the companion to the **User Guide**, which covers installation and everyday use.

`dockur/windows` is a thin, Windows-specific layer on top of the [`qemux/qemu`](https://github.com/qemus/qemu) base image. It contributes the logic to **acquire** Windows installation media, **inject** virtio drivers and an unattended-setup answer file, **rebuild** a bootable ISO, and **share** files back to the host. The heavy lifting of disk creation, networking, display, and the QEMU command line lives in the base image.

---

## 1. Executive Summary

| Aspect | Summary |
|---|---|
| **What it is** | A Docker image that runs Windows in a QEMU/KVM virtual machine, with fully unattended installation |
| **Base image** | `qemux/qemu:7.32` (provides the VM runtime, disk/network/display, web viewer) |
| **This repo's role** | Windows media download + driver injection + unattended answer file + ISO rebuild + Samba share + graceful shutdown |
| **Language** | Bash (six scripts in `src/`), plus per-version unattend XML assets |
| **Entry point** | `/usr/bin/tini -s /run/entry.sh` |
| **Acceleration** | Requires host KVM (`/dev/kvm`); x64 only |
| **Exposed ports** | `8006` (web viewer / noVNC), `3389` (RDP, TCP+UDP) |
| **Persistent state** | `/storage` volume (virtual disk + marker files) |
| **Licensing** | MIT-style open source; no Microsoft media redistributed (downloaded at runtime) |

> [!NOTE]
> This is an x64 image. The ARM64 variant is a **separate image**, `dockurr/windows-arm`, referenced as a multi-arch build target in the Dockerfile (`FROM dockurr/windows-arm:${VERSION_ARG} AS build-arm64`).

---

## 2. Architecture

### 2.1 High-level layout

```text
            Host (Linux with KVM)
  ┌───────────────────────────────────────────────┐
  │  Docker / Podman / Kubernetes                  │
  │                                                │
  │   ┌──────────────  Container  ──────────────┐  │
  │   │  tini → /run/entry.sh                    │  │
  │   │     │                                    │  │
  │   │     ├─ source base-image scripts ────────┼──┼─ from qemux/qemu:7.32
  │   │     │   (start, utils, reset, server,    │  │   (disk, network, display,
  │   │     │    disk, display, network, boot,   │  │    web viewer, QEMU args)
  │   │     │    proc, memory, balloon, config,  │  │
  │   │     │    finish)                         │  │
  │   │     │                                    │  │
  │   │     ├─ source THIS repo's scripts ───────┼──┼─ src/*.sh
  │   │     │   define · mido · install · samba  │  │
  │   │     │   power                            │  │
  │   │     │                                    │  │
  │   │     └─ exec qemu-system-x86_64 ──────────┼──┼─ the Windows VM
  │   │            ├─ /dev/kvm  (acceleration)   │  │
  │   │            ├─ virtio disk → /storage     │  │
  │   │            ├─ virtio-net → bridge/macvlan │ │
  │   │            └─ VGA/PTY → noVNC :8006       │  │
  │   └──────────────────────────────────────────┘  │
  └───────────────────────────────────────────────┘
   Browser :8006 (install/view)   RDP client :3389 (daily use)
```

### 2.2 Boot orchestration (`src/entry.sh`)

`entry.sh` is the container entry point. It sets four defaults, then **sources** a fixed ordering of shell modules — most from the base image, the Windows-specific ones from this repo — before launching QEMU:

```bash
: "${APP:="Windows"}"
: "${PLATFORM:="x64"}"
: "${BOOT_MODE:="windows"}"
: "${SUPPORT:="https://github.com/dockur/windows"}"

. start.sh      # Startup hook            (base image)
. utils.sh      # Load helper functions   (base image)
. reset.sh      # Initialize system       (base image)
. server.sh     # Start web server        (base image)
. define.sh     # Define versions         (THIS repo)
. mido.sh       # Download Windows        (THIS repo)
. install.sh    # Run installation        (THIS repo)
. disk.sh       # Initialize disks        (base image)
. display.sh    # Initialize graphics     (base image)
. network.sh    # Initialize network      (base image)
. samba.sh      # Configure Samba         (THIS repo)
. boot.sh       # Configure boot          (base image)
. proc.sh       # Initialize processor    (base image)
. power.sh      # Configure shutdown      (THIS repo)
. memory.sh     # Check available memory  (base image)
. balloon.sh    # Memory ballooning       (base image)
. config.sh     # Assemble QEMU arguments (base image)
. finish.sh     # Finish initialization   (base image)
```

Each sourced script runs at source time (they are procedural, not function libraries), so the install pipeline in `install.sh` executes **as it is sourced** — before QEMU launches. After all modules load:

```bash
cmd=(qemu-system-x86_64)
info "Booting ${APP}${BOOT_DESC} using QEMU v$version..."

if [[ "$SHUTDOWN" != [Yy1]* ]]; then
  exec "${cmd[@]}" ${ARGS:+ $ARGS}
fi
```

When graceful shutdown is enabled (the default), QEMU is run in the background through a named pipe (`qemu.pipe`) so its serial output can be filtered with `sed` and tee'd to the PTY. A 30-second watchdog (`boot`) checks that the VM started, and on exit the `finish` handler (in `power.sh`) cleans up.

> [!IMPORTANT]
> Because base-image scripts are sourced by name with no path, the container's working directory is `cd /run` and all scripts (this repo's `src/` plus the base image's) are copied to `/run/`. This is set in the Dockerfile: `COPY --chmod=755 ./src /run/`.

### 2.3 The Windows-specific modules

| Module | Responsibility |
|---|---|
| `define.sh` | Normalizes the `VERSION` and `LANGUAGE` inputs; holds the version-ID tables, the language/culture map, edition naming, and the mirror URL/hash/size tables; sets per-version QEMU machine quirks (`setMachine`) |
| `mido.sh` | Acquires media: the Microsoft consumer/eval download APIs, the ESD catalog path, and the four numbered fallback mirrors; verifies checksums |
| `install.sh` | The install pipeline: detect custom ISO, download, extract, detect version/language, inject drivers + answer file, rebuild bootable ISO, persist settings |
| `samba.sh` | Configures and starts Samba (and wsdd/NetBIOS) to expose the host share inside Windows |
| `power.sh` | Traps signals and performs a graceful ACPI shutdown of the guest; the `finish` cleanup handler |
| `entry.sh` | Sources everything and launches QEMU |

---

## 3. The Installation Pipeline (Core Internals)

The heart of this project is `src/install.sh`. Its driver section runs this sequence (each step exits with a distinct code on failure — see [Section 7](#7-exit-codes)):

```text
parseVersion        normalize VERSION  → canonical id (e.g. "11" → "win11x64")
parseLanguage       normalize LANGUAGE → culture code
detectCustom        look for a user-supplied /custom.iso or /boot.iso
startInstall        decide whether install is needed; back up prior install
  └─ if already installed → bootWindows() (reload saved settings) and run
downloadImage       fetch ISO/ESD from MS or a mirror (mido.sh)
extractImage        unpack ISO (7z) or apply ESD (wimlib) into a work dir
detectImage         identify Windows version + language from install.wim
prepareImage        per-version boot setup (etfsboot/efisys or legacy boot.img)
updateImage         inject virtio drivers + the unattend XML answer file
removeImage         delete the source ISO (unless custom)
buildImage          rebuild a bootable ISO with genisoimage
finishInstall       persist marker + settings files into /storage
```

### 3.1 Version normalization (`define.sh`)

`parseVersion()` maps a wide set of human aliases to a canonical internal ID. For example, all of `11`, `11p`, `win11`, `pro11`, `windows 11` map to `win11x64`. Variant suffixes encode edition and channel:

| Suffix pattern | Meaning |
|---|---|
| `-enterprise-eval` | Enterprise Evaluation media |
| `-enterprise-iot-eval` | IoT Enterprise LTSC |
| `-enterprise-ltsc-eval` | LTSC channel |
| `-hv` | Hyper-V Server |
| `nano11` / `core11` / `tiny11` / `tiny10` | Community trimmed builds (with a `DETECTED` base for the answer file) |

A parallel `getLanguage()` table maps language inputs to a `short` code, display name, and BCP-47 `culture` (e.g. `de-DE`). `parseLanguage()` rejects anything not in the table (exit 56).

### 3.2 Media acquisition (`mido.sh` + `define.sh`)

The container tries multiple sources in order, controlled by `downloadImage()`:

1. **Custom URL** — if `VERSION` starts with `http`, download it directly (two attempts with a retry delay).
2. **Mido / Microsoft API** (`isMido` → `getWindows`):
   - **Consumer Win11** (`download_windows`): scrapes `microsoft.com/.../software-download/windows11`, obtains a product-edition ID, passes the Microsoft "session protection" handshake (`vlscppe.microsoft.com/tags`, `ov-df.microsoft.com`), then requests a SKU and a signed ISO link.
   - **Evaluation / Server** (`download_windows_eval`): parses the Microsoft Evaluation Center page and follows the `go.microsoft.com/fwlink` redirect.
3. **ESD catalog** (`isESD` → `getESD`): for Win10/11 Pro and Enterprise, downloads the Microsoft ESD product catalog, filters by architecture/edition/language with `xmllint` XPath, and extracts the ESD URL, SHA-1, and size.
4. **Fallback mirrors** (`getLink1`..`getLink4`, `MIRRORS=4`): static URL/hash/size tables pointing at `dl.bobpony.com`, `files.dog/MSDN`, `nixsys.com/drivers`, and `archive.org/download`. Used when the Microsoft path is unavailable or for legacy versions.

`verifyFile()` checks the downloaded file's SHA-256 (or SHA-1 for ESD) against the table when `VERIFY` is enabled, and warns on size mismatch.

> [!NOTE]
> The Microsoft download path deliberately mimics a real browser: `get_agent()` computes an approximate current Firefox version from the system clock, and the curl calls pin `--tlsv1.2 --http1.1` with the `Accept` header removed. This is required to pass Microsoft's automated-download protection.

### 3.3 Image extraction (`install.sh`)

`extractImage()` branches on file type:

- **`.esd`** → `extractESD()` uses `wimlib-imagex` to apply the install image, export WinPE + Setup into `boot.wim`, then export the selected edition into `install.wim` (LZMS-compressed). A free-space pre-check requires ~9.6 GB.
- **`.iso`** → `7z x` unpacks the ISO; the volume label is read with `isoinfo`. If `UNPACK=Y` (set when the download was a compressed archive), a nested ISO is extracted first.

### 3.4 Version & platform detection (`install.sh`)

`detectImage()` reads `install.wim`/`install.esd` metadata (`wimlib-imagex info -xml`, converted from UTF-16LE):

- `checkPlatform()` compares the image architecture (`ARCH` 0=x86, 9=x64, 12=arm64) against `PLATFORM`; a mismatch exits 67 ("You cannot boot ARM64 images on a x64 CPU").
- `detectVersion()` → `selectVersion()` maps `DISPLAYNAME`/`PRODUCTNAME`/`NAME` to an internal ID and prefers Enterprise → Ultimate → base edition that has a matching `assets/<id>.xml` answer file.
- `detectLanguage()` reads the default language from the WIM XML.
- The matching answer file is chosen by `setXML()`, which searches `/custom.xml`, `$STORAGE/custom.xml`, `/run/assets/custom.xml`, the per-version asset, then a base fallback. If none is found, the install falls back to **manual** mode.

### 3.5 Driver and answer-file injection (`install.sh`)

`updateImage()` modifies the extracted media before the ISO is rebuilt:

- `addDrivers()` extracts the bundled **virtio-win** driver archive (`/var/drivers.txz`, version pinned by the Dockerfile `VERSION_VIRTIO` ARG) and, via `addDriver()`, copies the per-OS driver set (qxl, viofs, NetKVM, viostor, vioscsi, Balloon, pvpanic, vioinput, viogpudo, vioserial, etc.) into `$WinPEDriver$` and the `$OEM$` tree using `wimlib-imagex update`.
- The selected `assets/<id>.xml` answer file is copied in and patched by `updateXML()` — resolution, UI/locale/keyboard culture, username, a base64-UTF16 encoded administrator password, server edition, and an optional product `KEY`.
- For **legacy** Windows (2000/XP/2003), `prepareInstall()` performs a very different flow: it edits `TXTSETUP.SIF` with `sed` to register viostor/iaStor SCSI/SATA drivers, writes a `WINNT.SIF` unattend file, an `install.reg`, an `install.vbs` (renames the built-in Administrator and creates a desktop share shortcut), and a `cmdlines.txt`. Generic, official Microsoft trial keys are embedded for XP/2003 only when no key is supplied.

### 3.6 ISO rebuild & persistence (`install.sh`)

`buildImage()` calls `genisoimage` with mode-specific flags:

- **UEFI** (non-legacy): El Torito + UDF, dual boot catalog (`etfsboot.com` + `efisys_noprompt.bin`).
- **Legacy** (2k/XP/2003, 9x, others): BIOS boot images with reduced ISO levels.

`finishInstall()` then writes a set of **marker/settings files** into `/storage` so the VM reboots with consistent hardware on subsequent runs. `bootWindows()` reads them back when an install already exists:

| File in `/storage` | Holds |
|---|---|
| `windows.boot` | Marker that installation completed |
| `windows.base` | Basename of the prepared ISO |
| `windows.ver` | Image build version (`/etc/version`) |
| `windows.mode` | `BOOT_MODE` (e.g. `windows_legacy`, `windows_secure`) |
| `windows.old` | Legacy QEMU `MACHINE` type |
| `windows.args` | Extra QEMU `ARGS` |
| `windows.vga` / `windows.usb` / `windows.net` / `windows.type` | Non-default VGA, USB controller, network adapter, disk type |

A magic first byte (`0x16`, or `0x17` for manual installs) is written into the ISO to mark it as already processed, so `skipInstall()` can short-circuit on later boots.

> [!IMPORTANT]
> Windows 11 unattended installs force `BOOT_MODE=windows_secure` (Secure Boot + TPM) when required, and multi-socket systems are also switched to secure boot to work around a known freeze. These decisions are persisted in `windows.mode`.

---

## 4. Configuration Reference

All configuration is via environment variables. Defaults marked **(Dockerfile)** are set as `ENV` in the image; the rest default inside the scripts.

### 4.1 Core variables

| Variable | Default | Source | Purpose |
|---|---|---|---|
| `VERSION` | `11` | Dockerfile | Windows edition or custom ISO URL |
| `RAM_SIZE` | `4G` | Dockerfile | Guest memory (handled by base image) |
| `CPU_CORES` | `2` | Dockerfile | Guest vCPUs (handled by base image) |
| `DISK_SIZE` | `64G` | Dockerfile | Main virtual disk size (base image) |
| `DISK2_SIZE` / `DISK3_SIZE` | (unset) | base image | Additional virtual disks |
| `USERNAME` | `Docker` | `install.sh` | Account created at install |
| `PASSWORD` | `admin` | `install.sh` | Password for that account |
| `LANGUAGE` | `en` | `define.sh` | Display language of downloaded edition |
| `REGION` | (culture) | `install.sh` | User locale (`UserLocale`/`SystemLocale`) |
| `KEYBOARD` | (culture) | `install.sh` | Input locale |
| `KEY` | (unset) | `install.sh` | Product key injected into the answer file |
| `EDITION` | (unset) | `define.sh` | Server edition (`Standard`/`Datacenter`/`Core`) |

### 4.2 Install behavior

| Variable | Default | Purpose |
|---|---|---|
| `MANUAL` | (unset) | `Y` skips the unattended answer file; manual Setup |
| `VERIFY` | (unset) | `Y` verifies the downloaded ISO checksum |
| `REMOVE` | (unset) | `N` keeps the installation ISO after setup |
| `WIDTH` | `1280` | Display width baked into legacy answer files |
| `HEIGHT` | `720` | Display height baked into legacy answer files |
| `ARGUMENTS` | (unset) | Raw extra QEMU arguments (escape hatch) |

### 4.3 Networking

| Variable | Default | Purpose |
|---|---|---|
| `DHCP` | (unset) | `Y` gives Windows its own router-assigned IP (needs `/dev/vhost-net`) |
| `ADAPTER` | (per version) | Guest NIC model; persisted in `windows.net` |
| `NETWORK` | (base image) | Networking backend (`bridge`, `macvlan`, `passt`, `slirp`) |

### 4.4 File sharing (Samba)

| Variable | Default | Purpose |
|---|---|---|
| `SAMBA` | `Y` | Enable the Samba host share |
| `SAMBA_DEBUG` | `N` | Stream Samba/wsdd logs |
| `SAMBA_INTERFACE` | (unset) | Extra interface to bind Samba to |

### 4.5 Shutdown

| Variable | Default | Purpose |
|---|---|---|
| `SHUTDOWN` | `Y` | Enable graceful ACPI shutdown trapping |
| `TIMEOUT` | `115` | Seconds to wait for clean guest shutdown |
| `QEMU_TIMEOUT` | (unset) | Overrides `TIMEOUT` if set |
| `DEBUG` | (unset) | Verbose pipeline logging |

### 4.6 Volumes, devices, ports

| Mount / device | Role |
|---|---|
| `/storage` | Virtual disk + marker files (declared `VOLUME` in Dockerfile) |
| `/storage2`, `/storage3` | Backing for `DISK2_SIZE`/`DISK3_SIZE` |
| `/shared`, `/shared2`, `/shared3` (or `/data`, `/data2`, `/data3`) | Host folders exposed inside Windows as `Data`/`Data2`/`Data3` |
| `/oem` | Copied to `C:\OEM`; `install.bat` runs at end of setup |
| `/boot.iso` or `/custom.iso` | Bind a local ISO to install instead of downloading |
| `/custom.xml` | Override the unattended answer file |
| `/disk1` (device) | Pass-through main disk (formatted) |
| `/disk2`+ (device) | Pass-through secondary disks (untouched) |
| `/dev/kvm` | Required — KVM acceleration |
| `/dev/net/tun` | Required — container networking |
| `/dev/vhost-net` | Required only for `DHCP=Y` |
| Port `8006/tcp` | Web viewer (noVNC) |
| Port `3389/tcp+udp` | RDP |
| Port `5900/tcp` | VNC (exposed in the Kubernetes manifest) |

---

## 5. Deployment

### 5.1 Docker Compose (recommended)

The canonical `compose.yml` is shown in the User Guide. Key production notes:

- Always bind `/storage` to a host path or named volume.
- Keep `stop_grace_period: 2m` so the ACPI shutdown completes.
- `restart: always` brings the VM back after host reboots.

### 5.2 Kubernetes

The repo ships a complete `kubernetes.yml` with a `PersistentVolumeClaim` (64Gi), a `Deployment`, and a `Service`:

```yaml
securityContext:
  capabilities:
    add:
    - NET_ADMIN
  privileged: true
```

`/dev/kvm` and `/dev/net/tun` are mounted from the host via `hostPath: { type: CharDevice }`, and the Service exposes ports `8006` (http), `3389` (rdp tcp+udp), and `5900` (vnc).

> [!CAUTION]
> The Kubernetes manifest runs the pod **`privileged: true`**. This is the simplest way to grant device access but gives the pod broad host privileges. In multi-tenant clusters, replace it with a tightly scoped device plugin and the minimum capabilities, and schedule only onto nodes with `/dev/kvm`.

### 5.3 Other launch paths

The README also documents GitHub Codespaces and the third-party [WinBoat](https://winboat.app) graphical installer as front ends for the same image.

---

## 6. Operations

### 6.1 Startup and viewing

```bash
docker compose up -d
docker compose logs -f      # watch the install pipeline
```

Expected milestones in the log:

```text
❯ Requesting Windows 11 Pro from the Microsoft servers...
❯ Downloading Windows 11 Pro...
❯ Extracting downloaded ISO image...
❯ Adding drivers to image...
❯ Adding autounattend.xml for automatic installation...
❯ Building Windows 11 Pro image...
❯ Booting Windows using QEMU v9.x.x...
❯ Windows started successfully, visit http://127.0.0.1:8006/ to view the screen...
```

### 6.2 Graceful shutdown (`power.sh`)

`power.sh` traps `SIGTERM`, `SIGHUP`, `SIGABRT`, and `SIGQUIT`. On signal, `graceful_shutdown()`:

1. Verifies the guest is past Setup (`ready()` — checks the PTY for the Windows Boot Manager line, or legacy "Booting from Hard").
2. Sends `system_powerdown` over the QEMU monitor socket (`$QEMU_DIR/monitor.sock`) once per second.
3. Escalates to `SIGTERM` near the end of the timeout window, then `kill -9` in `finish()` if still alive.
4. `finish()` removes the post-install CD-ROM ISO (unless `REMOVE=N`), kills helper daemons (Samba, wsdd, TPM, web, passt, dnsmasq, ballooning), and closes networking.

The timeout window is derived from `TIMEOUT` (minimum enforced; default 115 s), which is why `stop_grace_period`/`--stop-timeout` on the host must be at least as long.

> [!WARNING]
> If the host kills the container before the ACPI shutdown completes, Windows may mark the filesystem dirty and run `chkdsk` (or lose unsaved writes) on next boot. Keep the host stop timeout ≥ `TIMEOUT`.

### 6.3 Backups

`/storage` holds the entire machine. To back up, stop the container gracefully and copy the `/storage` folder. `install.sh` also auto-creates `/storage/backups/<name>` when it detects that `VERSION` or a custom ISO changed, preserving the previous `windows.*` data files before re-installing.

### 6.4 File sharing internals (`samba.sh`)

`samba.sh` writes `/etc/samba/smb.conf` with a guest-only, `force user = root` configuration (`map to guest = Bad User`, `server min protocol = NT1` for legacy compatibility) and exposes the chosen share directory. It starts `smbd`, plus either `nmbd` (NetBIOS, for Windows 7 and older in legacy boot mode) or `wsddn` (Web Service Discovery, for Vista and newer). The share auto-selects `/shared` → `$STORAGE/shared` → `/data` → `$STORAGE/data` → a temp folder, and is reachable inside Windows at `\\host.lan\Data`.

> [!NOTE]
> `server min protocol = NT1` (SMB1) is enabled to support legacy guests. SMB1 is deprecated and insecure on real networks — acceptable here because the share is on an internal container interface, not exposed to the host LAN. Do not bridge this Samba instance onto a production network.

---

## 7. Exit Codes

`install.sh` uses distinct exit codes, which are valuable for diagnosing failed installs from container logs:

| Code | Stage | Meaning |
|---|---|---|
| 50 | startInstall | Could not create the temp working directory |
| 56 | parseLanguage | Invalid `LANGUAGE` value |
| 58 | parseVersion | (version normalization failure) |
| 59 | detectCustom | Bind-mount maps to a non-existent file |
| 60 | abort / esd / unpack | Aborted to manual install, or ESD/unpack path ended |
| 61 | downloadImage | Could not download the ISO/ESD |
| 62 | extractImage | Could not extract the image |
| 63 | updateImage | Driver/answer-file injection failed |
| 64 | removeImage | Could not remove the source ISO |
| 65 | buildImage | `genisoimage` rebuild failed |
| 66 | prepareImage | Boot-image preparation failed |
| 67 | checkPlatform / custom.xml | Wrong CPU architecture, or `/custom.xml` bind missing |
| 68 | startInstall + bootWindows | Neither install nor existing-boot path succeeded |
| 69 | finishInstall | Could not persist install markers |

`mido.sh` additionally maps every `curl` exit status to a descriptive message via `handle_curl_error()` (DNS failure, TLS error, timeout, IP-based block, etc.).

---

## 8. Security Analysis

| Area | Finding |
|---|---|
| **Privilege** | Requires `NET_ADMIN` (and often `privileged`/device access). The container hosts a VM with raw device access — treat it as a privileged workload. |
| **Default credentials** | `Docker` / `admin` are well-known defaults. Always override `USERNAME`/`PASSWORD`, and never expose port `3389` publicly. |
| **RDP exposure** | Exposed RDP invites brute-force and account lockouts (community issue: locked accounts). Keep `3389` on a private network or behind a VPN. |
| **Samba** | Guest-only, `force user = root`, SMB1 enabled. Safe on the internal container interface; never reuse this config on a real LAN. |
| **Product keys** | Keys in `define.sh`/`install.sh` are official Microsoft generic trial/placeholder keys (no activation). The project distributes no copyrighted media. |
| **Outbound network** | The image makes outbound HTTPS calls to Microsoft and mirror hosts to fetch media. In locked-down environments, pre-stage a local ISO via `/boot.iso` and avoid the download path entirely. |
| **Browser-like fetch** | The Microsoft download handshake spoofs a Firefox user agent and replays Microsoft's anti-automation tokens. This is benign (it only retrieves the publicly available ISO) but will look anomalous to network monitoring. |
| **macvlan isolation** | With macvlan, the host cannot reach the container — a containment benefit, but it also means host-based monitoring won't see the guest's traffic. |

> [!IMPORTANT]
> The single most important hardening step is to **change the default password and keep RDP off the public internet.** The default `Docker`/`admin` pair combined with an exposed `3389` is the most common real-world exposure for this image.

---

## 9. Build & Dependencies

### 9.1 Image build (`Dockerfile`)

```dockerfile
FROM scratch AS build-amd64
COPY --from=qemux/qemu:7.32 / /

RUN apt-get install samba wimtools dos2unix cabextract libxml2-utils libarchive-tools
    # + wsdd-native (VERSION_WSDD=1.24) installed from a .deb

COPY --chmod=755 ./src /run/
COPY --chmod=755 ./assets /run/assets
ADD https://github.com/qemus/virtiso-whql/releases/.../virtio-win-${VERSION_VIRTIO}.tar.xz /var/drivers.txz

VOLUME /storage
EXPOSE 3389 8006
ENV VERSION="11" RAM_SIZE="4G" CPU_CORES="2" DISK_SIZE="64G"
ENTRYPOINT ["/usr/bin/tini", "-s", "/run/entry.sh"]
```

| Build ARG / dependency | Pin | Role |
|---|---|---|
| `qemux/qemu` | `7.32` | Base VM runtime image |
| `VERSION_VIRTIO` | `1.9.57` | virtio-win WHQL driver bundle (`/var/drivers.txz`) |
| `VERSION_WSDD` | `1.24` | `wsdd-native` for Web Service Discovery |
| `samba` | apt | SMB host file sharing |
| `wimtools` (`wimlib-imagex`) | apt | WIM/ESD image manipulation |
| `cabextract` | apt | Extract Microsoft `.cab` catalogs |
| `libxml2-utils` (`xmllint`) | apt | XPath over the ESD product catalog |
| `libarchive-tools` (`bsdtar`) | apt | Extract the driver archive |
| `dos2unix` (`unix2dos`) | apt | CRLF conversion for answer/registry files |

The runtime tools `7z`, `genisoimage`, `isoinfo`, `wget`, `curl`, `jq`, and `nc` come from the base image.

### 9.2 The `assets/` directory

`assets/*.xml` holds **per-version unattended answer files** (`win11x64.xml`, `win10x64-enterprise-eval.xml`, …) — 40 templates spanning Vista through Server 2025. `updateXML()` patches these at runtime; `detectImage()`/`setXML()` select the right one.

---

## 10. Code Quality & Conventions

- **Strict Bash everywhere:** every script begins `set -Eeuo pipefail`. Failures propagate, and the install pipeline maps each to an exit code.
- **Defensive defaults:** variables use `: "${VAR:=""}"` parameter expansion so the scripts run cleanly under `nounset`.
- **Procedural source modules:** scripts are sourced (not exec'd) and run top-to-bottom at source time — order in `entry.sh` is significant.
- **Table-driven logic:** version IDs, languages, editions, and the four mirror sets are all `case` tables in `define.sh`, making them easy to audit and extend.
- **Heavy reliance on external CLIs:** the pipeline orchestrates `wimlib-imagex`, `genisoimage`, `7z`, `xmllint`, `sed`, `curl`/`wget`. Behavior is sensitive to those tools' versions (pinned via the base image).

---

## 11. Potential Improvements

These are observations for maintainers and forkers, derived from the source and community issues. They are not defects in normal operation.

- [ ] **`NET_RAW` discoverability** — RDP failing without `NET_RAW` on some hosts (community issue) is not surfaced in the default error path. A startup probe that detects the missing capability and prints a hint would cut support load.
- [ ] **`passt` backend stability** — a reported `passt.avx2` `SIGABRT` causes guest network loss on certain CPUs. Auto-falling back to bridge networking on a passt crash would harden the default path.
- [ ] **SMB1 default** — `server min protocol = NT1` is enabled unconditionally for legacy guests. Gating SMB1 behind a flag (or per-version) would reduce the attack surface for modern-only deployments.
- [ ] **Privileged Kubernetes manifest** — shipping a least-privilege variant (device plugin + dropped `privileged: true`) alongside the current manifest would suit hardened clusters.
- [ ] **Download-path observability** — the Microsoft handshake has many failure points; a `DEBUG`-independent structured log of which acquisition method (Mido/eval/ESD/mirror N) ultimately succeeded would aid air-gapped planning.

---

## 12. Quick Reference

```bash
# Start / stop
docker compose up -d
docker compose down                 # triggers graceful ACPI shutdown

# Logs & status
docker compose logs -f              # watch install pipeline
docker exec windows cat /etc/version

# Common overrides (compose environment:)
VERSION: "10"        # edition or ISO URL
RAM_SIZE: "8G"
CPU_CORES: "4"
DISK_SIZE: "256G"
USERNAME: "admin"; PASSWORD: "<strong>"
MANUAL: "Y"          # manual Setup
DHCP: "Y"            # own LAN IP (needs /dev/vhost-net)
VERIFY: "Y"          # checksum the download

# Required runtime
--device=/dev/kvm --device=/dev/net/tun --cap-add NET_ADMIN
-p 8006:8006 -p 3389:3389/tcp -p 3389:3389/udp
-v ./windows:/storage
```

| Connect | Address | Notes |
|---|---|---|
| Web viewer | `http://127.0.0.1:8006/` | Install/quick view; low quality, no audio/clipboard |
| RDP | container IP `:3389` | Full quality; `Docker`/`admin` by default |

---

_Generated June 2026 from dockur/windows at 0c9472b. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
