---
date: 2026-05-03
topic: 改 Caddyfile 没生效 — bind mount 因 inode 变化失联
tags: [caddy, docker, bind-mount, dev-env]
---

# 现象

改了 `/home/chentao/caddy-config/Caddyfile`，`docker exec caddy caddy reload` 也成功了，但请求依然按旧配置走（502 / EOF / 旧 upstream port）。

排查时发现：
```bash
md5sum /home/chentao/caddy-config/Caddyfile         # 新内容
docker exec caddy md5sum /etc/caddy/Caddyfile       # 旧内容（不一致！）
stat -c '%i' /home/chentao/caddy-config/Caddyfile   # inode 917935
docker exec caddy stat -c '%i' /etc/caddy/Caddyfile # inode 917721（不同！）
```

# 根因

Docker bind mount 单个文件（不是目录）时，**绑定的是 inode 而不是路径**。当用以下任何方式修改文件，会创建**新 inode**：

- `sed -i ...`（默认行为：写新文件 + rename）
- `python3 pathlib.Path.write_text(...)`（同上：rm + create）
- `vim` / `vi`（默认 `:set backup` 等同行为）
- `mv newfile origfile`

新 inode 创建瞬间，容器内绑定的还是旧 inode（被 unlink 的孤儿 inode 还活着，因为容器持有引用），所以容器看到的是旧内容。

`caddy reload` 读的是容器内 `/etc/caddy/Caddyfile`（旧 inode → 旧内容），所以 reload "成功" 但其实 load 了旧配置。

# 修复

## 临时（已经踩坑了）

```bash
docker restart caddy   # 重启容器，重新解析 mount path 拿到当前 inode
```

## 正确做法（编辑时保 inode）

用 in-place truncate + write，不要 rename：

```python
with open(path, 'r+') as f:
    f.seek(0)
    f.write(new_content)
    f.truncate()
```

或 shell：

```bash
# 用 cat 重定向，保留 inode
new_content="..."; printf '%s' "$new_content" > /home/chentao/caddy-config/Caddyfile

# 或 sed 的真·in-place（不所有版本支持）
sed -i.bak --follow-symlinks '...' /file && rm /file.bak

# 直接 dd
dd of=/file conv=notrunc if=/dev/stdin
```

**特别坑的工具**：`sed -i` 默认会破坏 inode（GNU sed 在 4.2+ 中默认 rename），即使加 `-c` / `--copy` 也只是改了实现，bind mount 仍然失联。

# 验证

每次 reload 后必须验证：

```bash
diff <(md5sum /host/path | cut -d' ' -f1) \
     <(docker exec container md5sum /container/path | cut -d' ' -f1) \
   && echo "OK: bind mount intact" \
   || echo "BROKEN: restart container or reapply mount"
```

# 教训

- bind mount **目录**没有这个问题（绑定的是目录 inode，子文件创建/删除/rename 都不影响）
- bind mount **单个文件**是脆弱的，任何"现代"编辑器/脚本都可能破坏它
- 最佳实践：要 bind 配置文件，**bind 整个 config 目录**（如 `/home/chentao/caddy-config/` → `/etc/caddy/`），让单文件可以自由替换
- 这次能定位是因为：最后试 `respond "hello"` 测试 reverse_proxy 是否本身就坏了，发现 `respond` 也返回 502，才意识到根本不是 reverse_proxy 的事

# 适用范围

任何用 docker bind mount 单文件的场景：
- Caddy / nginx / haproxy 配置文件
- envoy bootstrap.yaml
- 任意应用 config.{json,yaml,toml}
- 也包括 `~/.gitconfig` 之类挂进容器的场景

如果开发流程涉及频繁改 bind 的单文件，强烈建议改成目录 bind 或者用 ConfigMap 模式（生成独立目录后整体 rename）。
