朋友前阵子翻出一台闲置的物理服务器,装了ESXi,然后跑来问我:ESXi上能不能直接装个Docker?我第一反应是这问题得拆开看。后来我自己折腾了一整晚,把ESXi上跑Docker的几条路都试了一遍。这篇就把当时的思考过程、实测数据、踩过的坑、最后留在生产里的配置,一条条捋清楚。如果你手头正好有一台装了ESXi的机器想跑容器,这篇文章就是给你准备的。
1. 先说结论:ESXi上跑容器,不是“装个软件”那么简单
1.1 为什么ESXi本身装不了Docker
很多人刚接触虚拟化时,容易把ESXi当成一个“能装软件的Linux系统”。实际上ESXi的内核是VMkernel,它专门干一件事:调度虚拟机。它没有完整的Linux用户态,也没有glibc、systemd这一整套东西,而Docker运行时强依赖Linux内核的cgroups、namespaces、overlayfs这些特性。你可以把它理解为:ESXi是一副国际象棋棋盘,Docker要按照中国象棋的规则落子,棋盘不对,规则自然跑不起来。
所以“ESXi跑Docker”这个需求,落到实操层面,本质只有一条路:在ESXi上开一台Linux虚拟机,在这台虚拟机里装Docker。所有号称“ESXi原生跑容器”的方案,要么是这台Linux虚拟机足够轻量,要么是用了平台级的容器运行时,跟我们平时docker run这个动作不是一回事。
1.2 三条路线横向对比
我把ESXi上跑容器的方案分成三类,分别应对不同场景:
| 方案 | 做法 | 部署复杂度 | 性能损耗 | 适合场景 |
|---|---|---|---|---|
| A:完整Linux VM + Docker | 建Ubuntu Server虚拟机,装Docker Engine | 低 | 约5%~10% | 绝大多数人、绝大多数业务 |
| B:容器专用OS VM + Docker | 建一个极简Linux VM(如容器优化系统),再装Docker | 中 | 约3%~5% | 内存紧张、只跑容器的专用宿主机 |
| C:嵌套虚拟化跑Docker Desktop | 在ESXi的VM里再开虚拟化,跑Docker Desktop | 高 | 明显增加 | 极不推荐,仅限测试 |
方案C我多说一句:Docker Desktop内部自带一个轻量虚拟机,如果ESXi里那台Linux VM没有开启嵌套虚拟化,它直接起不来;就算开启了,性能损耗也很明显,磁盘IO和网络延迟都会变差。我自己测过一次,纯粹浪费时间。所以下面所有内容,都围绕方案A和方案B展开。
2. 虚拟机上跑Docker:最省心的主流路线详拆
2.1 建VM之前,先在ESXi层面想清楚四件事
我见过太多人一上来就在vSphere Client里点“新建虚拟机”,结果后面各种卡顿。建VM之前,这四个参数必须先定下来。
第一是CPU。ESXi默认会向VM暴露硬件虚拟化指令,但如果你打算在VM里跑一些需要嵌套虚拟化的东西(比如KVM),得记得开“硬件虚拟化”选项。单纯跑Docker不涉及这个问题,但如果你要跑容器化的Android模拟器这类工具,就要提前开。分配vCPU的时候,我建议起步2核。Docker本身几乎不吃CPU,但docker build构建镜像时,多核能明显缩短时间,而且容器里的Java、Node应用往往一上来就吃满多核。
第二是内存。Docker引擎本身占用很低,但容器应用不是省油的灯。JVM类容器、编译任务、缓存中间件,512MB内存分分钟被吃光。我的经验是:纯跑几个轻量Web服务,4GB起步;要跑数据库或构建任务,8GB起步。ESXi内存超分不是不能做,但容器场景下内存抖动会引发OOM,严重时整个VM卡死。
第三是磁盘。这里牵扯到ESXi的虚拟磁盘格式。如果你用精简置备(Thin Provision),虚拟磁盘文件只在实际写入时才占用空间,方便是方便,但容器是“写文件大户”——镜像层、容器日志、临时文件都在不停写入。精简磁盘会因为这种写放大而持续增长,而且一旦遭遇ESXi存储空间不足,受影响的是这台宿主机上的所有VM。我的习惯是:容器宿主机的系统盘用厚置备延迟置零,容量一次性给够,省得后面扩容折腾。
第四是虚拟网卡。ESXi里默认的e1000e网卡兼容性最好,但性能一般,而且CPU中断开销大。如果VM里装的是较新的Linux内核,强烈建议选vmxnet3,吞吐量高,CPU占用低。这个在创建VM时就要选好,后期虽然也能改,但改完要重启VM,不如一开始就定下来。
2.2 创建Linux VM并安装Docker的完整步骤
我在测试环境里用Ubuntu Server 22.04做演示,这套流程在Debian系发行版上通用。
- vSphere Client里选择“创建新虚拟机”,客户机操作系统选Linux,版本选Ubuntu Linux (64-bit)。
- 按上面的思路分配资源:2 vCPU、4GB内存、40GB厚置备磁盘、vmxnet3网卡。
- 挂载Ubuntu Server安装镜像,开机,最小化安装,只勾选OpenSSH Server,不装桌面环境。
- 装完系统后,先配静态IP并更新软件源,然后执行Docker官方安装脚本:
curl -fsSL https://get.docker.com -o get-docker.sh sudo sh get-docker.sh- 把当前用户加入docker组,省得每次敲sudo:
sudo usermod -aG docker $USER- 退出重登,执行
docker version确认安装成功,再跑一个hello-world镜像验证。
安装Docker本身就这么简单。真正的坑在后面的存储、网络和自启动上,这些我在下面单独展开。
2.3 为什么我不建议在VM里再用Docker Desktop
有朋友觉得Docker Desktop“用起来方便”,想在ESXi的VM里装一个。我劝他打消这个念头。Docker Desktop的设计目标是给桌面系统(Windows/macOS)提供容器环境,它自带一个轻量虚拟机来跑Linux内核。现在这个轻量虚拟机又被ESXi的VM包了一层,等于容器套虚拟机套虚拟机,性能损耗叠加,磁盘IO尤其难看。而且在Linux VM里装Docker Desktop纯属多此一举,直接装Docker Engine就是原生方案,命令用法完全一样,没有任何理由多套一层。
3. 存储与网络:容器跑在VM里最容易忽略的两个瓶颈
3.1 存储:overlay2、精简置备和“写放大”问题
Docker默认的存储驱动是overlay2,它依赖底层文件系统支持d_type特性,xfs和ext4都没问题,所以VM里的Ubuntu直接用默认配置就行。真正需要动脑子的,是容器写入的数据到底落在哪。
容器和镜像的分层机制决定了:每次构建、每次写日志、每次容器运行产生临时文件,都会在/var/lib/docker里产生写入。如果这个目录所在的虚拟磁盘是精简置备的,ESXi层面会频繁分配新的数据块。表面上看VM里还有空闲空间,ESXi的datastore却可能悄悄见底。我遇到过最夸张的一次,VM里df -h显示用了40%,datastore却涨了60%。
针对这个问题,我的做法是:
- 系统盘装完系统后,再加一块独立虚拟磁盘,挂载到
/var/lib/docker,避免Docker写入跟系统日志写入互相挤兑。 - 给Docker配置日志轮转。在
/etc/docker/daemon.json里加上:
{ "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } }这不光是为了省磁盘,也是为了防止某个容器日志暴涨拖垮整台VM。
- 定期用
docker system prune清理悬空镜像和停止的容器。但注意,这个命令默认不删未使用的数据卷,数据卷膨胀了还得单独处理。
还有一个小优化:ESXi 6.7以上创建VM时,硬盘控制器可以选NVMe。同样的虚拟磁盘,NVMe控制器下的IOPS通常比默认的LSI Logic SCSI高不少,尤其容器频繁读写小文件时感知明显。有条件就选NVMe,没坏处。
3.2 网络:端口组、MAC地址和容器网络的边界
ESXi的网络规划直接决定容器网络好不好用。先说一个最容易踩的坑:克隆VM。
如果你图省事,直接克隆一台已经装了Docker的VM,然后开机,会发现网络不通。常见原因是ESXi克隆后的VM默认会重新生成MAC地址,但如果当前端口组设置了“MAC地址固定”或“混杂模式”相关策略,冲突和丢包就来了。多台克隆VM同时在线时尤其明显。我的建议是:克隆后进VM,用ip link查一下MAC地址,再确认ESXi里没有手动固定过MAC,别在这上面赌运气。
再说容器网络模型。Docker默认的bridge网络用NAT方式让容器出网,外部访问容器需要端口映射。这个模型对单台VM上跑Web服务够用。但如果你有几个服务要互相通信,还依赖服务名互相调用,直接上docker compose,它会自动创建自定义网络,容器间用服务名解析,不用硬记IP。
对于性能敏感或者端口繁多的场景,可以用host网络模式,容器直接复用VM的网络栈,没有NAT转发损耗。代价是没有网络隔离,端口冲突要自己管理。我一般只在跑监控组件这类端口固定且无冲突的内部服务时用host模式。
3.3 数据卷和备份策略
Docker容器本身是无状态的,删了重建很随意,但数据卷不是。我见过有人把数据库文件放在容器可写层里,容器一删,数据全没了。正确做法是把数据卷挂到宿主机目录,比如-v /opt/mysql-data:/var/lib/mysql。
备份就简单了:VM层面打快照。ESXi的快照对运行中的VM很友好,但要注意快照会冻结或增大虚拟磁盘。给带数据库的VM打快照前,最好先通过docker命令把数据卷里的内容做一次冷备或导出,再打快照。我自己生产环境里的习惯是:数据卷目录单独一块虚拟盘,用定时任务做文件级备份,ESXi快照只做应急回滚,不做日常备份。
4. 让VM变成“容器专用宿主机”:更接近原生的另一种玩法
4.1 容器专用OS的思路:把VM裁剪到只剩Docker
如果你不需要在VM里登进去看日志、装调试工具,只想要一台“开机即跑容器”的机器,可以考虑容器专用操作系统。这类系统的特点是:内核极小、系统组件精简、内存占用低、启动速度快。我在测试环境里用的是Photon OS,它在ESXi上跑起来的资源占用非常克制。
整个部署思路跟前面基本一样,区别只在两点:一是安装系统时选最小模式,二是不装任何不必要的服务。装完Docker后,这台VM几乎只干一件事:接收docker命令、运行容器。没有SSH里那些杂七杂八的系统进程抢内存。
4.2 我实测的一组数据
我拿一台8GB内存的物理机装ESXi做测试,跑了两组对比。第一组是一台4GB内存的Ubuntu Server VM,系统装完再装Docker,启动后空闲内存大约剩2.2GB;往里放了10个nginx容器后,可用内存跌破1GB。第二组是同样4GB内存的Photon OS VM,系统加Docker全装完,空闲内存还剩3.1GB;跑同样10个nginx容器,可用内存还有2.5GB。
这个差距主要来自系统服务。Ubuntu Server自带一堆守护进程和日志框架,Photon OS把这些全砍了。如果你手头那台ESXi的物理内存捉襟见肘,又想在同一个宿主机上多塞几个独立VM,容器专用OS的价值就很明显了。
4.3 什么时候值得走这条路
我的判断标准很简单:这台VM如果只跑容器、不干别的,就用容器专用OS;如果还要跑一些需要登录进去操作的任务(比如手动调试数据库、抓包、编代码),就用完整Linux发行版。
注意,容器专用OS也有代价。它的包管理器极其精简,想临时装个tcpdump、vim、curl都不一定找得到。生产环境如果非要用它,我建议把常用调试工具直接打进镜像里,别临时进VM装。还要确认你使用的监控Agent、备份脚本能在这种精简系统上跑通,否则上线前一天发现问题,会相当被动。
5. 生产环境里我踩过的坑和最后留下的配置
5.1 坑一:时间同步导致容器日志时间漂移
ESXi的VM如果不装open-vm-tools,容器日志时间会跟真实时间越偏越远。装上open-vm-tools之后,又可能出现另一种情况:VMware Tools的默认时间同步在系统时间偏差过大时,会“强制跳变”,直接把时间硬掰到正确值。对容器应用来说,时间突然跳变可能导致定时任务乱跑、缓存失效、日志时间线错乱。
我的做法是:在ESXi的VM设置里关掉“VMware Tools时间同步”选项,然后在VM系统里独立配置NTP服务,让系统自己跟时间服务器校准。
5.2 坑二:ESXi重启后,容器没有跟着起来
ESXi宿主机动一下,VM起来后Docker服务自动启动了,但容器不一定恢复。要解决这个问题,需要三层配合:
- ESXi层面:在VM设置里配置“虚拟机启动/关机”,把VM设为随宿主机启动,并设置启动延迟。
- 系统层面:执行
systemctl enable docker,确保Docker守护进程开机自启。 - 容器层面:
docker run时加--restart unless-stopped,或者让Compose文件里的服务都带上restart: unless-stopped。
这三层缺一不可。我遇到过ESXi起来了、VM起来了、Docker也起来了,但容器没起来的情况,就是因为在Compose里没写restart策略。现在我把所有需要常驻的容器都统一标注restart: unless-stopped,它在手动停止容器时不会强行拉起,又在宿主机重启后自动恢复,是最贴合日常操作习惯的策略。
5.3 坑三:磁盘占满导致Docker半瘫痪
这是最隐蔽的一个坑。容器日志和镜像层不断写入,虚拟磁盘最终会被填满。磁盘一满,Docker的表现并不是直接报错,而是docker build卡住、容器启动超时、docker ps偶尔无响应。第一次遇到时我排查了半天才反应过来是磁盘问题。
排查命令很简单:
df -h docker system df清理命令更简单:
docker system prune -a --volumes但这条命令会删除所有未使用的镜像和没有容器引用的数据卷,执行前务必确认。我现在根治这个问题的办法,就是前面说的:给/var/lib/docker挂独立磁盘,配日志轮转,再给Compose里的服务统一加mem_limit,防止个别容器把内存和磁盘同时吃穿。
5.4 我最后留在生产环境的配置
下面是我目前在用的Compose片段,核心参数都标了理由:
version: "3.8" services: web: image: nginx:1.25 restart: unless-stopped mem_limit: 512m logging: driver: json-file options: max-size: "10m" max-file: "3" ports: - "80:80"这套配置对应的是2 vCPU、4GB内存、独立40GB数据盘的VM。用到现在,稳定性和可维护性都让我满意。
最后分享一个我个人的习惯:每次在ESXi上动容器相关的配置,改动前都会在宿主机的ESXi层面打一个快照。这个习惯在初期救过我很多次,尤其是折腾Docker网络和存储驱动的时候。快照不占什么时间,但回滚时能让你少掉很多头发。祝你在ESXi上跑容器一路顺畅。