把 去也网站 从 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 配独立 IP 192.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 绑的是主 IP 172.xx.117.60,从别名出去的包没有回程。所以 pkg 拉不到索引、fetch 超时。

解法很朴素 :Caddy 是 Go 写的单文件静态二进制,无动态依赖。直接把 host 上的 caddy 二进制拷进 jail:

bash 复制代码
sudo cp /usr/local/bin/caddy /usr/local/jails/containers/wwwjail/usr/local/bin/caddy

jail 内起一个 :8080 的 file_server,host 上 Caddy 按 Host 反代过去。两个坑也记下来了:

  1. rc 脚本里 </dev/null 必须加------否则 daemon 继承 ssh 的 stdin,远程 service start 会一直挂住不返回。
  2. 要给可写 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 承接):

cron 复制代码
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、残留占位。这条"禁止硬编码"的规则是模块化不退回的保险。

上线命令永远是一条:

bash 复制代码
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 重启后二进制与站点存活,一键恢复。

还没做完的:

  1. 只有 HTTP,没有 HTTPS。 上 TLS 时:申请证书(阿里云免费 DV 或 Let's Encrypt 通配符 *.quye.com)→ host nginx 加 listen 443 ssl(jail 侧不用动)→ 在 quye-headers.conf 加回 Strict-Transport-Security。
  2. 等 DNS 切换 :把 quye.com / www.quye.com 的 A 记录指到 39.97.8.60,证书续期才会真正生效。
  3. pf 开机自启 :往 /etc/rc.conf 加 pf_enable="YES"。
  4. 阿里云备案 :.com 解析到境内 ECS 必须备案,页脚替换真实备案号。

十、复盘:三条最值得带走的教训

  1. 先把"源可达 + 版本可升"验证掉。 受限网络里,pkg 源和 pkgbase 版本是最早、也最容易被低估的拦路虎。钉死 15.0、用阿里云内网镜像,是踩了超时之后才定的。

  2. jail 的"出不了网"是路由问题,不是权限问题。 单网卡 + NAT 环境下,给 jail 配一个本机不存在的子网别名,等于让它发出的包没有回程。要么 shared-IP(ip4=inherit)共享 loopback,要么把静态二进制拷进去,别妄想 pkg install。

  3. 证书续期失败,先查 DNS,别查配置。 HTTP-01 验证打的是域名真实解析目标。A 记录还指着老机器,新机器上怎么调配置都过不了 dry-run。这条能省下大把无意义排查。


本文由济宁极快软件部署过程的真实记录整理而成。相关技术资产与实战案例可在站内查看。

相关推荐
翔云1234561 小时前
RocksDB中,客户端每写入一次数据,需要访问几次磁盘
运维·数据库·mysql
爱莉希雅&&&2 小时前
GitLab 完整学习笔记
linux·运维·服务器
风笙9782 小时前
[文件系统]文件系统挂载与卸载详解
linux
GeW3 小时前
新质生产力路线图解读:数据库性能优化与Linux调优专场
linux·数据库
小杨爱科普4 小时前
制造业上自动化改造,改造部分的专利到底归谁
运维·经验分享·自动化
陈希1234 小时前
Linux 服务器性能排查手册(2026):CPU、内存、磁盘IO、网络四大瓶颈定位
linux·服务器·网络
方便面不加香菜5 小时前
Docker Container --容器及容器的操作案例详解
运维·docker·容器
测试界的小码农7 小时前
【Linux 命令实战(03):文件创建、复制、移动与删除,掌握 touch、mkdir、cp、mv、rm】
linux·服务器·php
没差c8 小时前
winsw将jar、nginx等注册为Windows服务
windows·nginx·jar