跳至内容
用功能角色命名子域名,管理自部署服务

用功能角色命名子域名,管理自部署服务

July 28, 2026

个人服务器从一两个服务扩张到五六个之后,第一个让人头疼的往往不是性能瓶颈,而是:“这个域名后面到底是什么?”

打开 DNS 面板看到一串 newapi.outsider-studio.cloudsub2api.outsider-studio.cloudmihomo.outsider-studio.cloudchatgpt-web.outsider-studio.cloud——每个都精确记录了当初装了什么软件。三个月后,你盯着域名列表问自己:“newapi 和 sub2api 的区别是什么来着?”

这不是记忆问题,是命名问题。

1. 问题:URL 绑了软件名,而不是功能

起初的直觉很简单:装了什么软件,就用它的名字做子域名。直观、好记、容易配置。

子域名后端软件功能
newapi.outsider-studio.cloudNewAPIAI 模型统一 API 网关
sub2api.outsider-studio.cloudSub2API订阅地址中继服务
mihomo.outsider-studio.cloudMihomo + Yacd代理管理面板
chatgpt-web.outsider-studio.cloudChatGPT Web UIAI 聊天界面

表面看没问题,但细想全是隐患:

换软件就得换 URL。 假如哪天想从 NewAPI 换到 LiteLLM,域名 newapi.outsider-studio.cloud 立刻变得名不副实,要么别扭地继续用,要么改 DNS 并通知所有使用者。后者更麻烦——你发现依赖这个域名的有桌面客户端、手机 App、Home Assistant 集成,改一次牵连一片。

新服务难以预测。 想加一个数据库管理面板。该给什么域名?Postgres?PgAdmin?如果以后换 MySQL 呢?名字绑软件之后,每次新增都要纠结一遍。

URL 暴露了技术栈。 公网域名直接泄露你在用什么软件——虽然对个人站威胁不大,但不算是好习惯。

问题的核心不是记忆力不够,是命名时把实现接口混在一起了。URL 是接口,应该承诺"这里提供什么服务",而不是"这里跑什么软件"。

2. 方案:用功能角色命名

解决方案极其简单:子域名描述这个地址做什么,而不是它背后用什么

改完之后的对应关系:

子域名当前软件功能角色端口
api.outsider-studio.cloudNewAPIAI API 网关:3000
relay.outsider-studio.cloudSub2API订阅中继:3090
proxy.outsider-studio.cloudMihomo + 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。改动只有两步:

  1. 更新 docker-compose.yml,新镜像占旧端口
  2. 打开浏览器确认能通

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. 扩展流程:加一个新服务

加入新服务时不需要任何决策,走流水线:

  1. 更新映射文档 —— 在 服务器服务映射.md 的表里加一行
  2. 启动 Docker 容器 —— docker compose up -d,端口绑 127.0.0.1
  3. 加 nginx 块 —— 在 proxy.conf 里复制一个 server 块,改 server_nameproxy_pass
  4. 签发证书 —— certbot --nginx -d 新域名.outsider-studio.cloud
  5. 检查生效 —— 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 即可