- PowerShell 48%
- Shell 37.6%
- Dockerfile 14.4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .claude | ||
| .forgejo/workflows | ||
| scripts | ||
| .dockerignore | ||
| .gitignore | ||
| AGENTS.md | ||
| Dockerfile | ||
| README.md | ||
wine-machine
Container image that runs Windows programs under Wine and takes on the identity of an existing Windows machine: computer name, user, MachineGuid, Windows build, volume serial, time zone and locale. RDP gives you an Xfce desktop. It's built for Kubernetes, but any container runtime works.
The image is generic. The identity is collected once from the Windows machine and mounted at runtime, so it never ends up in the image.
Built on scottyhardy/docker-wine (Ubuntu, WineHQ Wine, Xfce, xrdp), pinned by tag and digest. linux/amd64 only, on native x86_64 nodes. 32-bit Windows programs need real x86 hardware; QEMU user mode and Rosetta for Linux can't run them.
Dockerfile test stage (shellcheck) + image
scripts/wine-machine-entrypoint.sh root: load /opt/identity, generate locale, checks, exec docker-wine entrypoint
scripts/wine-machine-init.sh user: create prefix (first start), import identity.reg (every start), clean shutdown
scripts/collect-identity.ps1 runs on the Windows machine, writes the identity files
.forgejo/workflows/ci.yaml lint + build on push/PR, publish on release
Collecting the identity
On the Windows machine (Windows PowerShell 5.1, no admin rights):
powershell -ExecutionPolicy Bypass -File .\collect-identity.ps1
It writes Desktop\wine-identity\:
| File | Content |
|---|---|
identity.env |
WIN_HOSTNAME, WIN_USER, WIN_MAC, VOLUME_SERIAL, VOLUME_LABEL, WIN_TZ, WIN_LOCALE, WIN_SCREEN. UTF-8, LF, KEY=value. |
identity.reg |
MachineGuid and CurrentVersion values (edition, build, ProductId, registered owner). UTF-16 LE with BOM; keep it as written. |
installed-apps.csv |
Installed programs, to plan what to reinstall. Not used by the image. |
Handle its warnings in identity.env: an empty WIN_TZ needs an IANA name (e.g. Europe/Paris), and WIN_USER must be a valid Linux user name.
These files fingerprint the machine; license systems use the same values. Keep them out of public repos and images (.gitignore covers identity/).
Runtime contract
/opt/identity mount |
Required. identity.env and identity.reg, readable by UID 1000. On Kubernetes, use a Secret (identity.reg is binary UTF-16, and it's a fingerprint): kubectl create secret generic wine-identity --from-file=identity.env --from-file=identity.reg. |
USER_PASSWD env |
Required. RDP password as a SHA-512 crypt hash (openssl passwd -6), from a Secret. The container refuses to start without it. |
Volume at /home/<WIN_USER> |
Required, persistent, RWO. Holds the Wine prefix (.wine: registry, C:, installed programs, activations) and the D: drive (shared/). This is the machine's state. |
Hostname = WIN_HOSTNAME |
Pod spec.hostname. Wine uses it as the computer name (15 chars max). The entrypoint logs a warning on mismatch. |
Port 3389/tcp |
RDP. Keep it off the public internet. |
| Grace period ≥ 45 s | terminationGracePeriodSeconds. Shutdown needs up to 30 s to flush the registry; the Kubernetes default (30 s) is too short. |
| One replica | strategy: Recreate. Two writers corrupt the registry, and two copies of one identity confuse license servers. |
| Root at start | The base entrypoint creates the user and chowns the home as root, then drops to UID 1000. No runAsNonRoot, no read-only root filesystem. |
| amd64 node | nodeSelector: kubernetes.io/arch: amd64. |
Optional overrides: TZ (default from identity.env), USER_NAME (changes the home path). The locale from WIN_LOCALE is generated at each start (about 1 s).
Not applied by the image: WIN_MAC. A pod MAC address needs CNI support, so the deployment handles it if needed.
Expected logs: First run: creating the Wine prefix (first start only), Applying identity.reg, Ready: connect an RDP client. On stop: Stopping: closing Windows programs and saving the registry....
Verify the identity:
kubectl exec <pod> -- gosu <user> env HOME=/home/<user> wine cmd /c "hostname & echo %USERNAME% & ver & vol c:"
kubectl exec <pod> -- gosu <user> env HOME=/home/<user> wine reg query "HKLM\SOFTWARE\Microsoft\Cryptography" /v MachineGuid
Programs are installed over RDP, onto the volume. Put installers in /home/<user>/shared (D:). Runtimes: winetricks -q corefonts vcrun2022 dotnet48 as the user.
Building and releasing
docker build --target test . # shellcheck
docker build --platform linux/amd64 -t wine-machine:dev .
Smoke test on an x86_64 Linux host, with the identity files in ./identity:
. identity/identity.env
docker run --rm -it --hostname "$WIN_HOSTNAME" -e USER_PASSWD="$(openssl passwd -6)" \
-v "$PWD/identity:/opt/identity:ro" -v "$PWD/home:/home/$WIN_USER" \
-p 127.0.0.1:3389:3389 wine-machine:dev
CI (Forgejo Actions) runs the lint stage and an amd64 build on every push and PR. Publishing a release vX.Y.Z pushes <registry>/<owner>/wine-machine:X.Y.Z, and also :latest when the version is plain X.Y.Z and not a prerelease. It needs a REGISTRY_TOKEN secret (registry write) for publishing and the build cache.
Wine version: update the tag and digest in the Dockerfile's FROM (docker buildx imagetools inspect scottyhardy/docker-wine:<tag>), then release. Snapshot the volume first: Wine upgrades the prefix on first start. The identity is re-applied afterwards.
Limits
Not reproducible: user SID, CPU/RAM/GPU, SMBIOS UUID, disk serials, domain membership, Windows activation. Licensed programs may need reactivation once (the activation then lives on the volume). No kernel drivers, no GPU acceleration. This project reproduces OS-level identity only; it isn't a way around software licensing.