手动 incus launch 一台小鸡不难,难的是重复 6 次还不出错:装 Incus、建存储池、写 profile、装 SSH、配端口映射、设密码——任何一步手滑,后面全是坑。所以我写了 incus_3vps_docker_compatible_verified.sh(6 台版同源,只是 COUNT=6)。这篇逐段讲脚本,重点是每一行为什么这么写。
为什么这么做:可重复、可断点续跑
脚本的设计目标就三条:全新 Debian 12 上一遍跑通;网络中断、apt 卡住时重跑能接着干,不删已有容器、不重置已生成的密码;跑完自己验收,输出 PASS/FAIL,不靠人眼检查。
怎么做:逐段拆解
第一段:预检,拒绝在错误的环境上跑
[[ "$(id -u)" == 0 ]] || fail '必须以 root 运行'
[[ "${VERSION_CODENAME:-}" == 'bookworm' ]] || fail '仅适用于 Debian 12 bookworm'
[[ "$(dpkg --print-architecture)" == 'amd64' ]] || fail '仅适用于 amd64 架构'
Incus 的存储、网络操作全要 root;Zabbly 源只有 bookworm 分支;btrfs 镜像是 amd64 的。与其跑到一半报错,不如开头就拦。
第二段:Zabbly 源 + 指纹校验(上一篇讲过动机,这里直接给完整写法)
cat > /etc/apt/sources.list.d/zabbly-incus.sources <<'SRC'
Enabled: yes
Types: deb
URIs: https://pkgs.zabbly.com/incus/lts-6.0
Suites: bookworm
Components: main
Architectures: amd64
Signed-By: /etc/apt/keyrings/zabbly.asc
SRC
用的是 DEB822 格式的 .sources 文件,Signed-By 明确绑定 key,不污染全局信任。
第三段:初始化与存储
if ! incus storage list --format csv 2>/dev/null | grep -q '^default,'; then
incus admin init --minimal
fi
init --minimal 只做最少的事:建 incusbr0 网桥和默认存储。不搞集群、不问交互,适合单机。if 包裹保证重跑时跳过——这就是断点续跑的第一个体现。
第四段:Docker 共存的转发修复(原理下篇详讲,这里看结构)
脚本把修复逻辑写成独立脚本 /usr/local/sbin/incus-docker-forward-fix.sh,再配一个 systemd oneshot 服务开机执行:
[Unit]
Description=Permit Incus outbound forwarding through Docker firewall
After=docker.service incus.service
为什么是 oneshot + 开机自启?因为 Docker 每次重启都会重建它的 iptables 规则,之前加的放行规则会丢。服务设了 After=docker.service,保证每次开机都在 Docker 之后补规则。
第五段:容器创建——proxy 模式为什么不用 nat=true
每台小鸡的核心配置就三行:
incus launch images:debian/12 "$NAME" -p tiny-vps \
-c limits.cpu=1 -c limits.memory=256MiB -c boot.autostart=true
注意镜像源是 images:debian/12——LinuxContainers 官方镜像站。系统容器镜像和 Docker Hub 的应用镜像是两个体系:前者是完整操作系统用户态(含 systemd),后者是单个应用。别混了,混了就会问出"为什么这个镜像里没有 systemctl"。
SSH 端口映射用的是普通 proxy 设备,刻意不用 nat=true:
incus config device add "$NAME" "ssh${SSH_PORT}" proxy \
listen="tcp:0.0.0.0:${SSH_PORT}" \
connect="tcp:127.0.0.1:22"
为什么?nat=true 模式下 Incus 会用 iptables DNAT 实现转发,规则藏在系统里,ss 看不到监听,出问题难排查。而普通 proxy 模式是 Incus 自己在宿主机 0.0.0.0:2201 上起监听,ss -lnt 一目了然,行为可预测。代价是多占一点用户态转发的开销——SSH 场景下忽略不计。
第六段:随机密码与密码文件
PASS=$(openssl rand -hex 16)
printf 'root:%s\n' "$PASS" | incus exec "$NAME" -- chpasswd
32 位十六进制随机密码,umask 077 + chmod 0600 保证密码文件只有 root 可读。关键设计:密码只生成一次——重跑脚本时,已有记录的容器跳过密码生成,避免"我明明记得密码,怎么登不上了"的惨剧。
SSH 的配置写法也值得说:没有直接改 /etc/ssh/sshd_config,而是写 drop-in 文件:
mkdir -p /etc/ssh/sshd_config.d
printf "PermitRootLogin yes\nPasswordAuthentication yes\nKbdInteractiveAuthentication no\n" \
> /etc/ssh/sshd_config.d/00-incus-root.conf
sshd -t && systemctl enable --now ssh && systemctl restart ssh
drop-in 的好处:不污染发行版的主配置文件,以后 Debian 升级 sshd 不会产生 .dpkg-dist 冲突;sshd -t 先校验语法再重启,配错了不会把自己锁在外面;KbdInteractiveAuthentication no 关掉键盘交互认证,只留密码认证一种方式,attack surface 小一点是一点。
第七段:9 项自动验收
跑完逐台检查:容器 RUNNING、ssh 服务 active、网关可达、外网 IPv4、DNS、TCP 443、SSH 代理配置、宿主机端口监听、根盘 3GiB 配额。输出示例:
---- vps1 (SSH 2201) ----
[PASS] vps1 RUNNING
[PASS] vps1 ssh 服务 active
...
有一个 FAIL 就整体退出码非零,但不回滚——验收是告诉你哪里有问题,不是替你做决定。
脚本的工程习惯:幂等、 fail-fast、最小权限
通读脚本会发现几个贯穿始终的习惯,值得单独拎出来:
1. set -Eeuo pipefail + fail() 函数:任何命令失败、任何变量未定义,脚本立刻退出,不带病往下跑。配合开头的三重预检(root/bookworm/amd64),保证"要么完整跑完,要么一行不跑"。
2. 幂等设计:所有"创建"操作都用 if ! ... show 包裹——存储池已存在就跳过、profile 已存在就跳过、容器已存在就只调配置。配合"密码只生成一次",脚本可以反复重跑,网络中断后接着干,不会删容器、不会重置密码。这就是"断点续装"的全部秘密:没有魔法,只是每个步骤都先问一句"做过了吗"。
3. 密码文件格式:/root/incus-vps-passwords.txt 是 Tab 分隔的四列——容器名、用户名、SSH 端口、密码,chmod 0600 只有 root 可读。验收清单和密码表都从这个文件 awk 出来,单一数据源,不会出现"终端显示的密码和文件里记的不一样"。
4. 验收为什么查 9 项:每一项都对应一个真实翻车点。查"网关可达"用 ping,查"外网"除了 ping 1.1.1.1 还要查 TCP 443——因为有些网络禁 ICMP,ping 不通不代表 TCP 不通(脚本注释里明确写了"PING失败不一定代表TCP不可用")。查 SSH 要同时查代理配置和宿主机监听:incus config device get 对了只证明配置写了,ss -lnt 有 LISTEN 才证明真在监听。配置≠生效,这个习惯是排障排出来的。
验收失败时的处理哲学:有 FAIL 时脚本退出码为 2,但不回滚、不删容器,只打印一句"FAIL 可能由 ICMP 限制或已有容器配置不同引起,请核查对应项目"。为什么?因为验收失败的原因太多:有的是真问题(SSH 没起来),有的是环境特性(目标网络禁 ping)。脚本的职责是"把问题指出来",而不是"替你决定删不删"。自动化的边界感,体现在知道什么时候该停手。跑完先看 FAIL 项,一项项手动确认——这是脚本作者(我)能给你的最大诚实。
踩坑与演进
- 旧版残留清理:早期版本试过
nat-tcp/nat-udp端口段设备和 nft 防火墙守卫,后来发现 proxy 更直接、DOCKER-USER 定向规则更克制。脚本里专门有一段自动拆除旧版残留(删incus-vps-firewall.service、删 nft 表、删旧 NAT 设备),保证新旧版本混跑不打架。 - 6.0.6 的兼容坑:
incus config子命令不支持--format json,踩到过unknown flag报错,后续脚本统一改用incus query /1.0/...走 REST API(重装脚本里有体现)。
最终效果与事实边界
如实说:3 台版脚本已在 AndNode 年付机真机跑过,vps1 成功 launch,SSH 端口映射、存储池、profile 全部就绪,验收项通过。6 台版与 3 台版同源(仅 COUNT 不同),以你实际运行结果为准,跑完请核对 PASS/FAIL 清单。
下一篇:两次真实排障——Docker 把小鸡的网掐了,以及 apt 撞上 Fastly 超时。
