# nginx ↔ Caddy 共存方案（Phase 1 拓扑落地）

> **module**: internal-app-platform
> **doc_type**: ADR / ops design
> **status**: approved 2026-05-16，待实施（setup-test-server.sh 改造 + test 服务器 nginx 配置）
> **owner**: lijian.dai
> **last_verified**: 2026-05-16
> **supersedes**: 11-dns-tls-rollout.md §5（"Caddy 挂手工证书" 在 Phase 1 新拓扑下不再适用——TLS 在 nginx 终止）

## 1. 背景

Phase 0 dev 沙箱 `43.166.182.155` 是**专用机**，Caddy 独占 :80/:443，跑 [`setup-test-server.sh`](../../../scripts/internal-app-platform/setup-test-server.sh) 一遍齐活。

Phase 1 三环境（测试 / UAT / 生产）**全部和 FFOA 主站共机**：

| 环境 | 主机 | 已占 :80/:443 |
|---|---|---|
| 测试 | `170.106.161.71` | nginx（serving `ffworkspace.test.faradayfuturecn.com`） |
| UAT | `43.153.69.73` | nginx（serving FFOA UAT 主站） |
| 生产 | `43.130.6.44` | nginx（serving FFOA 生产主站） |

Caddy 容器直接绑 :80/:443 会和 nginx 冲突。原 `setup-test-server.sh` 上线即失败，且 11-dns-tls §5（Caddy 自己挂通配证书）失效——nginx 在主域名上已用 LE 证书 + certbot 续期。

## 2. 决策：nginx 前置 + Caddy 走 127.0.0.1:8080

**架构图**：

```
公网
  │
  ▼
┌──────────────────────────────────────────────────────────────┐
│ nginx :80/:443                                                │
│   ├─ server_name ffworkspace.test.faradayfuturecn.com         │
│   │    location /api/ → 127.0.0.1:5001 (backend pm2)          │
│   │    location /    → 127.0.0.1:5000 (frontend pm2)          │
│   │    TLS: /etc/letsencrypt/live/<main-domain>/              │
│   │                                                            │
│   └─ server_name *.apps.ffworkspace.test.faradayfuturecn.com  │
│        location / → 127.0.0.1:8080 (Caddy, HTTP)              │
│        TLS: wildcard cert（见 §4）                             │
└──────────────────────────────────────────────────────────────┘
                                  │ http (内网，明文)
                                  ▼
┌──────────────────────────────────────────────────────────────┐
│ ffoa-caddy 容器 (127.0.0.1:8080)                              │
│   /etc/caddy/Caddyfile import /etc/caddy/sites/*.caddy        │
│   /srv/caddy/sites/<emp>-<app>.caddy ← ffoa-deploy 写入        │
│   admin API :2019（仅 127.0.0.1）                              │
└──────────────────────────────────────────────────────────────┘
                                  │ http (ffoa-network)
                                  ▼
       per-app docker container (ffoa-app-<emp>-<app>)
```

**职责拆分**：

| 维度 | nginx | Caddy |
|---|---|---|
| 公网端口 | :80 / :443 | 无 |
| TLS 终止 | 主域名（已有）+ 通配（新增） | ❌ 不碰 cert |
| 路由策略 | 静态：主站 + `*.apps.*` 整段 proxy | 动态：per-app 反代 |
| reload 频率 | 罕见（仅加证书时 `nginx -t && reload`） | 高频（每次 deploy / destroy） |
| reload 方式 | `nginx -s reload`（master fork 平滑） | Caddy admin API（热） |

## 3. setup-test-server.sh 改造点

```diff
- docker run -d --name ffoa-caddy ...
-   -p 80:80 -p 443:443 -p 2019:2019 \
+ docker run -d --name ffoa-caddy ...
+   -p 127.0.0.1:8080:80 -p 127.0.0.1:2019:2019 \
    -v /srv/caddy:/etc/caddy ...
```

并加 **前置检测**：

```bash
# 共机模式（nginx 已占 :80）
if ss -tlnp 2>/dev/null | grep -qE ':80\s' && ! docker ps --format '{{.Names}}' | grep -q '^ffoa-caddy$'; then
  echo "[setup] 检测到 :80 已被占（疑似 nginx），走方案 B：Caddy 绑 127.0.0.1:8080"
  CADDY_BIND="127.0.0.1:8080:80"
  SKIP_UFW_80_443=1
else
  echo "[setup] :80 空闲，走独立模式：Caddy 绑 0.0.0.0:80/443"
  CADDY_BIND="80:80 -p 443:443"
fi
```

ufw 段：共机模式跳过 :80/:443（nginx 已开），只 deny :2019/:8080 公网。

## 4. wildcard cert 策略

`*.apps.ffworkspace.test.faradayfuturecn.com` 需要新 cert，不能复用主站 cert（CN/SAN 不覆盖）。

| 选项 | 适用 | 操作 |
|---|---|---|
| **LE DNS-01**（推荐 test/UAT）| DNS 在自动化平台 | `certbot certonly --manual --preferred-challenges dns -d '*.apps.ffworkspace.test.faradayfuturecn.com'`；renewal 跑 DNS hook |
| **公司内部 CA 长效证书** | 生产 | 走 IT 工单（11-dns-tls §3 模板），cert 放 `/etc/ssl/internal-app-platform/wildcard-apps.crt`，nginx 引用 |
| **自签**（仅 dev/test 应急）| 调通链路 | `openssl req -x509 -newkey rsa:2048 ...`，浏览器会告警，仅 curl `-k` 测试用 |

nginx `ssl_certificate` 路径与文件名按上面三选一统一到 `/etc/ssl/internal-app-platform/wildcard-apps.{crt,key}`。

## 5. nginx server block 模板

```nginx
# /etc/nginx/sites-available/internal-apps-wildcard
server {
    listen 80;
    listen [::]:80;
    server_name *.apps.ffworkspace.test.faradayfuturecn.com;

    location ^~ /.well-known/acme-challenge/ {
        root /var/www/html;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name *.apps.ffworkspace.test.faradayfuturecn.com;

    ssl_certificate     /etc/ssl/internal-app-platform/wildcard-apps.crt;
    ssl_certificate_key /etc/ssl/internal-app-platform/wildcard-apps.key;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    client_max_body_size 50M;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        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 https;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}
```

## 6. ffoa-deploy 模板调整

Caddy site 模板不再写 `tls` 段（HTTP 内网明文，TLS 由 nginx 终止）：

```diff
  <emp>-<app>.apps.ffworkspace.test.faradayfuturecn.com {
-     tls /etc/caddy/certs/wildcard-apps.crt /etc/caddy/certs/wildcard-apps.key
      reverse_proxy ffoa-app-<emp>-<app>:3000
  }
```

`reverse_proxy` 目标走 `ffoa-network` docker 内部别名，无变化。

## 7. 11-dns-tls-rollout.md §5 状态

| 子段 | 共机模式下的状态 |
|---|---|
| §5.1–5.5 "Caddy 挂手工证书"  | **废弃**（test/UAT/生产 全部走方案 B） |
| §6 "后续业务侧调整" | 保留（Gitea webhook URL / backend MCP URL / frontend API URL / 删 demo 路由 都还要做） |
| §9 "证书续期" | **改写**：监控对象从 `/srv/caddy/certs/wildcard-apps.crt` 改成 `/etc/ssl/internal-app-platform/wildcard-apps.crt`，alert 流程同 |

## 8. 回滚

如新 nginx server block 出问题：

```bash
sudo rm /etc/nginx/sites-enabled/internal-apps-wildcard
sudo nginx -t && sudo systemctl reload nginx
docker rm -f ffoa-caddy   # 可选，让 8080 端口空出来
```

主站 `ffworkspace.test.faradayfuturecn.com` 完全不受影响。

## 9. 后续演进（Phase 1.5+）

| 项 | 触发条件 | 备注 |
|---|---|---|
| 评估去 Caddy 化 | nginx site 动态生成的脚本稳定（C 方案：nginx 直接反代到容器） | 栈最简，但 ffoa-deploy 要重写 + integration test 要补 |
| 监控加 alert | per-app 5xx / latency / cert 剩余天数 | 沿用 Prometheus + grafana :5012（test 已有） |
| HTTP/3 | 主流浏览器普及 | nginx 1.25+ 原生支持，Caddy 内网仍 HTTP |

## 10. 相关文档

- [`08-phase-0-poc-runbook.md`](./08-phase-0-poc-runbook.md)：本设计的应用场景（员工接入 → 部署 → 访问）
- [`11-dns-tls-rollout.md`](./11-dns-tls-rollout.md)：本设计取代其 §5；§3/4/6 仍生效
- [`scripts/internal-app-platform/setup-test-server.sh`](../../../scripts/internal-app-platform/setup-test-server.sh)：本设计的安装脚本（按 §3 改造）
- [`scripts/internal-app-platform/nginx-site-wildcard.test.conf`](../../../scripts/internal-app-platform/nginx-site-wildcard.test.conf)：test 服务器 nginx server block 模板（§5）
- [`scripts/internal-app-platform/gen-self-signed-wildcard.sh`](../../../scripts/internal-app-platform/gen-self-signed-wildcard.sh)：test 环境自签通配 cert 生成脚本（§4 选项 3）
- [`.learnings/ERRORS/ERR-20260516-002-test-server-poc-infra-not-migrated.md`](../../../.learnings/ERRORS/ERR-20260516-002-test-server-poc-infra-not-migrated.md)：触发本设计的根因
