在Docker容器中运行Windows:基于QEMU/KVM的浏览器访问方案
1. 为什么要在 Docker 里跑 Windows
先把话说清楚:这个项目的核心,是在 Linux 宿主机上用 Docker 容器把一套完整的 Windows 系统跑起来,并且通过浏览器直接访问它的桌面。不是远程桌面连一台物理机,也不是在 Windows 里装 Docker Desktop 那种反向操作,而是宿主机是 Linux,Docker 负责拉起一个带 KVM 加速的 Windows 虚拟机,最后通过 Web 界面在浏览器里操作。
我第一次接触这个思路是在一台闲置的 NUC 上。那台机器装了 Ubuntu Server,平时跑一些容器服务,性能过剩。我想着能不能把一些只能在 Windows 下跑的旧软件也塞进去,省得再单独开一台机器。传统做法是装 VMware 或者 VirtualBox,但那样又多了一层管理,而且图形界面在服务器上很别扭。后来发现有人把 QEMU 和 KVM 打包进 Docker 镜像,配合 Web 端的 VNC 或者 RDP 转发,直接在浏览器里就能看到 Windows 桌面,这个方案就非常合适了。
它解决的核心问题有三个。第一是隔离,Windows 系统跑在容器里,跟宿主机的文件系统、网络栈是隔开的,折腾坏了直接删容器重建,几秒钟的事。第二是便携,整个 Windows 环境可以用一个 docker-compose.yml 描述,换台机器把文件拷过去就能复现。第三是可控,通过浏览器访问意味着你不需要在本地装任何客户端,手机、平板、公司电脑,只要能开网页就能连上你的 Windows。
适合谁来参考?我觉得三类人最有用。一类是运维或者开发,手头有 Linux 服务器,想临时开个 Windows 环境跑测试。一类是折腾党,喜欢把各种系统塞进容器里玩。还有一类是需要一个干净、可重置的 Windows 环境来做某些特定工作,比如跑一些老旧的行业软件,不想污染自己的主力机。
需要提前说明的是,这个方案对宿主机有硬性要求。CPU 必须支持硬件虚拟化,也就是 Intel 的 VT-x 或者 AMD 的 AMD-V,并且要在 BIOS 里打开。没有这个,QEMU 只能走纯软件模拟,速度慢到没法用。另外内核模块 kvm 和 kvm_intel(或 kvm_amd)必须能正常加载。这两点不满足的话,后面的步骤都跑不起来,建议先确认。
2. 整体方案设计与技术选型拆解
2.1 为什么是 Docker 加 QEMU 加 KVM 这套组合
要理解这个方案,得先拆开看每一层在干什么。Docker 在这里的角色其实很单纯,它负责打包运行环境和依赖,把 QEMU、KVM 相关的工具、Web 访问服务全部塞进一个镜像里,保证在不同 Linux 发行版上行为一致。它不负责虚拟化本身,虚拟化是 QEMU 和 KVM 干的活。
QEMU 是一个通用的机器模拟器,它能模拟出完整的硬件环境,包括 CPU、内存、磁盘、网卡、显卡。单独用 QEMU 的话,所有指令都是软件翻译执行,性能很差。KVM 是 Linux 内核自带的虚拟化模块,它让 QEMU 可以直接调用 CPU 的硬件虚拟化能力,把大部分指令直接交给物理 CPU 执行,性能接近原生。所以 QEMU 加 KVM 的组合,才是既有完整硬件模拟能力、又有接近物理机性能的方案。
那为什么还要套一层 Docker?直接宿主机装 QEMU 不行吗?行,但有几个麻烦。一是依赖管理,QEMU 版本、各种固件、Web 服务组件,在不同发行版上装法不一样,容易出兼容问题。二是隔离,直接跑在宿主机上,虚拟机的网络配置、文件路径都跟宿主机耦合,清理起来不干净。三是复现,用 Docker 镜像可以把整套环境固化下来,换机器直接拉镜像就行。所以 Docker 在这里是作为交付和隔离的载体,不是虚拟化引擎。
2.2 浏览器可控是怎么实现的
浏览器能操作 Windows 桌面,靠的是把虚拟机的显示输出和输入事件做转发。常见有两条路。一条是 VNC,QEMU 内置 VNC 服务端,把画面编码后通过 VNC 协议发出去,前端用一个 Web 版的 VNC 客户端(比如 noVNC)在浏览器里渲染,同时把键盘鼠标事件回传。另一条是 RDP,Windows 本身开启远程桌面,前端用 Web 版的 RDP 客户端连接。两条路各有取舍。
VNC 的好处是不依赖 Windows 内部配置,系统还没装好、还在 BIOS 界面的时候就能看到画面,适合安装阶段。缺点是画面传输效率一般,色彩和流畅度不如 RDP,尤其是播放视频或者拖动窗口时能感觉到延迟。RDP 的好处是效率高、体验好,支持剪贴板、音频重定向这些高级功能。缺点是必须等 Windows 装好并开启远程桌面之后才能用,安装阶段帮不上忙。
我自己的做法是两者都留着。安装阶段用 VNC 看画面,系统装好之后日常用 RDP。很多现成的镜像其实已经同时集成了这两套 Web 前端,通过不同端口访问,切换很方便。如果你只想选一个,我建议至少保留 VNC,因为它能覆盖从裸机到系统的全过程,RDP 作为体验优化再加。
2.3 镜像选型与版本考量
市面上能实现这个效果的镜像不止一个,选型时我主要看几个点。第一是维护活跃度,虚拟化这块内核和 QEMU 版本更新频繁,镜像太久不更新容易踩坑。第二是是否支持 KVM 直通,有些镜像为了通用性默认走软件模拟,性能差很多,要确认它能挂载 /dev/kvm。第三是 Web 访问是否开箱即用,有的镜像只提供 VNC 端口,需要自己再配 Web 前端,多一道工序。第四是磁盘和网络配置是否灵活,能不能方便地挂载宿主机目录、映射端口。
版本上有个细节要注意。Windows 镜像本身有版本差异,从早期的 Windows 7 到 Windows 10、Windows 11,对硬件的要求和激活机制都不一样。Windows 11 对 TPM 和 CPU 有额外要求,在 QEMU 里模拟 TPM 会复杂一些。如果只是跑一些普通软件,Windows 10 的兼容性和资源占用比较平衡,是我比较推荐的选择。Windows 7 更轻,但很多新软件不支持了。这个取舍要根据你实际要跑的软件来定。
另外磁盘格式也值得说一句。QEMU 常用的磁盘格式有 qcow2 和 raw。qcow2 支持快照、压缩、稀疏分配,文件实际占用随使用增长,适合做系统盘。raw 性能略好但不支持快照,占用是固定的。做 Windows 系统盘我一般用 qcow2,方便随时打快照回滚,装系统前打个快照,装崩了直接回退,省得重装。
3. 环境准备与核心配置实操
3.1 宿主机前置检查与依赖安装
动手之前先把宿主机的地基打好。第一步确认 CPU 虚拟化支持,在终端执行下面这条命令,输出大于 0 就说明支持。
如果结果是 0,先去 BIOS 里找 Intel Virtualization Technology 或者 SVM Mode 打开。这一步没做,后面全是白费。
第二步确认 KVM 模块加载正常。
正常应该能看到 kvm 和 kvm_intel(或 kvm_amd)两行。如果没有,尝试手动加载。
第三步确认 /dev/kvm 设备存在且有权限。
这个设备文件是 KVM 加速的入口,容器要挂载它才能用硬件虚拟化。如果权限不对,把当前用户加入 kvm 组。
然后重新登录生效。这一步很多人会漏,结果容器里跑起来发现没有加速,速度慢得怀疑人生。
第四步装 Docker 和 Docker Compose。现在主流发行版用官方脚本装最省事。
Compose 现在多数情况已经作为 Docker 插件存在,验证一下。
如果提示 unknown command,说明装的是老版本独立二进制,需要单独装 compose 插件。这个报错在热词里也出现过,属于高频问题,后面排查章节会细说。
3.2 docker-compose.yml 关键参数逐项拆解
环境就绪后,核心就是这个 compose 文件。我把它拆成几块讲,每一行参数为什么这么写都说清楚。
image 选的是社区维护比较活跃的 Windows 容器镜像,它内部已经集成了 QEMU、KVM 调用逻辑和 Web 访问前端,开箱即用。VERSION 指定 Windows 版本,填 10 就是 Windows 10,也可以填 11 或者更早的版本,镜像会自动下载对应的安装介质。
RAM_SIZE 和 CPU_CORES 是分配给虚拟机的资源。这里有个经验值,Windows 10 日常使用至少给 4G 内存和 2 核,要跑稍微重一点的软件建议 8G 加 4 核。给太多会挤占宿主机,给太少系统卡顿,按宿主机实际配置的 40% 到 60% 来分配比较稳妥。DISK_SIZE 是虚拟磁盘大小,注意 qcow2 是稀疏分配,写 64G 不代表立刻占 64G,实际占用随使用增长,所以可以放心写大一点。
devices 里挂载 /dev/kvm 是性能的关键,没有它 QEMU 只能软件模拟。/dev/net/tun 和 NET_ADMIN 权限是为了让容器能创建虚拟网络设备,实现虚拟机独立联网。ports 里 8006 是 Web 访问端口,浏览器打开 http://宿主机IP:8006 就能看到画面。3389 是 RDP 端口,TCP 和 UDP 都映射上,UDP 对 RDP 的流畅度有提升。
volumes 里 /storage 是虚拟磁盘和系统文件的存放位置,一定要映射到宿主机目录,否则容器一删系统就没了。/shared 是宿主机和 Windows 之间的共享目录,后面会讲怎么在 Windows 里访问它。stop_grace_period 给 2 分钟,是因为 Windows 关机需要时间,如果给太短,容器被强杀会导致磁盘文件损坏,这个坑我踩过,系统直接起不来,只能重装。
3.3 首次启动与系统安装过程
配置写好之后,启动命令很简单。
第一次启动会做几件事。先下载镜像,然后下载 Windows 安装介质,接着创建虚拟磁盘,最后启动 QEMU 并开始安装系统。整个过程是自动的,你不需要手动点安装向导,镜像里的自动化脚本会处理分区、复制文件这些步骤。下载和安装的时间取决于网速和磁盘性能,我实测在千兆网加 SSD 的环境下,从启动到能看到 Windows 桌面大概二十到三十分钟。
想看进度就盯日志。
日志里会显示当前阶段,比如下载镜像、创建磁盘、安装系统、启动完成。看到类似启动成功的提示后,浏览器打开 http://宿主机IP:8006,就能看到 Windows 桌面了。如果还在安装阶段,VNC 画面会显示安装进度,耐心等就行。
这里有个细节,安装过程中虚拟机会重启几次,VNC 画面可能会短暂断开,这是正常的,不用慌。等它稳定显示桌面,说明安装完成。默认用户名和密码就是 compose 文件里设置的,登录进去第一件事建议改密码,并且开启远程桌面,方便后面用 RDP 连接。
4. 网络、存储与性能调优实战
4.1 虚拟机联网的两种模式与选择
虚拟机能不能正常上网,取决于网络模式。QEMU 常见有两种。一种是用户模式网络,也叫 NAT,虚拟机通过宿主机的网络出口访问外网,配置简单,不需要额外权限,但外部设备访问虚拟机比较麻烦,需要做端口转发。另一种是桥接模式,虚拟机直接接入宿主机所在的物理网络,获得独立 IP,跟局域网里其他设备平级,互相访问很方便。
这个容器镜像默认走的是用户模式网络,配合端口映射,把虚拟机的端口暴露到宿主机。日常上网、下载东西完全够用。但如果你需要局域网里其他机器直接访问这台 Windows,比如把它当文件服务器或者跑个内网服务,桥接模式更合适。配置桥接需要在宿主机上创建网桥,然后把容器的网络接到网桥上,步骤稍多,涉及宿主机网络配置,改之前建议先确认不影响宿主机本身的网络。
我自己的用法是,如果只是自己用浏览器访问,用户模式足够了。如果团队里多台机器要连这台 Windows,就上桥接。判断标准很简单,看有没有"别人也要访问"这个需求。
4.2 宿主机与 Windows 共享文件
共享目录是这个方案里非常实用的一块。compose 文件里把宿主机的 ./shared 映射到了容器的 /shared,接下来要在 Windows 里访问它。因为 QEMU 支持 virtio 文件系统共享,Windows 端需要装对应的驱动才能看到这个目录。
具体做法是,在 Windows 里打开设备管理器,找到标记为未知设备的那一项,手动安装驱动,驱动文件通常在镜像内置的共享目录里,或者可以从 virtio 驱动包里获取。装好之后,Windows 里会多出一个盘符,指向宿主机的共享目录。往里拖文件,宿主机立刻能看到,反过来也一样。
如果嫌装驱动麻烦,还有个更简单的办法,直接用网络共享。在 Windows 里开一个共享文件夹,宿主机通过 SMB 协议访问,或者反过来宿主机开 SMB 共享,Windows 里映射网络驱动器。这种方式不依赖 virtio 驱动,配置直观,缺点是走网络协议,速度比 virtio 共享略慢,但日常传文档完全够用。
提示:共享目录的权限要提前在宿主机上设好,容器里的进程以特定用户身份运行,如果宿主机目录权限太严,Windows 里会看到目录但写不进去,这个报错很隐蔽,排查时容易忽略。
4.3 性能调优的几个关键点
性能这块,KVM 加速是基础,但还有几个参数能进一步榨出性能。第一是 CPU 模式,QEMU 默认可能用 qemu64 这种通用模式,改成 host 模式能让虚拟机直接使用宿主机的 CPU 指令集,性能提升明显。这个通常在镜像的启动参数里可以配,或者通过环境变量传入。
第二是磁盘缓存模式。QEMU 的磁盘缓存有 writeback、writethrough、none 几种。writeback 性能最好但断电有丢数据风险,writethrough 最安全但慢,none 配合宿主机层面的缓存比较平衡。做系统盘我一般用 writeback 加定期快照,兼顾性能和可恢复性。
第三是内存分配策略。给虚拟机分配内存时,如果宿主机内存充足,可以开启大页内存,减少 TLB 缺失,提升内存访问效率。这个需要在宿主机上预留大页,配置稍复杂,属于进阶优化,普通使用不配也能跑。
第四是显卡。QEMU 默认的显卡模拟性能一般,如果 Windows 里要跑图形密集的东西,可以试试 virtio-gpu 驱动,配合相应的 Windows 驱动,2D 加速会好一些。3D 加速在容器环境里比较难搞,如果真有 3D 需求,这个方案可能不是最优解,要考虑其他途径。
实测下来,一台 8 核 16G 的宿主机,分配 4 核 8G 给 Windows 10,日常办公软件、浏览器、轻量开发工具都很流畅,跟物理机差别不大。重负载场景比如视频渲染,还是会有差距,这个要有合理预期。
5. 常见问题排查与避坑经验
5.1 启动失败类问题速查
这类问题最让人头疼,因为容器起不来,日志又可能不够详细。我整理了一张速查表,覆盖我遇到过和社区里高频出现的几种情况。
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
| 容器启动后立刻退出 | /dev/kvm 不存在或无权限 | 检查宿主机 KVM 模块,确认设备文件权限,用户加入 kvm 组 |
| 日志提示无法访问 KVM | BIOS 未开启虚拟化 | 进 BIOS 打开 VT-x 或 SVM |
| 提示 docker compose 未知命令 | Compose 未安装或版本过旧 | 安装 compose 插件,或改用 docker-compose 独立命令 |
| 启动卡在下载镜像 | 网络问题或镜像源不可达 | 检查网络,配置镜像加速,或手动下载介质放入指定目录 |
| 系统装到一半卡死 | 磁盘空间不足或内存分配过小 | 检查宿主机剩余空间,适当调大 RAM_SIZE 和 DISK_SIZE |
| 浏览器打不开 8006 端口 | 端口未映射或被防火墙拦截 | 确认 compose 端口映射,检查宿主机防火墙规则 |
这张表里,我重点说两个。一个是 KVM 权限问题,表现是容器能启动但虚拟机跑得极慢,日志里可能只有一句轻描淡写的警告,很容易被忽略。判断方法是进容器看 QEMU 进程的启动参数,如果没有 -enable-kvm,那就是没走加速。另一个是磁盘空间,qcow2 虽然稀疏,但系统装完加上后续使用,实际占用会涨到十几个 G,宿主机空间不够会在写入时失败,表现是系统运行中突然卡死或者蓝屏,这种问题排查起来很绕,提前留足空间能省很多事。
5.2 运行中的稳定性问题
系统跑起来之后,也会遇到一些稳定性问题。最常见的是关机异常导致磁盘损坏。前面提过,Windows 关机需要时间,如果容器被强制停止,虚拟磁盘可能处于不一致状态,下次启动会触发磁盘检查,严重的话直接起不来。解决办法就是给足 stop_grace_period,并且养成在 Windows 里正常关机的习惯,不要直接 docker compose down。
另一个是内存溢出。宿主机内存被其他服务占用后,虚拟机可能因为拿不到足够内存而崩溃。可以给容器设置内存限制,但要注意限制值要大于分配给虚拟机的内存,否则 QEMU 申请内存失败。比如虚拟机给了 8G,容器限制至少设 9G 以上,留出 QEMU 自身和 Web 服务的开销。
还有网络抖动问题。用户模式网络下,如果宿主机网络切换或者重启,虚拟机的网络连接会中断,需要重新获取。表现是 Windows 里网络图标显示受限,等一会儿或者手动禁用启用网卡能恢复。如果对网络稳定性要求高,桥接模式会好一些。
5.3 几个我踩过的坑和独家技巧
第一个坑是镜像版本和 Windows 版本的匹配。有次我图省事用了比较老的镜像,结果它内置的自动化脚本对新版 Windows 安装介质支持不好,装到一半报错。后来换成活跃维护的镜像就正常了。所以选镜像别只看功能,看最近更新时间。
第二个坑是共享目录的路径。我一开始把共享目录设在宿主机的一个软链接路径下,结果容器里解析出来是错的,Windows 里死活看不到。后来改成绝对路径就好了。涉及挂载的路径,尽量用真实绝对路径,别用软链接。
第三个技巧是快照。装系统前、装完常用软件后,各打一个快照。QEMU 支持在线打快照,命令不复杂。这样系统玩坏了,回滚到快照就行,不用重装。我现在的习惯是每装完一个关键软件就打一次,磁盘占用增加不多,但省下的重装时间很可观。
第四个技巧是关于 RDP 的。开启远程桌面后,如果连接时提示证书问题,可以在客户端设置里忽略证书验证。另外 RDP 默认端口 3389 映射到宿主机后,如果宿主机本身也开了 3389,会冲突,改一下映射端口就行,比如映射成 13389。
第五个技巧是日志。遇到问题先看日志,docker compose logs 能看到大部分线索。如果日志不够,可以进容器看 QEMU 的详细输出,或者临时把日志级别调高。很多问题日志里其实有提示,只是被忽略了。
6. 这个方案还能怎么扩展
基础跑通之后,其实还有不少可以玩的方向。一个是多实例,同一台宿主机上跑多个 Windows 容器,每个分配不同端口和资源,互不干扰。适合需要多个隔离环境的场景,比如测试不同软件组合。资源分配上要注意总和别超过宿主机上限,否则会互相抢资源。
另一个是持久化和迁移。因为系统盘映射在宿主机目录,整个 Windows 环境其实就是那个目录。把它打包拷到另一台机器,配上同样的 compose 文件,就能原样恢复。这个特性做环境备份和迁移非常方便。我试过把一台机器上的 Windows 环境整个搬到另一台,改一下网络配置就能用,省去了重装和重新配置的麻烦。
还有就是和自动化流程结合。因为容器可以用命令行控制,可以把 Windows 环境的启动、快照、关机都写进脚本,跟 CI 或者其他自动化工具串起来。比如需要 Windows 环境跑某个测试时,脚本自动拉起容器,跑完自动打快照关闭。这种用法在需要 Windows 环境的自动化测试里挺有价值。
最后说个我个人的体会。这个方案最大的价值不是省了买一台 Windows 机器的钱,而是把 Windows 环境变成了一个可描述、可复制、可丢弃的东西。以前装个 Windows 要折腾半天,现在一个配置文件加一条命令,环境就起来了,坏了删掉重来,心态完全不一样。这种把环境当代码管理的感觉,用过就回不去了。