keelOS: Docker, 你一个BSD虚拟主机系统也能跑么?
能跑,而且是原封不动的 Docker
答案先说:能。keelOS 从 2026 年 10 月起带着 Docker,跑的是 Docker 官方的静态二进制(Docker 29.8、Compose 5.5),一个字节没改;你的 compose 文件直接粘进去就能用,在自己电脑上 docker context create 一行命令就能像连一台 Linux 机器一样连上它。
这个问题在 BSD 圈子里是个老梗。Docker 要的是 Linux 内核的东西:namespace、cgroup v2、overlayfs、netfilter、seccomp、BPF,FreeBSD 一样都没有。jail 很好,但它跑的是 FreeBSD 程序;Linux 兼容层能跑 Linux 的用户态程序,但 dockerd 要的不是系统调用翻译,是内核本身。所以凡是 BSD 底子的系统,要么告诉你"用 jail 吧",要么让你自己装一台 Linux 虚拟机再在里面折腾。
keelOS 本来就是一个虚拟机系统(底下是常驻内存的 hyp,上面一个可以随时重启的 FreeBSD root 管存储和网络,虚拟机在 root 重启时不停),所以我们没有去发明第三条路,而是把"自己装一台 Linux 虚拟机"这件事做成了产品:一台只为 Docker 而生、由 keelOS 自己维护的小虚拟机,叫 kcell。你看到的是应用和容器,虚拟机在幕后。下面讲它是什么、为什么这样做、用起来什么样、快不快、还差什么。
kcell 是什么:一台只为 Docker 而生的小虚拟机
kcell 是一台 bhyve 虚拟机,但里面没有"发行版"。内核是我们自己配的 Linux 7.2(从 allnoconfig 开始,只开 Docker 的 check-config.sh 要的那些和 virtio),第一个程序是我们用 nolibc 写的 init,系统是一份只读的 Alpine 最小根加 Docker 的静态二进制,压缩后 86 MB。里面没有包管理器,也没有需要你登进去维护的东西:每次开机系统都是干净的,升级 keelOS 就是升级了 kcell。
它和一台普通 Linux 虚拟机的区别在于三样东西都不在虚拟机的盘里:
- 系统是主机上的一个 ZFS dataset,经 virtio-fs(我们叫 keelfs)只读挂进去,上面盖一层 tmpfs。一台主机上的所有 kcell 共用同一份。
- 卷(容器的数据)也是主机的 ZFS dataset,挂在 kcell 的
/vols/名字,主机上直接看得见文件(下面单说)。 - 只有
/var/lib/docker(镜像和容器层)在一块 zvol 上的 ext4 里,稀疏分配,缺省 40 GB,用多少占多少。
网络上,kcell 有一个口接在 keelOS 的虚拟交换机上(局域网里的一台机器,DHCP 或固定地址),还有一条只通 root 的内部链路:keel 经这条链路说 Docker API,容器用 -p 发布的端口也经它转到主机地址上。开机不经固件:bhyve 直接装载内核(PVH 入口,和 Firecracker 一样),从 keel kcell start 到 dockerd 能接请求 3.9 秒(dev4,Xeon Silver 4110,2026-10-14)。
上面是你的电脑,中间是 keelOS 的 root(存储和管理都在这里),下面是 kcell:Docker 的两样数据从上往下走两条不同的路,卷和系统走 virtio-fs,镜像层走 virtio-blk。
为什么是虚拟机,不是 jail、不是兼容层
三条路我们都看过,取舍很清楚:
| 做法 | 能跑什么 | 代价 |
|---|---|---|
| jail | FreeBSD 程序 | 没有 Docker 镜像生态;用户要的是 jellyfin/jellyfin,不是再装一遍 |
| Linux 兼容层 | Linux 用户态程序 | dockerd 要 namespace、cgroup v2、overlayfs、netfilter,都是内核的事,兼容层给不了 |
| 一台小 Linux 虚拟机(kcell) | 原封不动的 Docker,所有镜像 | 一台虚拟机的内存(缺省 2 GiB),和一次开机的时间 |
选第三条,然后把它的代价压下去。开机时间靠两件事:不经固件的 PVH 引导(上面的 3.9 秒),和"停机 = 存检查点":keel kcell stop 把内存和设备状态写进它自己的 dataset(只写非零页,2 GB 的 kcell 落盘 160 MB 左右),start 从检查点恢复,容器接着跑,不是重新引导。dev4 上停 1.0–1.6 秒,开到 dockerd 应答 0.9–1.0 秒;主机经固件重启以后从 NVMe 冷读回来 1.4 秒。root 重启(keelOS 的 reload)时 kcell 和其它虚拟机一样挂起再恢复,容器的 TCP 连接不断。
内存那一条是真实的代价:每台 kcell 占自己的内存,没有 balloon。keelOS 的零页回收会把没用到的页还给主机,但一台忙着的 kcell 就是一台 2 GiB 的机器。所以缺省是一台 kcell 跑一个应用,但一台 kcell 里跑十个容器也完全可以,它就是一台装了 Docker 的 Linux。
用起来像什么:应用优先,虚拟机在幕后
网页上的"容器"页以应用为主。点"新建应用",选 Jellyfin 模板或者粘一份自己的 compose,keel 自己建一台同名的 kcell(网络、地址、CPU、内存、Docker 盘全是缺省,不问你),卷在第一次开机前建好,启动,拉镜像,起容器,输出在同一个对话框里。模板是一份 compose 加一段 x-keel 标注:哪个挂载是配置、缓存(自动建成小卷)、哪个是媒体库(从选择器里挑:本机的卷、主机别的 dataset、keelNAS 的共享),网页入口是哪个端口。表单下面有"高级",里面是生成的 compose,可以改。
从 Linux 过来的人大概不想点网页。那就把你的 SSH 公钥贴进 kcell 的设置,它会在自己的局域网地址上开一个只认密钥的 sshd,然后在你的电脑上:
docker context create jellyfin --docker "host=ssh://[email protected]"
docker context use jellyfin
docker compose up -d
从这一刻起它就是一台 Docker 主机:docker ps、docker logs、docker compose 都是你熟悉的那些。第一次打开容器页时,如果主机上还没有 kcell,keel 会先建一台叫 docker 的,所以装好 keelOS 就有一台现成的。局域网上不开 2375/2376。
几条习惯上的对应:-p 8096:8096 发布的端口在 kcell 自己的地址上,缺省也转一份到主机的管理地址(两台 kcell 争同一个端口时先到的占,页面上会标出来);bind mount 写 /vols/卷名;容器里的主机名就是 kcell 的名字,Jellyfin 向导里看到的服务器名不再是一串容器 ID。出了问题再到每台 kcell 的"设置(高级)"里调地址、CPU、内存、扩 Docker 盘。
数据在哪:卷就是主机的 ZFS dataset
这是 kcell 和"自己装一台 Linux 虚拟机跑 Docker"最大的区别。容器的数据不在虚拟机的盘镜像里,而是主机上一个个 ZFS dataset:zones/vm/guests/<kcell>/vols/media 这样的路径,kcell 里是 /vols/media。于是:
- 主机上直接看得见文件,keelNAS 可以把同一个 dataset 共享出去,媒体库不用拷两份。
- ZFS 的快照、压缩、发送都在;每个卷建的时候可以选 recordsize(一般文件 128K,数据库 16K)。
- 删一台 kcell 时卷缺省保留,挂到
kcell-kept/下,建新应用时能再选回来。
把主机文件系统送进虚拟机这件事我们先用 NFS 验证了思路,然后换成自己写的 virtio-fs 服务端 keelfsd(keelfs):每台 kcell 一个进程,直接映射 guest 内存,开着 FUSE 的写回缓存(一个卷属于一台 kcell)。它和虚拟机一起跨 root 重启、跨检查点,在运行中的 kcell 底下热换过好几次,guest 无感。
快不快
开关机的数字上面说过:冷启动到 dockerd 应答 3.9 秒,从检查点恢复 1 秒。下面是卷的 I/O:同一台 kcell(4 vCPU、4 GB)里,keelfs 卷(主机 dataset)和 Docker 自己的卷(kcell 的 zvol 上的 ext4)跑同一套测试,dev4,池在一块 Intel P3600 NVMe 上,2026-10-14(fio O_DIRECT;这一轮主机上还有别的测试在跑,QD1 和顺序读有噪声):
| 项 | keelfs 卷 | zvol 卷 |
|---|---|---|
| 1M 顺序写 QD16 | 742–777 MB/s | 711–743 MB/s |
| 1M 顺序读 QD16 | 578–800 MB/s | 600–672 MB/s |
| 4k 随机读 QD32 | 49.5k IOPS | 76.3k IOPS |
| 4k 随机写 QD32 | 22–23k IOPS | 21–23k IOPS |
| 4k 随机读 / 写 QD1 | 5.1k / 6.5k IOPS | 4.3k / 4.2k IOPS |
| pgbench(-s 20,8 连接) | 2952–3413 tps | 3948–3969 tps |
| npm install(缓存热) | 8.6–8.9 s | 8.4–8.8 s |
| 解开 Linux 源码包(9 万多个文件) | 50–56 s | 8.9 s |
结论是:大块顺序、随机写、数据库、npm 都和本地盘一个水平,媒体库、下载、数据库都放心放在主机卷上;大量小文件的元数据操作慢 5–6 倍,每个请求一个来回是 virtio-fs 的本性,KVM 的 virtiofs 也一样。所以镜像和容器层这种小文件堆留在 zvol 上,是有意的。还有一件小事值得记:kcell 内核一开始跑在 HPET 上(每次读时钟一次 VM exit),换到 TSC 以后 4k 随机读从 31k 到 49.5k。
硬件转码:核显的一个 VF 给 kcell
Jellyfin 没有硬件转码就是半个 Jellyfin。Intel 12 代起的核显支持 SR-IOV,keelOS 用自己的显示虚拟机当 PF 驱动把它切成几个 VF,其中一个直通给 kcell:kcell 内核里的 xe 驱动以 VF 模式起来(只在看到设备时才加载,0.16 秒),/dev/dri/renderD128 就有了,compose 里一行 devices: [/dev/dri:/dev/dri],Jellyfin 自带的 VA-API 就能用。模板里是一个"iGPU 转码"的勾,有 VF 的机器上才出现。
W5012(i5-12600K,UHD 770),2026-10-15,1080p30 H.264 测试片:
| 路径 | fps |
|---|---|
| Jellyfin 自己的转码流,1080p H.264 → 720p HEVC | 73(2.4 倍实时) |
| 容器里 ffmpeg VA-API H.264 → HEVC | 74 |
| 容器里 ffmpeg H.264 低功耗编码 | 366 |
| 对照:libx264 veryfast 软编 | 117 |
和一台普通 Ubuntu 虚拟机拿 VF 的数字一样,kcell 没有损失。代价是带直通设备的 kcell 不能存检查点:停就是正常关机(4 秒),开就是重新引导(6 秒到 Jellyfin 应答),root 重启时它也关掉再起一次,实测中断 73 秒。对一台媒体服务器来说可以接受。
还做不到的、要知道的
- 内存是整台算的:没有 balloon,改内存要冷重启一次(几秒)。十几个小应用放一台 kcell 比十几台 kcell 省内存。
- 小文件:解包、编译这种元数据密集的活在 keelfs 卷上慢 5–6 倍,放 Docker 卷(zvol)上。
- keelNAS 共享当媒体库:表单里有,生成的是 CIFS 卷,但这条路还没在真机上从头跑过。
- 核显:VF 要 12 代及以后的 Intel 核显;老核显只能整块直通,主机屏幕就没了。AMD 和 NVIDIA 没有做。VA-API 解码每帧的 CPU 比预期高(perf 看是 iHD 里的 memset),原因还没查。
- 模板:只有 Jellyfin 一个;没模板的镜像写 compose。模板从 keelos.dev 更新还没做。
- kcell 里没有包管理器:需要什么就跑个容器,这是有意的。系统升级后老检查点作废,那一次是冷启动。
- 只有 x86;kcell 里的 Docker 没有 swarm 的加密 overlay 网络、SELinux/AppArmor。
结语
"BSD 能不能跑 Docker"这个问题,诚实的回答从来都是"在里面跑一台 Linux"。keelOS 做的是把这台 Linux 做得足够小、足够快、足够不用管,再把数据留在 ZFS 里,让你看到的是应用而不是虚拟机。从 Linux 过来的人用自己的 docker 命令行,不想碰命令行的人点一个模板。
试的话:keelos.dev 上装好 keelOS,打开"容器"页,那里已经有一台叫 docker 的 kcell 在等你。文中的数字都是 2026-10-14 和 10-15 在我们的测试机上量的,设计和过程在仓库的 docs/80 里。