把 去也网站 从 Ubuntu 搬到 FreeBSD:一次 Service Jail + nginx 的部署实录
把 去也这个网站 从 Ubuntu 搬到 FreeBSD:一次 Service Jail + nginx 的部署实录
写在前面:这不是一篇"照着做就能成"的教程,而是一份踩坑后的真实复盘。本文记录的是我们内部官网(一套纯静态站点)从 Ubuntu 迁移到 FreeBSD 的完整过程。这次部署的核心矛盾只有一句:在一台 3Mbps 带宽、单网卡的云服务器上,把站点稳稳地跑起来,同时给将来切正式域名留好退路。下面的每一步决策、每一个坑,都来自实测,不是猜的。
一、起点:两台机器的样子
这次迁移涉及两台机器,先把它们分清,后面才不会混:
| 角色 | 系统 | 域名 | 在跑什么 |
|---|---|---|---|
| 老站 | Ubuntu 24.04.2 LTS / nginx 1.24.0 | example.com(A 记录仍指向这里) | 静态站/var/www/site+ 一个 rootless docker 的 playground 服务(:8080) |
| 新站(测试) | FreeBSD 15.0-RELEASE-p4 | test.example.com | 干净重装,啥都没装 |
老站的真实情况比想象中复杂:nginx 只占 80/443 服务静态站点,真正"活"的部分是那个 playground 容器(跑了 46 天)。所以"备份网站"只备/var/www/site是不够的,那个容器才是活的那部分。这点我们在迁移前就记下了,免得后面误判。 | |||
另外老站的 Let’s Encrypt 有两套证书:live/example.com已经在 2026-01-29 过期,实际生效的是example.com-0001(2026-12-03 到期)。nginx 引的是-0001,但certbot renew会尝试续两套——这是个埋雷点,后面再说。 |
二、重装清零,重新规划架构
新机器被我们重新安装成了干净的 FreeBSD 15.0-RELEASE-p4(pkgbase 安装,pkg 2.5.1)。重装前跑的 caddy、CBSD、旧 jail、fail2ban、备份脚本全没了。
用户给了一条很明确的约束:不要恢复旧配置,也不要恢复旧 jail。所以这是一次从零开始的部署,不是迁移配置。
目标架构定为一句话:“web 服务 Jail 跑 nginx + 主机 host nginx 做反代”。
客户端 │ HTTP :80 ▼ host nginx 监听 *:80,server_name test.example.com │ proxy_pass ▼ 127.0.0.1:8080 jail webjail 内 nginx(只监听回环,不对外) │ ▼ /usr/local/www/site 静态站 docroot三、为什么是 Service Jail + nginx(单网卡约束)
这个架构不是炫技,是被硬件逼出来的。
新机器只有一块网卡vtnet0,内网 IP 是**172.xx.117.60**/20,本机不存在192.168.1.x子网。最初设想给 jail 配独立 IP192.168.1.5,在阿里云单网卡环境里根本行不通——没有回程路由。
最终选shared-IP jail(ip4=inherit):jail 与 host 共享网络栈(包括 loopback),jail 内 nginx 监听127.0.0.1:8080,host 反代过去即可。不用加任何 IP 别名,在单网卡环境最稳。
职责划分也很清晰,避免双份响应头:
- host nginx:对外接入、真实 IP 透传、连接管理
- jail nginx:静态文件、gzip、缓存头、安全响应头
host 侧不再重复设置 gzip / 安全头,否则会出现重复响应头。
四、踩坑一:pkg 源不可达,版本钉死 15.0
部署第一个命令就卡住了:pkg install nginx连官方源超时(pkg.FreeBSD.org返回 124)。
解法:把/usr/local/etc/pkg/repos/FreeBSD.conf和 jail 内/etc/pkg/FreeBSD.conf全部指向阿里云内网镜像mirrors.cloud.aliyuncs.com。装包前不要改回官方源,否则整个装包流程会挂住。
接着是更隐蔽的坑:版本钉在 15.0,不要升 15.1。原因很硬——阿里云内网镜像的 15.1 pkgbase 包(Hashed/目录)是损坏的,下载必失败;USTC / 官方 / 公网阿里云在 15.1 上也没有可用 pkgbase 源。也就是说,升级路径被彻底堵死,15.1 只能留到以后单独处理。
经验:在受限网络环境里,先验证"源可达 + 版本可升级"再动手,比闷头照文档走重要得多。
五、插曲:测试环境里 jail 装不了 nginx
在aitest.quye.com这个测试机上,我们原本想用 Caddy(FreeBSD 上 nginx 一度被 pcre2 版本卡住:libpcre2-8.so.0: version PCRE2_10.47 required)。
更离谱的是:jail 里根本装不了 nginx。不是权限问题,是出不了网——jail 的源地址是别名192.168.1.5,而 ECS 的公网 NAT 绑的是主 IP172.xx.117.60,从别名出去的包没有回程。所以pkg拉不到索引、fetch超时。
解法很朴素:Caddy 是 Go 写的单文件静态二进制,无动态依赖。直接把 host 上的caddy二进制拷进 jail:
sudocp/usr/local/bin/caddy /usr/local/jails/containers/wwwjail/usr/local/bin/caddyjail 内起一个:8080的 file_server,host 上 Caddy 按 Host 反代过去。两个坑也记下来了:
- rc 脚本里
</dev/null必须加——否则daemon继承 ssh 的 stdin,远程service start会一直挂住不返回。 - 要给可写
HOME——jail 的/是只读模板,Caddy 想写/.local/share/caddy会报错,用HOME=/usr/local/var解决。
经验:在 jail + 单网卡 + NAT 这条组合里,"出不了网"是 jail 最容易被忽略的死穴,不是权限,是路由。
六、上 HTTPS:证书迁移与 DNS 未切换的真相
在把 quye.com 正式切过来之前,我们提前把老站的证书迁到了新机器。
过程和普通迁移一样,但要适配 FreeBSD 路径:老机是 Ubuntu 的/etc/letsencrypt,FreeBSD 是/usr/local/etc/letsencrypt。renewal/*.conf里的路径全部改写。目录结构照抄:
/usr/local/etc/letsencrypt/ ├── archive/quye.com-0001/ cert/chain/fullchain/privkey 1..6.pem ├── live/quye.com-0001/ → 软链指向 archive 的 *6.pem ├── renewal/quye.com-0001.conf └── accounts/ acme-v02 账户目录nginx 引用live/quye.com-0001/*.pem,续期后软链自动指向新文件,无需 reload 之外的额外动作。
certbot 装的是 4.2.0 + nginx 插件,cron 照抄老机两条(FreeBSD 没有 systemd,用 crontab 承接):
0 */12 * * * /usr/local/bin/certbot -q renew --no-random-sleep-on-renew 0 1 1 * * sleep 2 && /usr/local/bin/certbot renew -q--no-random-sleep-on-renew是必需的,否则 certbot 会加最多 400 秒随机延迟(实测遇到过 397.3 秒)。
关键的真相在 dry-run 暴露出来:
Domain: quye.com Detail: 123.56.226.145: Invalid response from http://quye.com/.well-known/acme-challenge/... : 404验证请求打到了123.56.226.145(老机),因为quye.com的 A 记录还指向老机。HTTP-01 验证必须由"域名的实际解析目标"应答,所以在新机上永远通不过。这不是配置错误,是 DNS 未切换的必然结果。
结论很清楚:DNS 切到 39.97.8.60 之前,续期不会成功。证书 2026-12-03 到期,距今 31 天,只要在那之前完成 DNS 切换,cron 就会正常工作。切 DNS 之前,想验证链路只有两条路:切 DNS,或给aitest.quye.com单独签一张证(它 DNS 已指向本机,HTTP-01 能过)。
顺带还做了两件事:
- 打开pf 防火墙(
*:443放行规则生效)。但要注意pf_enable当时没写进rc.conf,重启不自动启用——这是个遗留项。 - certbot 版本差异:
renewal/*.conf是 5.2.2 写的,本机是 4.2.0,日志有This might not work警告,但实测能正确识别证书。
七、3Mbps 带宽纪律:纯静态的自我约束
quye.com 跑在阿里云 3Mbps 固定带宽的 ECS 上。这个约束直接塑造了整站的设计。
先算账:
| 项目 | 数值 |
|---|---|
| 3 Mbps 理论下行 | ≈ 375 KB/s |
| 首页首访(gzip 后) | ≈ 10 KB |
| 现实可承载 | 20~30 并发在线 / 日均 2000~5000 PV |
结论很清醒:瓶颈不是带宽,是内容更新频率。但要守住一条纪律——不引入任何位图、视频、在线 Demo、第三方统计脚本。一旦加进去,上面所有数字立刻作废。
具体做法:
- 零位图:拓扑图用内联 SVG(各约 1.5 KB),背景网格用 CSS
linear-gradient画。 - 不加载任何 webfont:一个中文 webfont 动辄 2–5 MB,等于把 3M 带宽全部吃掉。只用系统字体。
- 静态站发布就是覆盖文件:用
rsync -avz --delete上传到/srv/quyecom/,无需 reload(HTML 缓存 5 分钟自动过期)。改 CSS/JS 要连带改文件名或加?v=,否则 30 天缓存不会失效。
带宽告急时的三级预案也提前写好了(迁 OSS+CDN、全站静态托管、开 WAF),但那是后话。
八、构建与门禁:让"换备案号"不再改 16 个页脚
站点源是可配置的模板,产物纯静态;无框架、无 npm,只有三个零依赖的 Python 脚本。
三层结构一句话:config.ini(数据)→ 源 HTML(结构)→出/(产物)。
公司名、备案号、邮箱、快照日期只在config.ini写一次,页面里全是{{公司.备案号}}这样的占位;跑工具/构建.py渲染进出/,出/上传就完事。
为什么值得这一步?因为不构建就得把备案号抄在 16 个页脚里——换号时改一处漏三处,这是必然发生的。现在换 ICP 只改config.ini一行,重跑构建即可。代价是多了 90 行 Python,没有 npm、没有 node_modules、没有版本地狱。
门禁替人复查:工具/自检.py分源/产物两段。源段专门查有没有人又把公司信息写死(发现鲁ICP备…、hello@…、公司全称就直接红牌),产物段查死链、canonical、体积、位图、JSON-LD、残留占位。这条"禁止硬编码"的规则是模块化不退回的保险。
上线命令永远是一条:
python3 工具/构建.py&&python3 工具/自检.py --上线&&rsync-avz--delete出/ user@host:/srv/quyecom/九、当前状态与下一步
实测结果(2026-11-01 起):
http://aitest.quye.com/返回 200,全站 23 个 URL 全部 200,0 例外。- 首页 18,857 B → gzip 后6,671 B(省 65%)。
- 自定义 404 页、-
Server头隐藏、X-Robots-Tag: noindex生效(测试站不收录)。 - jail 重启后二进制与站点存活,一键恢复。
还没做完的:
- 只有 HTTP,没有 HTTPS。上 TLS 时:申请证书(阿里云免费 DV 或 Let’s Encrypt 通配符
*.quye.com)→ host nginx 加listen 443 ssl(jail 侧不用动)→ 在quye-headers.conf加回Strict-Transport-Security。 - 等 DNS 切换:把
quye.com/www.quye.com的 A 记录指到39.97.8.60,证书续期才会真正生效。 - pf 开机自启:往
/etc/rc.conf加pf_enable="YES"。 - 阿里云备案:
.com解析到境内 ECS 必须备案,页脚替换真实备案号。
十、复盘:三条最值得带走的教训
先把"源可达 + 版本可升"验证掉。受限网络里,
pkg源和 pkgbase 版本是最早、也最容易被低估的拦路虎。钉死 15.0、用阿里云内网镜像,是踩了超时之后才定的。jail 的"出不了网"是路由问题,不是权限问题。单网卡 + NAT 环境下,给 jail 配一个本机不存在的子网别名,等于让它发出的包没有回程。要么 shared-IP(ip4=inherit)共享 loopback,要么把静态二进制拷进去,别妄想
pkg install。证书续期失败,先查 DNS,别查配置。HTTP-01 验证打的是域名真实解析目标。A 记录还指着老机器,新机器上怎么调配置都过不了 dry-run。这条能省下大把无意义排查。
本文由济宁极快软件部署过程的真实记录整理而成。相关技术资产与实战案例可在站内查看。