Some work still runs better on Windows: a vendor's desktop app, a Windows-only browser extension, a file format that only one program opens properly. If your daily workstation already lives on a rented Linux server, renting a separate cloud Windows desktop for those tasks means a second bill, a second machine to patch and a second place your files end up.
This post describes the alternative we put into service on October 14, 2026: a Windows 11 virtual machine on the same Linux server, opened in an ordinary browser tab, behind the same zero-trust login as everything else, with the server's backups kept in existing Google Drive storage instead of a new storage bill. It is the next step after The Sovereign AI Playbook, and it is written so you can repeat it on your own hardware.
Download the Windows Desktop and Drive Backup Guide in Markdown. It has the Compose files, the database setup, the Cloudflare steps, the backup commands and the checks. Read it before running anything on a server that holds data.
What the finished setup looks like
Scroll to compare columns and rows when the table extends beyond the box.
| Piece | Software | Job |
|---|---|---|
| Host | A bare-metal Linux server with KVM | Runs everything; no separate Windows host |
| Windows | dockur/windows (QEMU/KVM in a container) | Downloads Windows 11 from Microsoft, installs it unattended, keeps a thin virtual disk |
| Browser access | Apache Guacamole | Turns Windows Remote Desktop into an HTML5 session |
| Front door | Cloudflare Tunnel and Cloudflare Access | One private hostname, email sign-in, no open inbound ports |
| Way back in | The provider's IPMI/KVM console | Keyboard and screen when the operating system or network is down |
| Backups | restic and rclone to Google Drive | Encrypted history off the server; only the newest restore point stays local |
Nothing in that table needs a new subscription if you already rent the server and pay for Google storage. The Windows license is the one new cost: an unactivated install runs, but activation needs a valid Windows 11 license.
Check the hardware before you plan
A Windows guest needs hardware virtualization. On the server, lscpu should report VT-x or AMD-V, /dev/kvm must exist, and systemd-detect-virt should print none. If it prints anything else, you are already inside a virtual machine and nested virtualization may be slow or unavailable.
Our reference host is the dual Xeon Silver 4214R, 96 GB, mirrored NVMe server from the earlier posts. We gave Windows 16 cores, 32 GB of memory and a 150 GB disk. The guest holds its full memory allocation while it runs, so budget the RAM as spent. During first-boot setup the VM used about three cores. The rest of the server kept 43 GB of memory available.
There is no graphics card in most rented servers. Windows draws its screen in software. Office work, browsers and admin tools are fine. Video, 3D and fast-scrolling dashboards will never feel like a local PC.
Check one more thing before you depend on the server: its out-of-band console. Our SYS-5 is in OVHcloud's Eco line, where OVHcloud's IPMI guide warns the feature "might be unavailable or limited". On ours, the IPMI/KVM tab was present with a browser KVM, a Java fallback and Serial over LAN. Open that tab on your own server rather than assuming.
Windows 11 in a container, without a hypervisor console
dockur/windows wraps QEMU and KVM in a Docker container. Set VERSION: "11" and it requests Windows 11 from Microsoft's servers, builds an unattended answer file and installs Windows without you clicking through setup. Our run took about 23 minutes from container start to a Windows desktop, including an 8.5 GB download. The same image also runs under Podman; Podman or Docker? compares the two engines.
Four settings matter more than the rest:
- Pin the image by digest, not just a tag. A tag can move; a digest cannot.
- Keep the disk thin.
ALLOCATE: "N"creates a sparse image. Check it: our 150 GB disk showed 150 GB apparent size and about 2.5 GB of real usage when the install began. - Bind every port to
127.0.0.1. The container's web viewer (port 8006) and Remote Desktop (3389) should never listen on a public address. - Give Windows time to shut down.
stop_grace_period: 2mlets the guest close cleanly when the container stops.
The container image also changed its networking in recent releases. The VM's internal address in version 6.06 was 172.30.0.2, not the 20.20.20.21 older guides mention. The container records the current address in /run/shm/qemu.ip. Read it instead of hard-coding one.
Treat the first password as a secret, even on your own server
The unattended install needs the Windows account password up front. Three habits kept it contained:
- Put it in its own file, readable only by you, and load it with Compose's
format: rawso a$in the password is not treated as a variable. That option arrived in Docker Compose 2.30. - Verify it by hash, never by printing it. Compare a SHA-256 of the value inside the container with one of the file. Our first comparison falsely failed because
printenvadds a newline; strip it before hashing. - Delete the file after Windows reaches the desktop. The running container still holds the value in its environment until it is recreated, so recreate it once you are not in a session.
dockur also turns on automatic sign-in by default. That makes the raw web viewer an open desktop to anyone who can reach port 8006. Keep it on loopback, and use it only through an SSH tunnel as a recovery console.
Publish one hostname, and protect it twice
We already reach the server's other tools through a Cloudflare Tunnel, so Windows got one more hostname on the same tunnel. The order matters:
- Add the hostname to an Access application first. One self-hosted Access app can cover several hostnames with one reusable policy, here an allow-list of two email addresses with a 24-hour session. Cloudflare's Zero Trust free plan covers teams under 50 users.
- Then add the tunnel route, with Protect with Access turned on. cloudflared then checks the Access token on every request at the server itself, so traffic that somehow skipped the Access check is still refused. Cloudflare's documentation recommends validating that token at the origin.
- Let the dashboard create the DNS record, then test from outside.
curlthe page and the session's WebSocket path. Both should return a 302 to your team'scloudflareaccess.comsign-in page. A WebSocket path that answers directly is a hole.
VNC is where the lag comes from; RDP fixes most of it
The first working version used the container's built-in web viewer. It works, but it is VNC: a stream of screen images with no knowledge of what Windows is drawing. Over the internet it felt slow.
Apache Guacamole speaks Windows Remote Desktop to the VM and serves the session to the browser. It ran as three containers: guacd, the web application, and PostgreSQL. guacd joins the Windows container's Docker network and connects to it by name. For responsiveness, the connection turns off wallpaper, desktop composition, menu animations and full-window drag, uses 24-bit color, and resizes Windows to the browser window.
Two decisions keep it simple and safe:
- No stored Windows password. The connection names the account and leaves the password blank, so Guacamole asks for it when a session starts. The next section explains when you might change that.
- No second login screen. Guacamole's header authentication trusts the
Cf-Access-Authenticated-User-Emailheader that Access adds. That is acceptable here because the web application listens only on loopback, cloudflared enforces the Access token, and everyone Access admits is the same owner. A header is only a claim. If several people with different rights will use it, use Guacamole's OpenID Connect support instead. Either way, delete the stockguacadminaccount and grant the Windows connection only to named users. Our test with an unknown email signed in to an empty account that could see nothing; we deleted it.
We moved the tunnel route from the VNC viewer to Guacamole and kept the VNC viewer on loopback as a recovery console. The route change needed no Windows restart.
Why a browser desktop drops, and how to make it painless
On the first afternoon of real use, the desktop disconnected about a dozen times, after anywhere from one to seventeen minutes. Windows itself was fine. The logs on both sides of Guacamole told the same story: the browser had stopped answering.
Guacamole gives up when the stream goes quiet for 15 seconds. Its web application waits 15 seconds for data and then closes the connection. That number is a constant in its source code, not a setting. A hidden browser tab is a common trigger: Chrome runs a hidden page's timers once per second, and after five minutes hidden and 30 seconds silent, as rarely as once per minute. A closing laptop lid, a phone screen turning off or a short network stall has the same effect.
You cannot configure the 15 seconds away, so make a drop cheap:
- Give the desktop its own browser window and keep it visible. A window you can see is not throttled the way a background tab is.
- Lengthen Guacamole's own sign-in. Its session tokens expire after 60 minutes of inactivity by default. The Docker image reads any property from an environment variable, so
API_SESSION_TIMEOUT: "300"gives five hours. - Keep the Windows session alive between connections. A dropped Remote Desktop connection leaves the Windows session running, so reconnecting brings back the same open apps, unless a session time limit ends it. Leave those limits at "never", and turn off sleep and the display-off timer. For a safety net, set the machine inactivity limit: ours locks Windows after four hours without input. Microsoft notes that this limit also locks the session when the display turns off, and that it takes effect after a restart.
- Decide whether a reconnect should ask for the Windows password. With no stored password, every drop ends at a password prompt. Storing it in the Guacamole connection makes a reconnect seamless. The cost: anyone who gets past Access gets the desktop without the Windows password, and the password sits in Guacamole's database. With Access limited to one owner's sign-in, that can be a fair trade. With more people, keep the prompt.
Let an assistant work inside Windows
The Desktop Commander Remote Device runs inside Windows as well as on Linux. Install a pinned Node.js and a pinned Desktop Commander version for the Windows account, and pair it once in a browser. Then start it hidden at every sign-in, with a small loop that restarts it if it exits. Remember what the Remote Device is: tool calls travel through Desktop Commander's relay service and run with that account's permissions.
For automation from the server itself, Windows' own OpenSSH Server is the simpler bridge. Allow one key, turn password logins off, and let the firewall accept only the server's private Docker network. dockur forwards the container's ports to Windows, so the server reaches Windows' SSH without any port being published.
Move files in, and retire the old machine
The Windows desktop was meant to replace an older cloud Windows machine, so its first job was receiving that machine's files. Five practices kept the move safe:
- Use a shared folder for bulk copies. dockur shares a host folder with Windows over Samba as
\\host.lan\Data; release 6.06's setup also mapped it toZ:and put a Shared shortcut on the desktop. Samba listens only on the VM's internal network. Copy into the host folder over SSH, then into Windows fromZ:. - Send one archive per set, hashed at both ends. Stream
tarover SSH, hash the stream locally as it leaves and hash the file again once it lands. A set passes only when the two hashes match. Windows has a built-intarto unpack it. - Leave sign-ins behind. Do not copy command-line tool tokens, SSH private keys, API-key files or password notes. Sign in again on the new machine, then revoke the old machine's sessions.
- Skip what can be rebuilt, but check before skipping. Dependency folders and generated pages dominated the time: one build folder of about 48,000 small files moved at under half a megabyte per second. Before skipping a project because "it's on GitHub", look for unpushed work. Ours turned up three commits from 18 days earlier that were on neither GitHub nor the live site. A git bundle preserved them, and they went to GitHub ahead of the site's next scheduled deploy.
- Keep a final copy of the old machine on Drive. Upload the verified archives with
rclone copy, then confirm thatrclone checkreports zero differences. Only then retire the old machine.
Keep a way in when everything else is down
Tunnels, Access and SSH all depend on the operating system and network being healthy. The provider's IPMI/KVM console does not. Put three links somewhere private you will find in a bad moment, such as a link directory behind the same Access login:
- the server's IPMI/KVM page in the provider control panel,
- the provider's support-ticket page,
- the Windows desktop and its loopback recovery console.
Set a local console password before you need it. The KVM console is a physical-style login; SSH keys do not work there.
Back up to the Google Drive you already pay for
If you pay for Google One or Google Workspace storage, much of it may be empty. Our account showed about 5 TB free. That can hold encrypted server backups without opening a new storage account, provided you respect how Drive behaves.
Use restic for the backup and rclone for the transport. restic encrypts and deduplicates on the server, so Drive only ever holds ciphertext. rclone gives restic a Drive backend.
Give rclone the narrowest scope. With the drive.file scope, rclone can see only the files it created, not the rest of your Drive. The same page notes that rclone's shared Google client ID is being retired during 2026. Create your own OAuth client now rather than discovering it during a restore.
Keep the history on Drive and only the newest restore point locally. This was the most important lesson. A local repository mirrored to Drive with sync semantics will copy a local prune to Drive and erase the history you meant to keep. Instead, back up to a fast local repository, copy each new snapshot to a separate Drive-backed repository, then apply retention separately: keep 7 daily, 5 weekly and 12 monthly snapshots on Drive and keep the last one locally. The local copy is your immediate rollback. Drive is your history.
Your own OAuth client is also the biggest speed setting. In the field observation reported in Sovereign AI Made Simple, a Drive copy went from under 1 MiB/s on the shared client to a sampled rate near 27 MiB/s on an own client, and a 19.5 GiB first copy finished in about 11 minutes. Do not throttle a healthy copy to a fixed low request rate; let rclone back off when Google says to, and raise parallel transfers only after measuring. Name the rclone configuration file explicitly on every run.
Respect Drive's limits. Google documents an upload and copy limit of 750 GB per user per day. A first upload larger than that takes more than a day. Deleted files sit in Drive's trash and still count against your storage. Consider a larger restic pack size so the repository creates fewer, larger files.
Verify, then trust. After each upload, run rclone check for plain archives and restic check --read-data-subset through the Drive backend for repositories. Schedule a real restore test. A backup you have not restored is a hope.
Keep the restic password off the server. Store it in a password manager. If the server is lost and the only copy of the password was on it, the encrypted Drive copy cannot be restored.
Database services deserve their own dump: a consistent export uploaded the same way, with only the newest export kept locally. A file-level copy of a running database is not a consistent backup.
Other pieces worth reusing
The same parts do more than serve one Windows desktop. The guide has a dependency table and the settings for each of these:
- Guacamole as one gateway. The same login can list SSH connections to Linux servers, VNC connections to other desktops (view-only if you like) and more Remote Desktop machines. It can add file transfer, PDF printing, clipboard limits, session recording and an authenticator-app second factor. Leave each one off until you need it.
- The QEMU monitor as a headless control panel. It can take a screenshot during setup, ask Windows for a clean shutdown or send Ctrl+Alt+Delete. A qcow2 disk adds snapshots; with a raw disk, a sparse copy of the stopped image is your rollback point.
- VNC as the recovery console. It shows the boot screen and updates before Remote Desktop starts. Keep it on loopback and reach it through SSH.
- Patterns from the same rollout. A private link directory, one tunnel with many protected hostnames and Drive backups for a serverless business app. Also: deploy helpers that cannot hide a failed step, a secret-scanned private GitHub mirror, IndexNow for static sites, and headless agent jobs that always leave a result.
Clean up before you add a large disk
A thin disk grows into whatever space you leave it. Before adding Windows, a cleanup job took inventory first, wrote it down and only then acted. It freed 139 GB of unused Docker build cache and 24 images no container used, and kept every image a container or rollback record referenced. Backups followed one rule: upload, verify, then delete the local copy. The server went from 410 GB to 533 GB free.
The checks that caught real mistakes
- A deploy script that "passed" had failed. The log ended
EXIT 1, but the wrapper reported success because the last command wastail. Record the real exit code and read it. - A transient API error looked like a missing table. The same check had passed seconds earlier. Retry reads before deciding a migration failed, and confirm state directly before re-running anything.
- A test can be wrong in the same direction as the bug. The newline in the password hash check, and the old VM address in the install watcher, would both have sent us down the wrong path if we had trusted the first result.
- The new VM disk was about to join the nightly backup. The plan said to exclude it, but the server's real exclude file did not. A 150 GB live disk image would have gone into that night's file backup: large, and not consistent while Windows runs. After adding a VM, read the actual exclude file.
- "It's on GitHub" was not the whole story. A project we planned to skip had unpushed commits that the live site lacked too. Check
git log --branches --not --remotesbefore treating any local copy as disposable.
Fact-check notes and sources
- Reference build and timings: private operating records from October 14, 2026, sanitized here. Hardware and resource figures were measured on the reference server during and after installation.
- Windows container: dockur/windows. The
ALLOCATE,DISK_FMT, account andVERSIONbehavior described here was read from the version 6.06 image's own scripts. - Compose raw env files: Docker Compose environment variables.
- Guacamole: Docker installation and HTTP header authentication, which warns that a front proxy must not let clients supply the header themselves. Session timeout and environment properties: Configuring Guacamole. The 15-second read timeout:
InetGuacamoleSocket.java. The disconnect count and durations come from the reference server's logs on October 14, 2026. - Browser and Windows behavior: Chrome timer throttling and Microsoft's machine inactivity limit. The shared folder behavior was read from the version 6.06 image's Samba and setup scripts.
- Desktop Commander Remote Device: setup and relay architecture.
- Cloudflare: Self-hosted applications and origin token validation and the Zero Trust plans announcement. Check the current plan page for limits.
- Out-of-band console: OVHcloud IPMI console guide, updated January 29, 2026.
- Backups: rclone Google Drive backend, Google Drive API usage limits, restic repository operations and restic tuning parameters.
Related reading
- The Sovereign AI Playbook introduces the server and its software map.
- The Sovereign Advantage covers the economics and limits of a dedicated workstation.
- Sovereign AI Workstation Best Practices covers keeping desktop sessions, updates and scheduled jobs reliable.
- Mobile AI Workspace Lessons covers reaching the workstation from a phone.
- Sovereign AI Made Simple summarizes the savings and the rules, including the measured Drive copy.
- The Work Around the Model covers operations and recovery, with the operator kit.
- Desktop Commander with Codex and Claude shows how assistants can work on the same server's files.
- Podman or Docker? starts this Windows container under three Podman versions and explains when to choose each engine.
- Kubernetes, Docker or Podman for Full Windows in a Container explains Windows containers, KubeVirt and pasta, and when Kubernetes is worth it.
This post is informational, not security-consulting or licensing advice. Check Microsoft's license terms for running Windows in a virtual machine. Mentions of OVHcloud, Cloudflare, Google, Microsoft, Docker, Apache Guacamole, restic and rclone are nominative fair use. No affiliation is implied.