---
date: 2026-05-03
topic: 域名访问开发机时，frontend NEXT_PUBLIC_API_URL 必须用相对路径
tags: [nextjs, caddy, domain, dev-env, env]
---

# 现象

通过 Caddy 域名 `https://<name>.<user>.test.jiachentao.com/login` 访问开发机的 dev frontend，页面能加载、CSS/JS 正常，但**点登录按钮报 `ERR_CONNECTION_REFUSED`**：

```
localhost:4001/api/v1/auth/login  Failed to load resource: net::ERR_CONNECTION_REFUSED
```

# 根因

`frontend/.env` 里：

```
NEXT_PUBLIC_API_URL=http://localhost:4001/api/v1
```

`NEXT_PUBLIC_*` 在 Next.js build 时被注入到浏览器 bundle，所以 `localhost` 在**用户本机的浏览器**里被解析 — 用户本机当然没起后端 → 拒绝连接。

# 修复

把 `NEXT_PUBLIC_API_URL` 改成**相对路径** `/api/v1` 或**当前域名** `https://<domain>/api/v1`。下面示例用相对路径写脚本骨架（**项目源头实际选的是绝对域名**，见后文「两种方案对比」段），照搬时按需替换：

```bash
# 三个 env 文件都要改
for f in /worktree/.env /worktree/frontend/.env /worktree/backend/.env; do
  python3 -c "
import pathlib
p = pathlib.Path('$f')
c = p.read_text()
new = '\n'.join('NEXT_PUBLIC_API_URL=/api/v1' if l.startswith('NEXT_PUBLIC_API_URL=') else l for l in c.split('\n'))
with open(p,'r+') as f: f.seek(0); f.write(new); f.truncate()
"
done
```

然后**重启** `next dev`（`NEXT_PUBLIC_*` 在 bundle 编译时注入，hot reload 不会重新注入）：

```bash
pkill -f "next dev.*-p <port>"
PORT=<port> npm run dev
```

两种方案对比：

| 方案 | 域名访问 | 本机直连 `localhost:<port>` | 多域名复用 |
|---|---|---|---|
| 相对 `/api/v1` | 同源 → Caddy 反代 ✓ | Next.js `rewrites` 转 backend ✓ | 任意域名同源 ✓ |
| 绝对 `https://<domain>/api/v1` | 同 origin → Caddy 反代 ✓ | 仍 hit 域名（不能本机直连） | 锁死单域名 |

相对路径在通用性上略胜，**但项目实际选了绝对域名**——`scripts/dev/setup-worktree.sh:517` 自 commit `7637af53`（2026-04-29）起注入 `NEXT_PUBLIC_API_URL=https://${domain}/api/v1`。开发流程默认走 Caddy 域名，不依赖直连 `localhost`，所以"本机直连"那一栏的劣势被实际工作流避开。

# 老 worktree 修复

`7637af53` 之前创建的老 worktree 没自动注入，需要手动跑上面的修复脚本（把脚本里的 `/api/v1` 换成 `https://<domain>/api/v1` 以与源头一致）。

# 诊断 checklist（域名访问失败时跑一遍）

三个层面都查：

1. **Caddyfile bind mount inode** — `sed -i` 改 host 文件会断 bind，`docker exec caddy md5sum /etc/caddy/Caddyfile` 与 host 端 md5 对比；不一致就 `docker restart caddy`（详见 [2026-05-03-caddyfile-bind-mount-inode-broken.md](2026-05-03-caddyfile-bind-mount-inode-broken.md)）
2. **Caddy 反代端口** vs **本机实际 listening 端口** — 4000 端口被 NoMachine/NX 占了，dev frontend 必须用 4010+；`ss -tlnp | grep :<port>` 验证 Caddyfile 配的反代端口跟实际监听一致
3. **frontend NEXT_PUBLIC_API_URL** — 必须相对路径或当前域名，绝对 `localhost:<port>` 必跪（本文主题）

# 教训

任何"前端打包变量带 localhost"在 BFF / proxy / 跨域访问场景下都会变陷阱。原则：**bundle 里不要硬编码 `localhost:<port>`**——用相对路径让基础设施层（dev rewrite / Caddy / nginx）决定路由，或用稳定的对外域名。两种都行，硬编码 localhost 不行。

下次见到 `Failed to load resource: net::ERR_CONNECTION_REFUSED` 且地址是 `localhost:<port>` 时，第一直觉应该是检查前端 `NEXT_PUBLIC_*` 环境变量。
