Caddy Docker 反向代理:带自动 HTTPS 的 Compose 配置
将 Caddy 作为 Docker 反向代理运行:一个 compose 栈、一个共享网络、按服务名 reverse_proxy 并自动 HTTPS,以及何时适合使用 caddy-docker-proxy 标签。
简答:让 Caddy 运行在独立容器中并发布 80 和 443 端口,把它和你的应用容器接入同一个共享 Docker 网络,然后让 `reverse_proxy` 指向各个服务名和内部端口(例如 `reverse_proxy app:3000`)。Caddy 通过 Docker 内置 DNS 解析服务名,并自动为每个站点地址获取 TLS 证书。
本指南先介绍纯 Caddyfile 方案,再介绍基于标签的 caddy-docker-proxy 方案,并说明在单台 VPS 上各自适用的场景。如果你直接在宿主机而非 Docker 中运行 Caddy,请参阅基础的 Caddy 反向代理指南。
各组件如何协同工作
只有 Caddy 容器向外部发布端口。每个后端容器都加入一个用户自定义的 bridge 网络(这里叫 `web`),并且只在该网络上暴露端口。用户自定义网络提供的 Docker 内置 DNS 让 Caddy 能通过服务名或容器名访问 `app:3000`,这样你就无需硬编码每次重建都会变化的容器 IP。
不发布后端端口比看上去更重要:Docker 会写入自己的 iptables 规则,因此即使 ufw 显示已拦截,用 `ports:` 发布的端口仍可能从公网访问。凡是应该位于代理之后的服务,请使用 `expose:` 或完全不做端口映射。
| 组件 | 作用 | 是否在宿主机发布? |
|---|---|---|
| caddy 容器 | TLS 终止、按主机名路由、HTTP 到 HTTPS 重定向 | 是:80/tcp、443/tcp、443/udp |
| web 网络 | 共享 bridge 网络,内置服务名 DNS | 否 |
| 应用容器 | 你的服务(Web 应用、API、仪表盘) | 否,仅能通过 Caddy 访问 |
| caddy_data 卷 | 证书、私钥、ACME 账户 | 否,但必须持久化 |
分步操作:纯 Caddyfile + docker compose
- 01.在启动 Caddy 之前,把每个主机名的 A 记录(如使用 IPv6 还需 AAAA 记录)指向 VPS,否则证书签发会失败。
- 02.创建一次共享网络:`docker network create web`。
- 03.按下文所示创建 `/opt/proxy/compose.yaml` 和 `/opt/proxy/Caddyfile`,然后在 `/opt/proxy` 中运行 `docker compose up -d`。
- 04.将每个应用栈接入外部 `web` 网络,并用 `docker compose up -d` 启动。
- 05.在 `/opt/proxy` 中用 `docker compose logs -f caddy` 观察首张证书的签发过程。
- 06.编辑 Caddyfile 后无停机重载:`docker compose exec -w /etc/caddy caddy caddy reload`。
# /opt/proxy/compose.yaml
services:
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp" # HTTP/3
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data # certificates and ACME account keys
- caddy_config:/config
networks:
- web
volumes:
caddy_data:
caddy_config:
networks:
web:
external: true接入应用并编写 Caddyfile
每个应用栈都声明同一个外部网络。服务名就是 Caddy 要连接的主机名。`caddy_data` 卷绝不能丢失:删除它会迫使 Caddy 重新申请所有证书,可能触发 ACME 速率限制。
# /opt/app/compose.yaml
services:
app:
image: ghcr.io/example/app:latest
restart: unless-stopped
expose:
- "3000" # reachable on the network, NOT published on the host
networks:
- web
networks:
web:
external: true
# /opt/proxy/Caddyfile
{
email [email protected]
}
app.example.com {
reverse_proxy app:3000
}
grafana.example.com {
reverse_proxy grafana:3000
}
api.example.com {
reverse_proxy api-1:8080 api-2:8080 {
lb_policy round_robin
health_uri /healthz
health_interval 10s
}
}Caddy 后端:上游、负载均衡与健康检查
在 Caddy 的术语中,后端就是 `reverse_proxy` 的上游(upstream)。你可以在一条指令中列出多个上游,选择 `round_robin`、`least_conn` 或 `ip_hash` 等 `lb_policy`,并用 `health_uri` 添加主动健康检查,让故障容器退出轮询。当应用没有健康检查端点时,被动检查(`fail_duration`、`max_fails`)也很有用。
Caddy 默认会在代理请求上设置 `X-Forwarded-For`、`X-Forwarded-Proto` 和 `X-Forwarded-Host`。请将应用配置为信任代理所在的网段,这样它才能看到真实客户端 IP 并生成 `https://` 链接。WebSocket 和 gRPC 无需额外设置即可通过 `reverse_proxy`;对于明文 gRPC,使用 `reverse_proxy h2c://service:50051`。
用标签代替 Caddyfile:caddy-docker-proxy
搜索“Caddy proxy manager”的人,通常想要的是 Nginx Proxy Manager 那样的体验:添加一个容器,无需编辑中心配置文件就能获得主机名和证书。Caddy 没有官方图形界面,但社区项目 caddy-docker-proxy(github.com/lucaslorentz/caddy-docker-proxy)会根据容器标签生成 Caddyfile,并在容器启动或停止时重载 Caddy。
它需要读取 Docker socket,这实际上等同于宿主机的 root 权限。请以只读方式挂载,保持代理镜像更新;如果你只运行少量服务,优先选择纯 Caddyfile。
services:
caddy:
image: lucaslorentz/caddy-docker-proxy:ci-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
environment:
- CADDY_INGRESS_NETWORKS=web
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- caddy_data:/data
networks:
- web
whoami:
image: traefik/whoami
networks:
- web
labels:
caddy: whoami.example.com
caddy.reverse_proxy: "{{upstreams 80}}"
volumes:
caddy_data:
networks:
web:
external: true纯 Caddyfile 与 caddy-docker-proxy 对比
| 纯 Caddyfile | caddy-docker-proxy 标签 | |
|---|---|---|
| 配置存放位置 | 一个纳入 git 版本管理的文件 | 分散在各个 compose 文件的标签中 |
| Docker socket 访问 | 不需要 | 需要(安全敏感) |
| 新增服务 | 编辑 Caddyfile,运行 `caddy reload` | 添加标签,启动容器 |
| 镜像 | 官方 `caddy:2` | 基于 Caddy 的社区镜像 |
| 调试 | 直接阅读文件 | 在日志中查看生成的配置 |
| 最适合 | 小型、稳定的 VPS 部署 | 大量短生命周期或频繁变更的服务 |
在 VPS 上运行
在 NoctisNode KVM VPS 上,你拥有完整 root 权限和独立 IPv4,因此由一个 Caddy 容器发布 80 和 443、隐藏其他所有服务非常简单。Wisp(1 vCPU、1 GB 内存)足以运行 Caddy 加几个轻量容器;更重的技术栈适合 Pulse 或 Umbra。证书签发细节和故障模式见 Caddy 自动 HTTPS 的工作原理,原始 TCP 或 SSH 转发见 Caddy TCP 与 SSH 代理。用 SSH 加固指南锁定宿主机 SSH。
常见问题
Q如何让 Caddy 反向代理到 Docker 容器?
A把 Caddy 和该容器放在同一个用户自定义 Docker 网络中,并在 Caddyfile 中使用 `reverse_proxy service-name:port`。不要使用 `localhost`,因为在 Caddy 容器内它指的是 Caddy 自己。
Q为什么 Caddy 对我的容器返回 502 Bad Gateway?
A通常是容器不在共享网络中、端口错误(应使用容器的内部端口,而非宿主机发布端口),或应用在容器内只监听 127.0.0.1。让应用绑定 0.0.0.0,并检查 `docker network inspect web`。
Qcaddy-docker-proxy 安全吗?
A它运行良好,但需要 Docker socket,这相当于授予宿主机的 root 级控制权。以只读方式挂载,保持镜像更新,并避免暴露 Caddy 管理端点。
QDocker 中的 Caddy 把证书存在哪里?
A在官方镜像内的 `/data`,因此应在该处挂载一个命名卷。丢失它意味着要重新签发所有证书,并可能触发 ACME 速率限制。
Q有带 Web 界面的 Caddy proxy manager 吗?
ACaddy 本身没有官方 Web 界面;它通过 Caddyfile、JSON 或管理 API 配置。caddy-docker-proxy 等基于标签的工具提供了类似“添加容器即可使用”的工作流。