资讯动态

Docker+QEMU+noVNC:构建浏览器访问的虚拟机管理平台

发布时间:2026/9/16 15:39:59 来源:尧图企业网站定制
第一次意识到装虚拟机这件事不一定非要在自己电脑上完成是在一次远程交付现场。客户的机器没有虚拟化权限管理员密码锁在千里之外的运维手里我手头只有一个浏览器。那时候我才认真琢磨 Docker 里跑 QEMU 这条路——把 QEMU 装进容器用 noVNC 把 VNC 画面推到网页上宿主机放机房任何一台有浏览器的设备都能连上去操作虚拟机。这套方案断断续续跑了大半年期间踩过不少坑也总结出一套相对稳定的部署流程。这篇文章就把完整链路、关键配置、常见故障和扩展玩法一次讲透适合想搭浏览器可控虚拟机平台的研发同学、测试团队或者自建实验室的人参考。1. 为什么要把 QEMU 塞进 Docker还要用浏览器去操作1.1 这套方案到底解决了什么痛点传统玩法是每台工作机装 VirtualBox 或 VMware本地建虚拟机本地打开图形界面操作。短期用着还行一旦机器多了就乱镜像散落在不同电脑上版本对不上排障时我得远程到对方桌面还得等他先把虚拟机开起来。更麻烦的是宿主机之间互相抢虚拟化资源——同一台电脑上同时装 VMware 和 VirtualBoxVT-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 本身是个万能模拟器它有两种截然不同的工作模式。第一种是纯软件模拟TCGTiny Code Generator。QEMU 把客户机的每一条指令翻译成宿主机指令来执行不需要 CPU 的虚拟化扩展兼容性极强但性能差。实测在没有 KVM 的情况下跑一个 Ubuntu 桌面虚拟机开机都要好几分钟装软件更是煎熬。第二种是硬件加速KVM。宿主机把/dev/kvm设备透传给 QEMUQEMU 变成轻量级 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 端口5900display 编号 0 对应 5900display 1 对应 5901以此类推websockify 监听端口6080对外只暴露这一个端口即可noVNC 网页资源websockify 启动时用--web参数指定网页文件目录2.3 容器的网络、设备透传和存储规划这一层很多人会忽略但它直接决定了部署是否稳。先看网络方案。桥接模式下容器有自己的虚拟网卡端口通过-p 6080:6080映射到宿主机多台容器互不冲突。host 模式下容器直接用宿主机网络端口直接用但容易碰撞我一般只用桥接。再看设备透传。KVM 加速必须把宿主机的/dev/kvm设备映射进容器--device/dev/kvm如果容器里还希望虚拟机做网卡桥接或者创建复杂网络拓扑往往还需要--cap-addNET_ADMIN和--cap-addNET_RAW。这个配置在 Docker Compose 里对应devices和cap_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 和 supervisorQEMU、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] nodaemontrue [program:novnc] commandwebsockify --web/usr/share/novnc 6080 localhost:5900 autorestarttrue [program:qemu] commandqemu-system-x86_64 -m 2048 -smp 2 -hda /data/ubuntu.qcow2 -vnc :0 autorestartfalse注意这里我特意没加-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.htmlnoVNC 页面会自动尝试连接看到连接界面后点 Connect就能看到 QEMU 的启动画面了。因为磁盘是空的QEMU 会从 ISO 引导。但前面那个 supervisord 配置里并没有-cdrom参数所以想要从 ISO 装系统第一次启动时可以把 QEMU 启动命令临时改成[program:qemu] commandqemu-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 wasnt detected. 或者搜索框里常见的适用于linux的windows子系统和虚拟机平台。这两个勾不上每次重启他都会恢复原状。这个问题的根源往往不是 Docker 本身而是 Windows 功能开关和 BIOS 设置配合不上。正确排查路径是进入 BIOS确认 SVMAMD或 VT-xIntel已开启注意部分主板还需要开启VT-d。打开Windows 功能窗口勾选适用于 Linux 的 Windows 子系统和虚拟机平台。如果勾选后每次重启都恢复原状说明 Hyper-V 相关组件未完全安装需要在管理员 PowerShell 里执行Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux Enable-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform执行完重启机器再重新打开功能窗口确认。Docker Desktop 设置里选 WSL2 后端确认默认发行版已安装。这一步每台机器情况都不一样但核心原则是先 BIOS、后 Windows 功能、再 Docker Desktop 设置顺序不能乱。4.2 KVM 透传失败时的性能断崖容器里没有/dev/kvmQEMU 会默默回退到 TCG 模式。如果你没注意虚拟机也能跑起来只是慢到让你怀疑人生。CPU 密集型的编译任务从几分钟拉长到几个小时这种情况最容易在踩坑初期出现。判断当前是 KVM 还是 TCG最直接的方法是在 noVNC 里按CtrlAlt2进入 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 模拟必须显式指定机器类型和 CPUqemu-system-aarch64 -M virt -cpu cortex-a57 ...-M virt是通用的 ARM 虚拟化机器类型-cpu cortex-a57是比较常见的模拟 CPU也可以换成cortex-a72或max。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 的 CtrlAltDel 这类组合键需要从 noVNC 的菜单里发送直接按键盘是无效的。CtrlAlt2 切换 QEMU monitor 这类特殊键在 noVNC 里通常也要通过菜单或粘贴面板操作。如果键盘布局不对检查 QEMU 的-k参数例如美式键盘用-k en-us。5. 进阶实战多虚拟机编排、磁盘扩容、快照恢复5.1 用多容器编排跑多台虚拟机单台虚拟机跑通了接下来的需求一定是同时跑多台。我倾向于每个 QEMU 进程一个容器通过 Compose 来管理。新建一个docker-compose.multi.ymlservices: vm1: image: qemu-novnc:latest container_name: vm1 environment: - VM_DISK/data/vm1.qcow2 - VM_MEMORY2048 - VM_SMP2 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_MEMORY4096 - VM_SMP4 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.qcow2convert会把磁盘里未使用的块去掉新文件比旧文件小很多然后再替换旧文件。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 的人都能操作你的虚拟机安全边界必须提前划好。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价