PVE 里跑 Docker:LXC 容器 CT702 实战
摘要
PVE 里跑 Docker,有两种姿势:开一台虚机装 Docker,或者直接在 LXC 容器里跑。这套家庭数据中心选了后者:CT702(docker-lab,Debian 13,Docker 29.8.1,2 核 3G)就是专门的 Docker 实验田,Stirling PDF、Uptime Kuma、Dashy 导航页全用 compose 编排,一份容器内的 /root/lab-compose.yml 管所有。本文讲为什么选 LXC、容器里跑 Docker 的前置条件、三应用怎么部署、"新增应用默认纯内网"这条用户规则如何落地,以及三个真实踩坑:pct push 丢执行权限、国内拉镜像慢、Dashy 3.x 配置挂载位置变了。
目录
- 引言:PVE 里跑 Docker 的两种姿势
- CT702 速览
- 为什么选 LXC 而不是虚机
- LXC 里跑 Docker 的两个前置条件
- 日常操作的标准姿势:经 pct exec 进 702
- Compose 编排:一份文件管三个应用
- 用户规则:新增应用默认纯内网
- 导航页上线:nav.028000.xyz
- 1Panel:留着当 Docker 的图形手
- 踩坑与注意事项
- 总结
引言:PVE 里跑 Docker 的两种姿势
要在 PVE 上跑 Docker 应用,摆在面前的有两条路:起一台 Debian 虚机,在里面装 Docker(重但标准);或者直接用 LXC 容器跑 Docker(轻但要处理一点容器嵌套的细节)。这套环境选了后者,理由后面细说。先说结论:CT702 这个 LXC 容器现在是全家 Docker 应用的试验田,PDF 工具箱、服务监控、导航页都在里面跑得好好的,资源占用还很低。
CT702 速览
| 项目 | 数值 |
|---|---|
| 容器 ID / 名称 | CT702 / docker-lab |
| 系统 | Debian 13 |
| Docker 版本 | 29.8.1 |
| CPU / 内存 | 2 核 / 3G(3072M) |
| 内网 IP | 192.168.0.127 |
| 编排文件 | /root/lab-compose.yml(在 CT702 容器内,不在 PVE 宿主机上) |
| 当前应用 | Stirling PDF(:8080)、Uptime Kuma(:3001)、Dashy(:3002)、EasyImage(:8090)、Halo 博客(:8093→8090) |
| 额外组件 | 1Panel(:18080,Docker 管理)、xray-client(镜像代理) |
审计时的资源余量:容器可用内存约 1.9G、磁盘约 53G 可用。PVE 宿主机当前的主要瓶颈是内存(可用约 3.7G),所以每个新应用上架前都会先看一眼余量,这是固定动作。
注意"当前应用"里后加的两项:EasyImage 图床(8090)和 Halo 博客(宿主 8093→容器 8090)后来也进了 702,不过它们各有独立的 compose 文件和数据目录(/opt/easyimage、/opt/halo2),不归 lab-compose.yml 管。一台容器、多种编排并存,互不干扰。
为什么选 LXC 而不是虚机
| 维度 | LXC 容器 | 虚机 |
|---|---|---|
| 资源开销 | 与宿主机共享内核,几乎零额外开销 | 独立内核,内存/CPU 都有固定损耗 |
| 启动速度 | 秒级 | 分钟级 |
| 快照与备份 | 快,vzdump 负担小 | 慢,镜像体积大 |
| 资源隔离 | 够用(cgroup 限 CPU/内存) | 更彻底 |
| 适用场景 | 轻量应用、实验田 | 需要独立内核、跑异构系统的场景 |
对 Stirling PDF、Uptime Kuma、Dashy 这种轻量 Web 应用来说,LXC 的隔离度完全够用,省下的内存和备份时间都是实打实的。虚机留给真正需要的场景:黑群晖要直通物理盘、OpenWrt 要当软路由、办公虚机要跑完整桌面环境,这些才值得一台虚机的开销。简单说就是:杀鸡用 LXC 这把小刀,杀牛才上虚机。
LXC 里跑 Docker 的两个前置条件
Docker 本来就是容器技术,在 LXC 容器里再跑 Docker,属于"容器套容器",有两个前置条件要满足。这是通用知识,任何要在 PVE LXC 里跑 Docker 的人都绕不开:
条件一:允许嵌套(nesting)。 LXC 默认不允许容器内再创建新的命名空间和 cgroup 层级,Docker 起容器时需要这些。PVE 里给 LXC 开 nesting=1 特性(容器配置的"特性"里勾选"嵌套"),Docker 才能正常创建子容器。不开的话,docker run 会报各种权限错误,表象千奇百怪,根因就这一个。
条件二:keyctl 相关能力。 Docker 的某些存储驱动和密钥管理要用到内核 keyring,非特权容器默认被 AppArmor/seccomp 挡掉一部分。实践中 nesting=1 加上 PVE 默认的 LXC 配置,Docker 29 这类新版本基本能直接跑起来;如果遇到诡异的权限报错,再去查容器的 AppArmor profile 和 seccomp 设置。
CT702 就是按这个思路配好的,Docker 29.8.1 一次装好,后面部署的五个应用都没再为"容器套容器"操过心。先满足前置条件,再谈应用部署,这个顺序别反。
日常操作的标准姿势:经 pct exec 进 702
这里有个极易踩的坑,值得单独成节:/root/lab-compose.yml 在 CT702 容器内部,不在 PVE 宿主机上。之前有人(就是我)下意识地在 PVE 宿主机上找这个文件,找了个寂寞。
所以操作 702 里 Docker 的标准姿势是:
# 在 PVE 宿主机上,经 pct exec 进容器执行 docker 命令
pct exec 702 -- docker ps
pct exec 702 -- docker compose -f /root/lab-compose.yml up -d
pct exec 702 -- docker logs stirling-pdf
# 需要交互式进去排查时
pct enter 702
如果是经 VPS 远程操作,就再套一层 SSH(ssh pve-home 'pct exec 702 -- docker ps')。要点是:永远明确你当前在哪一层——VPS、PVE 宿主机、LXC 容器,是三层不同的 shell,compose 文件只在最里那层。传文件进容器用 pct push(注意它会丢执行位,见下文踩坑),或者整文件写好再推进去,比在多层 SSH 里拼 heredoc 引号靠谱得多。
Compose 编排:一份文件管三个应用
lab-compose.yml 管的三个应用全部用 Docker Compose 编排,一处修改、一处版本管理。应用清单:
| 应用 | 内网地址 | 验收状态 | 用途 |
|---|---|---|---|
| Stirling PDF | http://192.168.0.127:8080 | 首页 200 | PDF 工具箱:合并、拆分、压缩、转换 |
| Uptime Kuma | http://192.168.0.127:3001 | 302(首次设置跳转,正常) | 服务监控:给家里各服务做存活告警 |
| Dashy | http://192.168.0.127:3002 | 首页 200 | 导航页「028000 · 家庭数据中心」 |
compose 文件的结构示意(以实际部署为准,这里只画骨架):
# /root/lab-compose.yml(在 CT702 容器内,结构示意)
services:
stirling-pdf:
ports:
- "8080:8080" # 内网访问端口
uptime-kuma:
ports:
- "3001:3001"
dashy:
ports:
- "3002:3002"
volumes:
# Dashy 3.x 的配置实际从 /app/user-data/conf.yml 读取
- /opt/dashy/conf.yml:/app/user-data/conf.yml
- /opt/dashy/conf.yml:/app/public/conf.yml
注意 Dashy 的 volumes 有两行,这不是重复,而是踩坑换来的(详见下文"坑三"):Dashy 3.x 真正读取的是 /app/user-data/conf.yml,只挂 /app/public/conf.yml 会被无视,页面显示英文默认配置。两个都挂,一劳永逸。
Dashy 值得多说两句。它的页面名叫「028000 · 家庭数据中心」,按"常用服务、PVE 虚拟化、应用服务、VPS"等分组,把全家十几项服务的内外网入口、用途说明都录了进去,配置文件在容器内的 /opt/dashy/conf.yml。之前整理服务地址用的是 Word 文档,现在 Word 照出(归档用),日常查地址看导航页。这次部署的应用、后来的图床和 Halo 博客,都第一时间录进了 Dashy,导航页就是全家的"服务黄页"。
Uptime Kuma 有个待办:首次打开需要用户自己设置管理员账号,具体监控项还没配。这是个"先搭台、再唱戏"的安排,监控项可以慢慢加。
compose 日常管理就四个命令,够用了:
pct exec 702 -- docker compose -f /root/lab-compose.yml up -d # 启动/更新
pct exec 702 -- docker compose -f /root/lab-compose.yml ps # 看状态
pct exec 702 -- docker compose -f /root/lab-compose.yml logs -f # 看日志
pct exec 702 -- docker compose -f /root/lab-compose.yml pull # 拉新镜像
改完 compose 文件,up -d 之后一定要做三件事:看容器状态是不是 healthy、curl 内网端口看真实返回、把新入口录进 Dashy 导航页。
用户规则:新增应用默认纯内网
这是用户亲口定的一条规则,原文精神是:今后新增应用默认纯内网,不配域名、不做外网,除非明确说明需要。Stirling PDF 和 Uptime Kuma 就是这条规则的第一个执行案例。
最初部署时,给三个应用都配了 nps 隧道和公网域名(pdf.028000.xyz、status.028000.xyz,外加隧道 28501、28502,对应 nps 任务 11 和 12)。规则一定,立刻回滚:删掉 pdf 和 status 的 nginx 站点配置,删除 nps 任务 11 和 12(VPS 端口 28501、28502 关闭),删之前配置分别备份在 VPS 的 /root/nginx-bak-20261001/ 和 /root/tasks.json.bak-20261001。现在 Stirling PDF 和 Uptime Kuma 只有内网入口(192.168.0.127:8080、:3001),想用?先回家,或者连 VPN。
唯一保留公网入口的是 Dashy 导航页,因为它本身就是"从外面查家里服务地址"的入口。但它也不是裸奔:上了 basic auth 认证,还配了 Let's Encrypt 证书走 HTTPS。逻辑很清晰:纯内网是默认,公网是例外,例外必须有认证。
导航页上线:nav.028000.xyz
Dashy 的公网入口是 nav.028000.xyz,走 nps 隧道 28503 → 192.168.0.127:3002,VPS 上 nginx 反代。上线过程:
- 用户在 DNSPod 添加 nav 的 A 记录指向 82.139.204.8;
- 签发 Let's Encrypt 证书(有效期至 2026-12-30),certbot 自动续期;
- nginx 配 443 端口:basic auth + 反代到 127.0.0.1:28503,HTTP 301 跳 HTTPS;
- 端到端验收:无认证返回 401,有认证返回 Dashy 真实页面,HTTP 正确 301 跳转。
验收用的还是"看真实效果"那一套:401 说明认证生效,真实页面说明隧道和反代链路全通,301 说明强制 HTTPS 生效。三个断言都过,才算上线。
1Panel:留着当 Docker 的图形手
CT702 里除了业务应用,还有个 1Panel(1panel-core 监听 0.0.0.0:18080)。审计时它被标记为"未登记的额外攻击面",用户逐项定夺后决定保留:留着当 Docker 的图形化管理手,命令行之外多一个可视化入口。
保留不等于放任:待办事项里明确写着"改强密码"。纯内网、无公网域名、无 nps 隧道,是它目前的安全边界。1Panel 这类面板的常见风险就是弱口令,密码强度是保留它的前提条件,这一条已经记在待办里,不会忘。
踩坑与注意事项
坑一:pct push 会丢文件执行权限。 从 PVE 往 LXC 里推文件(比如 xray 客户端二进制),pct push 能保留文件内容,但可能丢掉执行位。表象很迷惑:文件在那,ls -l 一看权限不对,运行报 Permission denied。修复办法很简单,进容器 chmod +x 就行。但更好的习惯是:push 完顺手检查一下权限,别等报错了再回头找。
坑二:家里直连 Docker Hub 极慢,ghcr.io 甚至 DNS 不可达。 这是部署 Stirling PDF 时撞上的第一堵墙。当时的 workaround 很"重型":在本机用 crane 把镜像拉下来,按 200M 分片(Stirling PDF 镜像约 1.1G,大文件直传管道会断),经 VPS/PVE 中转过去再 docker load。能用,但每次拉镜像都这么折腾受不了。后来上了 xray 代理一劳永逸,完整过程见《内网拉镜像太慢?crane 分片中转 + xray 代理双方案》。
坑三:Dashy 3.x 的配置挂载位置变了。 带 Node 后端的 Dashy 3.x,配置实际从容器内的 /app/user-data/conf.yml 读取并服务,挂载到 /app/public/conf.yml 会被无视——页面显示英文默认配置,改配置怎么都不生效,特别容易误判为"配置写错了"。compose 里要把宿主(容器内)的 conf.yml 同时挂到这两个路径。另外注意:bind 挂载单个文件后,如果在宿主侧重写文件(换了 inode),容器内看到的还是旧内容,需要重启或重建容器。
坑四:Uptime Kuma 的 302 别当故障。 首次访问返回 302 是跳转到初始化设置页,属正常。验收时看到 3xx 先别慌,看它跳去哪。
通用提醒: 给 CT702 加新应用前,先看 PVE 宿主机内存余量(当前主要瓶颈),再看容器内磁盘。compose 文件改完,docker compose up -d 之后一定要做三件事:看容器状态是不是 healthy、curl 内网端口看真实返回、把新入口录进 Dashy 导航页。
资源监控:别让单个容器吃光宿主机
CT702 里应用越加越多,资源要盯着。PVE 宿主机当前的主要瓶颈是内存(可用约 3.7G),CT702 分了 3G,容器内再不做限制,某个应用暴走会拖累整台容器。
# 看容器内各应用的资源占用
pct exec 702 -- docker stats --no-stream
# 给胃口大的应用加内存上限(compose 里)
services:
stirling-pdf:
mem_limit: 1g # 处理大 PDF 时别把整台容器拖死
mem_reservation: 512m
docker stats 是日常巡检的一员,看一眼哪个容器 CPU/内存异常。mem_limit 是保险丝:Stirling PDF 处理几百 M 的 PDF 时 JVM 可能吃很多内存,设个上限,超了只杀它一个,不连累 Dashy 和 Uptime Kuma。资源限制和备份一样,是"出事时才觉得值"的配置。
另外,PVE 宿主机层面也要看:pvesh get /nodes/pve/status 或面板里看内存曲线。CT702 的 3G 是宿主机内存池里的一块,加新应用前先看宿主机余量,这是固定动作。
总结
CT702 这套实践可以复制给任何 PVE 用户:LXC 跑 Docker 省资源,compose 一份文件管所有应用,"默认纯内网"是安全的基本盘,导航页是服务的黄页,镜像代理解决国内拉取难题。几个坑(pct push 丢执行位、拉镜像慢、Dashy 配置挂载位置、compose 文件到底在哪一层)都是环境特有的,提前知道能省半天时间。下一步:Uptime Kuma 的监控项配起来,让它真正盯着全家的服务;1Panel 的强密码改掉,把保留它的前提条件落实。
相关阅读:《家庭数据中心架构总览:VPS、PVE、NAS 三方分工》、《nps 内网穿透实战:一机一 Client 的隧道管理体系》、《内网拉镜像太慢?crane 分片中转 + xray 代理双方案》