Docker+QEMU+noVNC:构建浏览器访问的虚拟机管理平台
2026/9/16 15:39:57 网站建设 项目流程

第一次意识到"装虚拟机"这件事不一定非要在自己电脑上完成,是在一次远程交付现场。客户的机器没有虚拟化权限,管理员密码锁在千里之外的运维手里,我手头只有一个浏览器。那时候我才认真琢磨 Docker 里跑 QEMU 这条路——把 QEMU 装进容器,用 noVNC 把 VNC 画面推到网页上,宿主机放机房,任何一台有浏览器的设备都能连上去操作虚拟机。这套方案断断续续跑了大半年,期间踩过不少坑,也总结出一套相对稳定的部署流程。这篇文章就把完整链路、关键配置、常见故障和扩展玩法一次讲透,适合想搭"浏览器可控虚拟机平台"的研发同学、测试团队或者自建实验室的人参考。

1. 为什么要把 QEMU 塞进 Docker,还要用浏览器去操作

1.1 这套方案到底解决了什么痛点

传统玩法是每台工作机装 VirtualBox 或 VMware,本地建虚拟机,本地打开图形界面操作。短期用着还行,一旦机器多了就乱:镜像散落在不同电脑上,版本对不上;排障时我得远程到对方桌面,还得等他先把虚拟机开起来。更麻烦的是宿主机之间互相抢虚拟化资源——同一台电脑上同时装 VMware 和 VirtualBox,VT-x 经常被其中一个锁住,另一个直接报错。

把 QEMU 搬进 Docker 之后,整个局面变了。虚拟机跑在服务器上,本地只需要一个浏览器,通过 noVNC 就能看到完整的虚拟屏幕、键盘、鼠标,操作手感跟本地虚拟机几乎没有差别。镜像、磁盘文件、启动参数全部收进容器,用 Dockerfile 和 docker-compose.yml 管理,团队里任何一个人都能用同一条命令拉起一套一模一样的实验环境。宿主机集中放在机房或者云上,资源利用率比个人电脑高得多,也给后续的权限控制、审计留了窗口。

1.2 哪些人适合用这套架构

我实际用下来,下面几类场景特别适合这种 Docker + QEMU + 浏览器的组合:

  • 跨架构测试:x86 服务器上模拟 ARM64 环境,编译产物直接扔进模拟器里跑,不需要买实体 ARM 板卡。这是"qemu模拟arm64"被搜爆的主要原因。
  • 团队共享测试环境:测试同事不用装任何客户端,打开浏览器输入 URL 就能进虚拟机,测完一键销毁重建。
  • 教学和演示:给学员发一个链接,任何人都能连进同一样式的虚拟机环境,省去"我装的是 32 位还是 64 位"这类十万个为什么。
  • 个人 Homelab:一台 NAS 或小主机,跑几个 Docker 容器,每个容器里一台虚拟机,浏览器访问,手机也能凑合操作。

2. 底层架构拆解:KVM 透传、VNC 协议和 noVNC 的关系

2.1 QEMU 的两种执行模式:TCG 纯模拟与 KVM 加速

QEMU 本身是个万能模拟器,它有两种截然不同的工作模式。

第一种是纯软件模拟(TCG,Tiny Code Generator)。QEMU 把客户机的每一条指令翻译成宿主机指令来执行,不需要 CPU 的虚拟化扩展,兼容性极强,但性能差。实测在没有 KVM 的情况下跑一个 Ubuntu 桌面虚拟机,开机都要好几分钟,装软件更是煎熬。

第二种是硬件加速(KVM)。宿主机把/dev/kvm设备透传给 QEMU,QEMU 变成轻量级 hypervisor,让虚拟机指令直接跑在 CPU 上,性能接近裸机。这个模式要求 CPU 支持 VT-x/AMD-V,并且宿主机已加载 KVM 模块。

对比项TCG 纯模拟KVM 加速
CPU 虚拟化扩展要求不要求必须支持 VT-x/AMD-V
性能大约比原生慢 10~50 倍接近原生性能
适用场景跨架构模拟、临时测试同架构高性能虚拟化
Docker 透传设备--device=/dev/kvm

部署时我会优先尝试 KVM,透传失败则由 QEMU 自动回退到 TCG。搞清楚当前容器到底跑在哪种模式下,是后续排查性能问题的关键,这一点在第 4 章会详细展开。

2.2 noVNC 怎么把 VNC 变成浏览器能打开的页面

QEMU 自带 VNC server,启动虚拟机时加一个-vnc :0参数,QEMU 就会在 5900 端口监听 VNC 协议。但 VNC 协议走的是原始 TCP,浏览器直接连不了,这时候就需要 noVNC 登场。

noVNC 是一个纯前端的 VNC 客户端,配合 websockify 这个 WebSocket 代理使用。流程是这样:浏览器先发起 WebSocket 连接到 websockify(默认 6080 端口),websockify 把 WebSocket 数据包翻译成原始 TCP 数据,转发给 QEMU 的 VNC 端口。整个链路里的数据"翻译"过程对用户完全透明,浏览器里的<canvas>元素负责实时渲染虚拟屏幕,键盘鼠标事件则反过来通过 WebSocket 回传。

从用户视角看,最终效果就是打开一个网页,看到虚拟机的桌面,直接在网页里操作。这里的核心要点是端口规划要理顺:

  • QEMU VNC 端口:5900(display 编号 0 对应 5900,display 1 对应 5901,以此类推)
  • websockify 监听端口:6080(对外只暴露这一个端口即可)
  • noVNC 网页资源:websockify 启动时用--web参数指定网页文件目录

2.3 容器的网络、设备透传和存储规划

这一层很多人会忽略,但它直接决定了部署是否稳。先看网络方案。桥接模式下,容器有自己的虚拟网卡,端口通过-p 6080:6080映射到宿主机,多台容器互不冲突。host 模式下容器直接用宿主机网络,端口直接用但容易碰撞,我一般只用桥接。

再看设备透传。KVM 加速必须把宿主机的/dev/kvm设备映射进容器:

--device=/dev/kvm

如果容器里还希望虚拟机做网卡桥接或者创建复杂网络拓扑,往往还需要--cap-add=NET_ADMIN--cap-add=NET_RAW。这个配置在 Docker Compose 里对应devicescap_add字段。

存储方面,虚拟机的磁盘文件(qcow2 格式)必须放在持久化卷里,否则容器删了磁盘也一起没了:

-v /opt/vm/data:/data

我把-v挂载目录固定为/data,虚拟机磁盘统一放这里面,备份、迁移都方便。

3. 完整部署步骤:写 Dockerfile、起容器、装系统、连浏览器

3.1 宿主机准备:先确认虚拟化能力

在写 Dockerfile 之前,先在宿主机上执行这几条命令,确认硬件虚拟化条件是否满足:

# 检查 CPU 是否支持虚拟化扩展,输出大于等于 1 说明支持 egrep -c '(vmx|svm)' /proc/cpuinfo # 检查 KVM 设备是否存在 ls -l /dev/kvm

如果第一条命令输出为 0,基本可以断定 BIOS 里的 VT-x/AMD-V 没开。如果/dev/kvm不存在,即使 CPU 支持,也需要先加载 KVM 内核模块:

modprobe kvm_intel # Intel CPU modprobe kvm_amd # AMD CPU

处理完这些再安装 Docker。Linux 服务端直接装 Docker Engine 和 Compose 插件即可;Windows 上则需要 Docker Desktop + WSL2 后端,并且 BIOS 里必须启用"虚拟机平台"和"适用于 Linux 的 Windows 子系统"这两个功能,这也是很多人反复重启后仍报虚拟化检测失败的原因,第 4 章会专门拆解。

3.2 镜像构建:一条 Dockerfile 打通 QEMU、noVNC 和 supervisor

QEMU、VNC server、websockify、noVNC 页面这四样东西需要同时跑在容器里。我直接用 supervisor 把这些进程统一管起来,避免手写一堆后台启动脚本。

FROM debian:bookworm-slim RUN apt-get update && \ apt-get install -y --no-install-recommends \ qemu-system-x86 qemu-system-aarch64 qemu-utils \ novnc websockify supervisor ca-certificates && \ rm -rf /var/lib/apt/lists/* RUN mkdir -p /data /iso /opt/novnc COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf EXPOSE 6080 CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisor/supervisord.conf"]

对应 supervisord.conf 配置同样很关键:

[supervisord] nodaemon=true [program:novnc] command=websockify --web=/usr/share/novnc 6080 localhost:5900 autorestart=true [program:qemu] command=qemu-system-x86_64 -m 2048 -smp 2 -hda /data/ubuntu.qcow2 -vnc :0 autorestart=false

注意这里我特意没加-enable-kvm,先让 QEMU 自己跑起来,再在容器里验证/dev/kvm是否存在。如果确实存在,启动 QEMU 时手动加上这个参数,或者干脆把-enable-kvm写进启动参数里,让 QEMU 在设备不可用时自动报错,方便定位问题。

3.3 用 docker compose 创建磁盘、启动容器

在工作目录下建好目录结构:

vm-platform/ ├── Dockerfile ├── docker-compose.yml ├── supervisord.conf └── data/

docker-compose.yml 内容如下:

services: qemu-novnc: build: . container_name: qemu-novnc ports: - "6080:6080" devices: - "/dev/kvm:/dev/kvm" cap_add: - NET_ADMIN - NET_RAW volumes: - ./data:/data - ./iso:/iso restart: unless-stopped

第一次启动前,先用 qemu-img 创建虚拟磁盘。这里我建议在宿主机上先拉一个系统镜像 ISO,放在./iso目录下,然后进入容器执行:

docker compose build docker compose run --rm qemu-novnc qemu-img create -f qcow2 /data/ubuntu.qcow2 20G docker compose up -d

磁盘创建好之后,supervisor 会自动把 QEMU 拉起来。但要注意一个问题:上面 supervisord.conf 里 QEMU 的-hda指向的是ubuntu.qcow2,如果你创建的是其他名字的磁盘,得先把这条命令改对,再启动容器。

3.4 从浏览器安装系统并完成首次启动

容器起来后,浏览器访问宿主机的 6080 端口:

http://你的服务器IP:6080/vnc.html

noVNC 页面会自动尝试连接,看到连接界面后点 Connect,就能看到 QEMU 的启动画面了。因为磁盘是空的,QEMU 会从 ISO 引导。但前面那个 supervisord 配置里并没有-cdrom参数,所以想要从 ISO 装系统,第一次启动时可以把 QEMU 启动命令临时改成:

[program:qemu] command=qemu-system-x86_64 -m 2048 -smp 2 -hda /data/ubuntu.qcow2 -cdrom /iso/ubuntu-24.04-server.iso -boot d -vnc :0

或者在容器里手动停掉 supervisor 管理的 QEMU 进程,用上面的命令前台跑一次,等系统安装完成再恢复。整个安装过程画面都在浏览器里,跟本地虚拟机体验差别不大。

安装完成后,我给 QEMU 的启动参数补上几个生产中必要的项:

qemu-system-x86_64 \ -enable-kvm \ -m 2048 -smp 2 \ -hda /data/ubuntu.qcow2 \ -vnc :0 \ -usbdevice tablet \ -k en-us

-usbdevice tablet是为了让 noVNC 的鼠标指针不再漂移,这个坑务必要提前踩平,否则后面操作会非常痛苦。

4. 踩坑实录:我从这些故障里学到的教训

4.1 Docker Desktop 虚拟化检测失败的连锁反应

Windows 上装 Docker Desktop,很多人会卡在启动报错:"Docker Desktop failed to start because virtualisation support wasn't detected." 或者搜索框里常见的"适用于linux的windows子系统和虚拟机平台。这两个勾不上每次重启他都会恢复原状"。

这个问题的根源往往不是 Docker 本身,而是 Windows 功能开关和 BIOS 设置配合不上。正确排查路径是:

  1. 进入 BIOS,确认 SVM(AMD)或 VT-x(Intel)已开启,注意部分主板还需要开启"VT-d"。
  2. 打开"Windows 功能"窗口,勾选"适用于 Linux 的 Windows 子系统"和"虚拟机平台"。如果勾选后每次重启都恢复原状,说明 Hyper-V 相关组件未完全安装,需要在管理员 PowerShell 里执行:
    Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux Enable-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform
    执行完重启机器,再重新打开功能窗口确认。
  3. Docker Desktop 设置里选 WSL2 后端,确认默认发行版已安装。

这一步每台机器情况都不一样,但核心原则是:先 BIOS、后 Windows 功能、再 Docker Desktop 设置,顺序不能乱。

4.2 KVM 透传失败时的性能断崖

容器里没有/dev/kvm,QEMU 会默默回退到 TCG 模式。如果你没注意,虚拟机也能跑起来,只是慢到让你怀疑人生。CPU 密集型的编译任务从几分钟拉长到几个小时,这种情况最容易在踩坑初期出现。

判断当前是 KVM 还是 TCG,最直接的方法是在 noVNC 里按Ctrl+Alt+2进入 QEMU monitor,输入:

info kvm

输出kvm support: enabled就是 KVM 模式,否则就是 TCG。另一个快速判断是看启动速度:虚拟机开机如果超过三分钟还进不了系统,基本可以断定走了 TCG。

如果容器里确实没有 KVM 设备,先回宿主机执行ls -l /dev/kvm看看设备是否存在。存在的话,多半是 Docker Compose 的devices配置写错或者容器没重建,修正后docker compose up -d --force-recreate重新创建容器即可。实在没有 KVM 的环境,只能接受 TCG 的性能,或者考虑换一台支持嵌套虚拟化的云主机。

4.3 模拟 ARM64 时最容易掉进去的三个坑

模拟 ARM64 的完整命令格式跟 x86 差别很大,光是把之前 x86 的启动命令换个qemu-system-aarch64是起不来的。我总结三个高频坑:

机器类型和 CPU 型号。ARM64 模拟必须显式指定机器类型和 CPU:

qemu-system-aarch64 -M virt -cpu cortex-a57 ...

-M virt是通用的 ARM 虚拟化机器类型,-cpu cortex-a57是比较常见的模拟 CPU,也可以换成cortex-a72max

UEFI 固件缺失。ARM64 不像 x86 那样有 BIOS,必须加载 UEFI 固件。Debian/Ubuntu 下一般需要:

qemu-system-aarch64 -M virt -cpu cortex-a57 \ -bios /usr/share/qemu-efi-aarch64/QEMU_EFI.fd \ ...

如果QEMU_EFI.fd不存在,说明没装qemu-efi-aarch64软件包。

系统镜像选型。ARM64 模拟对 ISO 镜像的兼容性比 x86 挑剔得多。很多 x86 的安装 ISO 放到 ARM64 QEMU 里连引导都过不去。我推荐直接用云镜像或者专门的 ARM64 Server ISO,比如 Ubuntu 的 ARM64 Server ISO 或者 Debian 的 arm64 netinst ISO,成功率会高很多。

启动后的 VNC 和 noVNC 配置跟 x86 完全一致,-vnc :0-usbdevice tablet这些参数照抄就行。

4.4 noVNC 鼠标漂移和快捷键失效的修复

noVNC 的鼠标指针漂移是最磨人的问题。浏览器里看到的鼠标和虚拟机里的鼠标位置对不上,点都点不准。原因是 VNC 默认使用相对坐标定位,但浏览器端的触摸屏、高分屏缩放会把坐标搞乱。解决办法是让 QEMU 使用绝对坐标的 USB 平板设备:

-usbdevice tablet

加了这条参数后,鼠标位置由虚拟机直接获取绝对坐标,不再依赖相对移动,漂移问题基本消失。

快捷键方面,noVNC 的 Ctrl+Alt+Del 这类组合键需要从 noVNC 的菜单里发送,直接按键盘是无效的。Ctrl+Alt+2 切换 QEMU monitor 这类特殊键,在 noVNC 里通常也要通过菜单或粘贴面板操作。如果键盘布局不对,检查 QEMU 的-k参数,例如美式键盘用-k en-us

5. 进阶实战:多虚拟机编排、磁盘扩容、快照恢复

5.1 用多容器编排跑多台虚拟机

单台虚拟机跑通了,接下来的需求一定是同时跑多台。我倾向于每个 QEMU 进程一个容器,通过 Compose 来管理。

新建一个docker-compose.multi.yml

services: vm1: image: qemu-novnc:latest container_name: vm1 environment: - VM_DISK=/data/vm1.qcow2 - VM_MEMORY=2048 - VM_SMP=2 ports: - "6081:6080" devices: - "/dev/kvm:/dev/kvm" volumes: - ./data/vm1.qcow2:/data/vm1.qcow2 vm2: image: qemu-novnc:latest container_name: vm2 environment: - VM_DISK=/data/vm2.qcow2 - VM_MEMORY=4096 - VM_SMP=4 ports: - "6082:6080" devices: - "/dev/kvm:/dev/kvm" volumes: - ./data/vm2.qcow2:/data/vm2.qcow2

这里需要把 supervisor 配置改成支持环境变量注入的形式,让 QEMU 启动命令从$VM_DISK$VM_MEMORY$VM_SMP读取参数。不同容器暴露不同的宿主机端口(6081、6082),浏览器分别访问对应端口即可。

资源限制也不能忘。在容器配置里加deploy.resources.limits--cpus--memory,防止某台虚拟机把宿主机内存吃爆。

5.2 磁盘扩容与回收空间

qcow2 磁盘是动态增长的,但用久了也不是无限制变大。扩容流程分两步。先扩展磁盘文件本身:

qemu-img resize /data/vm1.qcow2 40G

然后在虚拟机内部调整分区和文件系统:

# 在虚拟机里执行,假设分区是 /dev/vda1,文件系统是 ext4 sudo growpart /dev/vda 1 sudo resize2fs /dev/vda1

回收空间则要反过来:虚拟机里用fstrim或者删除大文件后,宿主机上执行:

qemu-img convert -O qcow2 /data/vm1.qcow2 /data/vm1_compact.qcow2

convert会把磁盘里未使用的块去掉,新文件比旧文件小很多,然后再替换旧文件。

5.3 快照、回滚与备份

QEMU 自带的快照功能对测试场景非常实用。创建快照:

qemu-img snapshot -c clean_state /data/vm1.qcow2

列出现有快照:

qemu-img snapshot -l /data/vm1.qcow2

回滚到某次快照:

qemu-img snapshot -a clean_state /data/vm1.qcow2

注意,做快照前最好让虚拟机处于关机状态或者把磁盘设为只读模式,否则快照可能不一致。生产环境里我还会再加一层宿主机层面的备份,直接在数据目录做 rsync 或者定期用qemu-img convert导出完整磁盘,双保险。

整个这套部署方案,从单台虚机起步,到多机编排、快照恢复,链路打通之后,后续维护成本其实很低。我个人实际用下来的体会是,最难啃的部分永远是"虚拟化能力是否正确透传"——这个问题排查清楚了,剩下的都是常规配置。另外一个值得提早做的小事是给 noVNC 页面加一个简单的访问令牌,或者说在 websockify 前面套一层反向代理做认证。这套平台一旦开放出去,任何拿到 URL 的人都能操作你的虚拟机,安全边界必须提前划好。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询