问题背景
一个 Spring Boot 服务上线后,往往不只是「一个 Jar 包跑起来」这么简单。你的架构图上会有:
- 前端静态资源:Vue/React 构建产物
- API 后端:Spring Boot 容器(可能不止一个)
- 管理面板:PGAdmin、RabbitMQ 控制台等工具
- 上传资源:/uploads 目录的静态映射
你需要在同一台机器上暴露 443 端口,让所有这些服务共用同一个域名/端口,同时自动搞定 HTTPS。大多数人的第一反应是 Nginx + Certbot,但维护成本逐年走高:证书要手动续、CRON 要配、配置要分 site-available / sites-enabled。
本文将拆解我生产环境使用的方案——Caddy 作为 Docker 网关,前置代理多个 Spring Boot 服务和静态站点,一个 Caddyfile 解决所有路由+HTTPS。
为什么是 Caddy 而不是 Nginx
| 维度 | Nginx + Certbot | Caddy |
|---|---|---|
| HTTPS 配置 | 手动配 SSL 块 + certbot 定期续约(CRON) | 自动申请/续约 Let's Encrypt,零配置 |
| 配置复杂度 | 每个站点要 server{} 块、ssl_certificate、proxy_pass | 一行 reverse_proxy,自动 HTTPS |
| 重载部署 | nginx -s reload 或 systemctl reload | caddy reload,原子切换不下线 |
| Docker 集成 | 需额外处理容器间网络、卷挂载认证 | 官方镜像,天然适配 Compose 网络 |
简单说:如果你的目标是「快速、安全、少维护」地上线一组 Docker 服务,Caddy 比 Nginx 更省心。Nginx 在超高并发静态资源吞吐上仍有优势,但对大多数微服务中台场景,Caddy 的自动 HTTPS 和无缝重载是更实际的收益。
整体架构
Docker Compose 编排如下:
# compose.yaml
services:
caddy:
image: caddy:2.9-alpine
restart: unless-stopped
cap_add:
- NET_ADMIN # HTTP/3 QUIC 所需
ports:
- "80:80"
- "443:443"
- "443:443/udp" # QUIC
volumes:
- ./caddy:/etc/caddy # Caddyfile 目录(注意是目录不是文件!)
- caddy_data:/data # 证书持久化——最重要的一步
- caddy_config:/config
networks:
- web
app-backend:
image: app-backend:latest
restart: unless-stopped
expose:
- "8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
volumes:
- uploads_data:/app/uploads
networks:
- web
admin-tool:
image: dpage/pgadmin4
restart: unless-stopped
expose:
- "80"
networks:
- web
volumes:
caddy_data: # 外部名=myproject_caddy_data(防止 docker compose down 丢失)
caddy_config:
uploads_data:
networks:
web:
⚠️ 黄金规则:/data 卷必须用 named volume + external 声明,绝不能是 bind mount 或匿名卷。Caddy 在此目录存储证书私钥和 OCSP 装订信息,丢失意味着所有证书重新申请,触发 Let's Encrypt 速率限制。
Caddyfile 配置实战
以下是我生产环境的 Caddyfile(已脱敏):
# 全局配置
{
email admin@example.com
# 默认 admin API 只监听容器内 localhost,不对外暴露
admin localhost:2019
}
# 主站:blog.example.com — 静态站点 + API 反代
blog.example.com {
# 静态根目录
root * /srv/site-a
file_server
# API 反向代理到 Spring Boot 后端
reverse_proxy /api/* app-backend:8080 {
# 保留原始 Host 头
header_up Host {host}
# 传递真实客户端 IP
header_up X-Real-IP {remote_host}
}
# 上传资源直接映射
reverse_proxy /uploads/* app-backend:8080
# 管理后台子路径
reverse_proxy /admin/* app-backend:8080
# 日志
log {
output file /data/access.log {
roll_size 100mb
roll_keep 5
}
}
}
# 子站 shop.example.com — 另一个 Spring Boot 服务(或纯静态)
shop.example.com {
root * /srv/site-b
file_server
reverse_proxy /api/* pay-backend:8080
}
# Snippet 复用(生产环境加安全头)
(security) {
header {
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
Referrer-Policy "strict-origin-when-cross-origin"
}
}
关键细节
- Caddyfile 必须是目录挂载:挂载
./caddy/到/etc/caddy(目录级),而不是./Caddyfile:/etc/caddy/Caddyfile(文件级)。后者会导致caddy reload失败,因为 Caddy 需要在配置目录写入临时文件。 - 容器间通信用服务名:
app-backend:8080直接解析到 Docker 网络内的容器 IP,不用处理端口映射——后端容器只需expose而不需要ports,减少攻击面。 - 路径精确匹配:
reverse_proxy /api/* backend:8080只代理/api/开头的请求,不会把前端路由也转发到后端。
优雅重载:零停机配置更新
修改 Caddyfile 后,不需要 docker compose restart caddy(会导致几秒连接中断),而是用:
# 验证配置语法(推荐先跑这一步)
docker compose exec caddy caddy validate --config /etc/caddy/Caddyfile
# 零停机重载
docker compose exec -w /etc/caddy caddy reload
caddy reload 的工作原理:向 admin API 发送新配置 → Caddy 解析并应用 → 如果新配置无效则自动回滚 → 已有连接继续运行不受影响。整个过程毫秒级原子切换。
配置重载出错自动回滚(不会炸服务),但 validate 步骤可以提前拦截语法错误,推荐脚本化:
#!/bin/bash
# caddy-reload.sh — CI/CD 部署后调用
set -e
cd /opt/myproject
docker compose exec caddy caddy validate --config /etc/caddy/Caddyfile
docker compose exec -w /etc/caddy caddy reload
echo "Caddy reloaded OK"
踩坑记录
证书丢失怪现象
现象:服务正常运行一段时间后,浏览器突然报 NET::ERR_CERT_COMMON_NAME_INVALID。排查发现docker compose down+up时,匿名卷被删除,Caddy 重新申请证书但尚未完成。
解决:改名卷为caddy_data,并在volumes段声明external: true或至少用顶级 named volume 确保down不删除。Spring Boot 获取不到真实 IP
现象:后端日志里客户端 IP 全是 Docker 网关 IP(172.x.x.x)。
原因:Caddy 反代后默认不传递真实 IP。
解决:Caddyfile 加header_up X-Real-IP {remote_host},Spring Boot 侧加server.forward-headers-strategy=framework(Spring Boot 2.2+)。文件上传大小限制
Caddy 默认限制请求体 32MB,上传大文件会被截断返回 413。
解决:在站点块加request_body <size>,如request_body 100MB。
最佳实践总结
- ✅ 用 named volume 持久化
/data(证书)和/config - ✅ 构建 Caddyfile 时用
(snippet)提取通用安全头/日志,通过import复用 - ✅ 所有后端容器只
expose端口,不要映射到宿主机——减少攻击面 - ✅ 部署脚本中先
caddy validate再caddy reload - ✅ Spring Boot 开启
forward-headers-strategy=framework获取真实客户端 IP - ❌ 切忌直接将 Caddyfile 作为文件挂载——必须是目录挂载
- ❌ 切忌用
:latest标签——���版本号如caddy:2.9-alpine
适用场景与局限
这套方案最适合 单机多服务 或 小规模微服务中台:
- ✅ 2-10 个后端容器的 Docker 部署
- ✅ 需要自动 HTTPS 且不想维护 Certbot CRON
- ✅ 前端 SPA + Java 后端的标准 Web 架构
- ❌ 大规模 K8s 集群——此时 Traefik 或 Ingress Controller 更强
- ❌ 纯静态文件巨量吞吐——Nginx 仍有性能优势
但对本文目标场景——一台 VPS,几个 Spring Boot 服务,N 个前端站点——Caddy 是最简洁、最安全的入口网关,没有之一。