用功能角色命名子域名,管理自部署服务
个人服务器从一两个服务扩张到五六个之后,第一个让人头疼的往往不是性能瓶颈,而是:“这个域名后面到底是什么?”
打开 DNS 面板看到一串 newapi.outsider-studio.cloud、sub2api.outsider-studio.cloud、mihomo.outsider-studio.cloud、chatgpt-web.outsider-studio.cloud——每个都精确记录了当初装了什么软件。三个月后,你盯着域名列表问自己:“newapi 和 sub2api 的区别是什么来着?”
这不是记忆问题,是命名问题。
1. 问题:URL 绑了软件名,而不是功能
起初的直觉很简单:装了什么软件,就用它的名字做子域名。直观、好记、容易配置。
| 子域名 | 后端软件 | 功能 |
|---|---|---|
| newapi.outsider-studio.cloud | NewAPI | AI 模型统一 API 网关 |
| sub2api.outsider-studio.cloud | Sub2API | 订阅地址中继服务 |
| mihomo.outsider-studio.cloud | Mihomo + Yacd | 代理管理面板 |
| chatgpt-web.outsider-studio.cloud | ChatGPT Web UI | AI 聊天界面 |
表面看没问题,但细想全是隐患:
换软件就得换 URL。 假如哪天想从 NewAPI 换到 LiteLLM,域名 newapi.outsider-studio.cloud 立刻变得名不副实,要么别扭地继续用,要么改 DNS 并通知所有使用者。后者更麻烦——你发现依赖这个域名的有桌面客户端、手机 App、Home Assistant 集成,改一次牵连一片。
新服务难以预测。 想加一个数据库管理面板。该给什么域名?Postgres?PgAdmin?如果以后换 MySQL 呢?名字绑软件之后,每次新增都要纠结一遍。
URL 暴露了技术栈。 公网域名直接泄露你在用什么软件——虽然对个人站威胁不大,但不算是好习惯。
问题的核心不是记忆力不够,是命名时把实现和接口混在一起了。URL 是接口,应该承诺"这里提供什么服务",而不是"这里跑什么软件"。
2. 方案:用功能角色命名
解决方案极其简单:子域名描述这个地址做什么,而不是它背后用什么。
改完之后的对应关系:
| 子域名 | 当前软件 | 功能角色 | 端口 |
|---|---|---|---|
| api.outsider-studio.cloud | NewAPI | AI API 网关 | :3000 |
| relay.outsider-studio.cloud | Sub2API | 订阅中继 | :3090 |
| proxy.outsider-studio.cloud | Mihomo + Yacd | 代理管理 | :9091 / :9090 |
| link.outsider-studio.cloud | (未定) | 短链跳转 | — |
| monitor.outsider-studio.cloud | (未定) | 服务监控 | — |
命名规则就是一句话:单英文词,描述服务做什么,不绑定软件名。
api是 AI API 网关,不管下面是 NewAPI 还是 LiteLLM,对外都是同一个入口relay是中继转发,换乘 Sub2API 还是其他工具,URL 不变proxy是代理管理,后台从 Mihomo 换成 Clash 或者其他,用户仍然访问https://proxy.outsider-studio.cloud/
每个词都回答一个清晰的问题:“我能在这个地址得到什么服务?"——“API 调用”、“订阅中继”、“代理控制”。
不需要知道的:“那些服务具体用了什么软件跑起来的。”
3. 为什么重要:可替换性与可预测性
3.1 软件可替换
这是最大的收益。2026 年 7 月做了一次迁移——AI API 网关从 NewAPI 换成了 One API。改动只有两步:
- 更新
docker-compose.yml,新镜像占旧端口 - 打开浏览器确认能通
URL 没变,客户端没变,DNS 没变,nginx 配置没变。api.outsider-studio.cloud 这个域名完全不知道下面换了软件——也不应该知道。
3.2 新增可预测
现在想加新服务,不需要想名字——直接看功能属于哪个类别:
- 需要一个数据库管理界面 →
db.outsider-studio.cloud - 加个 CI/CD 构建状态 →
ci.outsider-studio.cloud - 搭个知识库文档站 →
docs.outsider-studio.cloud - 上 uptime 监控 →
monitor.outsider-studio.cloud - 文件同步管理 →
sync.outsider-studio.cloud
所有名字都是单英文词、全小写、描述功能。不需要发明、不需要记录、不需要解释。告诉别人 “docs.outsider-studio.cloud 是文档站”,对方立刻理解。
3.3 命名空间干净
旧的 sub2api.outsider-studio.cloud 有四个音节。新的 relay.outsider-studio.cloud 两个。读起来、写起来、分享起来都更轻。如果你经常在命令行切换上下文,短的子域能省不少键盘时间。
4. Nginx 反代配置
命名体系落实到最后,就是一组干净的 nginx server 块,每个对应一个功能子域。
下面是服务器上实际的 proxy.conf,全部功能域归入同一个文件:
# /etc/nginx/conf.d/proxy.conf
#
# 命名规则:单英文词,描述服务做什么,不绑定软件名
# ============================ api.outsider-studio.cloud => :3000 ============================
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name api.outsider-studio.cloud;
ssl_certificate /etc/letsencrypt/live/outsider-studio.cloud/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/outsider-studio.cloud/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
add_header Strict-Transport-Security "max-age=31536000" always;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 600s;
proxy_buffering off;
}
}
# ============================ relay.outsider-studio.cloud => :3090 ============================
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name relay.outsider-studio.cloud;
ssl_certificate /etc/letsencrypt/live/outsider-studio.cloud/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/outsider-studio.cloud/privkey.pem;
# ... SSL 参数同上,可抽取到 snippet ...
location / {
proxy_pass http://127.0.0.1:3090;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 600s;
proxy_buffering off;
}
}
# ============================ proxy.outsider-studio.cloud => :9091 ============================
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name proxy.outsider-studio.cloud;
ssl_certificate /etc/letsencrypt/live/outsider-studio.cloud/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/outsider-studio.cloud/privkey.pem;
# ... SSL 参数同上 ...
location / {
proxy_pass http://127.0.0.1:9091; # Yacd 面板
}
location /api/ {
proxy_pass http://127.0.0.1:9090/; # Mihomo 控制器
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400s;
}
}注意 proxy.outsider-studio.cloud 这个例子特殊:它把面板 UI(Yacd,:9091)和控制 API(Mihomo,:9090)合在同一个域名下,靠 location 分流——面板前端通过 /api/ 调后端,避免跨域和混合内容的麻烦。功能角色仍然是"代理管理”。
HTTP 到 HTTPS 的跳转可以留给统一入口(或用 certbot 的 if ($host) 块处理),在主站配置里补一段 map 处理 WebSocket 的 Upgrade 头:
# /etc/nginx/nginx.conf — http 块内
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}5. 文档:一张表管所有服务
命名方案解决的是"怎么叫",但运维还需要知道"叫什么在什么端口、如何访问、密钥在哪"。一张单页文档就够了。
建一个 docs/服务器服务映射.md 文件,放在 Hugo 站点同层目录(不进博客,但版本管理):
# 服务器服务映射
更新:2026-07-28
服务器:Tencent LightHouse, 124.222.69.84
OS:Ubuntu 22.04
域名:outsider-studio.cloud(Cloudflare DNS)
## 服务总表
| 子域名 | 功能 | 软件 | 端口 | 部署路径 | 认证方式 |
| ------ | ---- | ---- | ---- | -------- | -------- |
| api | AI API 网关 | NewAPI -> One API | :3000 | ~/stack/api/ | 虚拟 Key |
| relay | 订阅中继 | Sub2API | :3090 | ~/stack/relay/ | 路径密钥 |
| proxy | 代理管理 | Mihomo + Yacd | :9090/:9091 | ~/stack/proxy/ | 面板密码 |
| link | 短链跳转 | — | — | — | 未部署 |
| monitor| 服务监控 | — | — | — | 未部署 |
## 证书
- 提供商:Let's Encrypt(Certbot)
- SAN 域:outsider-studio.cloud, *.outsider-studio.cloud
- 证书路径:/etc/letsencrypt/live/outsider-studio.cloud/
- 续期:systemctl certbot.timer
- 下次续期:2026-10-26
## DNS(Cloudflare)
| 类型 | 名称 | 值 | 代理 |
| ---- | ---- | --- | ---- |
| A | @ | 124.222.69.84 | 橙云 |
| A | api | 124.222.69.84 | 橙云 |
| A | relay | 124.222.69.84 | 橙云 |
| A | proxy | 124.222.69.84 | 橙云 |
| A | * | 124.222.69.84 | 橙云 |
## 敏感信息(不写在此文件)
- Docker .env / 数据库密码
- API 虚拟 Key
- ssh 私钥路径
- 面板登录凭据这张表的价值在于:所有信息集中在一页,一个人维护就够了。 找人交接服务时发这一个文件,而不是翻遍十几个目录找 docker-compose.yml 里暴露的端口。
6. 扩展流程:加一个新服务
加入新服务时不需要任何决策,走流水线:
- 更新映射文档 —— 在
服务器服务映射.md的表里加一行 - 启动 Docker 容器 ——
docker compose up -d,端口绑127.0.0.1 - 加 nginx 块 —— 在
proxy.conf里复制一个server块,改server_name、proxy_pass - 签发证书 ——
certbot --nginx -d 新域名.outsider-studio.cloud - 检查生效 ——
curl -I https://新域名.outsider-studio.cloud
全过程大约五分钟,其中两分钟在复制粘贴改名字。不需要查端口有没有冲突、不需要想域名叫什么、不需要担心 HTTPS。
7. 回顾
这次改造没有引入新技术栈、没有改架构、没有买新工具——只是改了命名方式。
回头看,旧的命名方式犯了一个很容易犯的错:把"我装了什么"和"我提供了什么"混在一起说。
命名这件事,看起来是"叫什么"的问题,实际上是"这个东西的本质是什么"的问题。软件名是偶然的——你选 NewAPI 而不选 LiteLLM,可能是某篇博客推荐的结果、可能是某次随手 docker pull 的结果。但 api 这件事不是偶然的——你的服务器确实需要开放一个 API 入口。
命名是架构的一部分。用功能命名,你在向未来的自己承诺:这个地址承诺提供这个服务,底层可能换、承诺不变。
可执行的 checklist
- 所有子域名检查一遍:域名是否描述"做什么"而不是"用什么"
- 已废弃的旧软件名子域:加 301 跳转到新域名,客户端平滑过渡
-
proxy_pass指向127.0.0.1,不暴露端口 - 所有业务容器的端口绑
127.0.0.1,不绑0.0.0.0 - Certbot 续期定时任务正常
- 映射文档已更新,存在 Hugo 源码目录的同级
docs/下 -
*泛域名 A 记录到位,新增服务时直接certbot --nginx即可