如果你在 VPS 上部署过 Node.js、Next.js、WordPress、Directus、n8n、Vaultwarden、Uptime Kuma 或任何自托管应用,很快就会遇到同一个问题:应用通常只监听 127.0.0.1:3000、localhost:8080 或 Docker 内部端口,真正对外访问时却需要域名、HTTPS、证书续期、HTTP 到 HTTPS 跳转、WebSocket、日志和多站点管理。
传统方案经常是 Nginx 加 Certbot。它很强,但对新手来说有两个痛点:配置块容易写错,证书续期还要额外维护。Caddy 的价值就在这里:它默认自动申请和续期 TLS 证书,Caddyfile 语法非常短,适合 VPS 上快速把多个服务整理成可维护的入口。
本文会从零开始讲清楚:如何在 VPS 上安装 Caddy,如何配置 DNS,如何反向代理本机应用和 Docker 应用,如何同时托管多个域名,如何处理 WebSocket、静态文件、日志、重载和常见错误。
Caddy 最适合这些 VPS 场景:
- 你有多个服务跑在不同端口,例如
3000、5678、8080。 - 你希望用
app.example.com、api.example.com、status.example.com分别访问不同应用。 - 你不想手动管理 Let's Encrypt 证书和续期任务。
- 你希望配置文件短、可读、容易回滚。
- 你部署的是自托管应用、Docker Compose、个人网站、内部工具或小型业务服务。
Caddy 不等于“只能新手用”。它的自动 HTTPS、反向代理、静态文件服务、压缩、日志、访问控制、header 改写都很完整。只是它把常见 HTTPS 站点的默认配置做得更省心。
开始前,你需要准备:
- 一台 VPS,推荐 Ubuntu 22.04 / 24.04 或 Debian 12。
- 一个域名,并能修改 DNS 解析。
- VPS 公网 IPv4 或 IPv6 地址。
- 服务器开放
80和443端口。 - 一个已经在本机端口运行的应用,例如
127.0.0.1:3000。
先确认服务器时间正常:
timedatectl
如果时间明显错误,TLS 证书申请可能失败。大多数云服务器默认已开启 NTP,不需要额外处理。
假设你的域名是 example.com,VPS IPv4 是 203.0.113.10。
常见解析方式如下:
A app.example.com 203.0.113.10
A api.example.com 203.0.113.10
A status.example.com 203.0.113.10
如果你使用 IPv6,则添加 AAAA 记录:
AAAA app.example.com 2001:db8::10
如果你使用 Cloudflare,有两种模式:
- 只想让 Caddy 自己申请证书:先把小云朵设为 DNS only,等证书成功后再考虑代理模式。
- 使用 Cloudflare 代理:SSL/TLS 模式建议使用 Full 或 Full (strict),不要用 Flexible,否则容易出现重定向循环。
DNS 生效需要时间。可以在本地检查:
dig app.example.com +short
返回 VPS IP 后,再继续配置 Caddy。
官方源安装方式更适合长期维护,因为后续可以用系统包管理器更新。
sudo apt update
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install -y caddy
安装后检查服务状态:
systemctl status caddy --no-pager
确认 Caddy 正在运行:
caddy version
如果你使用 UFW:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw status
如果云厂商有安全组,还要在云厂商控制台放行入站 TCP 80 和 443。很多“Caddy 证书申请失败”的根因不是 Caddyfile,而是安全组没放行 80 端口,Let's Encrypt 无法完成 HTTP-01 验证。
Caddy 的主配置文件通常是:
/etc/caddy/Caddyfile
假设你的应用运行在 127.0.0.1:3000,域名是 app.example.com,配置可以非常短:
app.example.com {
reverse_proxy 127.0.0.1:3000
}
编辑文件:
sudo nano /etc/caddy/Caddyfile
保存后先格式化:
sudo caddy fmt --overwrite /etc/caddy/Caddyfile
再检查配置:
sudo caddy validate --config /etc/caddy/Caddyfile
最后重载:
sudo systemctl reload caddy
访问 https://app.example.com。如果 DNS、端口和应用都正常,Caddy 会自动申请证书并转发请求。
很多新手会让应用直接监听 0.0.0.0:3000,然后再用 Caddy 反代。这通常不是最佳实践。
更推荐:
公网用户 -> Caddy :443 -> 127.0.0.1:3000
原因是:
- 外部用户只能访问 80/443,不能直接绕过 Caddy 访问应用端口。
- TLS、压缩、日志、安全 header 都集中在 Caddy 处理。
- 应用服务可以更简单,不需要自己管理 HTTPS。
- 多个应用之间的入口更清晰。
如果应用支持设置监听地址,优先设置成 127.0.0.1。例如 Node.js 应用可以只监听本机地址,Docker 应用可以只映射到 127.0.0.1:端口。
一台 VPS 可以跑多个服务。Caddyfile 可以这样写:
app.example.com {
reverse_proxy 127.0.0.1:3000
}
api.example.com {
reverse_proxy 127.0.0.1:4000
}
status.example.com {
reverse_proxy 127.0.0.1:3001
}
每个站点块独立管理一个域名。Caddy 会为每个域名自动处理 HTTPS。
如果多个域名指向同一个服务,也可以写在同一个站点块:
example.com, www.example.com {
reverse_proxy 127.0.0.1:3000
}
假设你的 Docker Compose 服务在容器内监听 3000,最简单的方式是把端口只暴露到本机:
services:
app:
image: your-app:latest
ports:
- "127.0.0.1:3000:3000"
restart: unless-stopped
然后 Caddy 反代本机端口:
app.example.com {
reverse_proxy 127.0.0.1:3000
}
这样比直接映射 3000:3000 更安全,因为外部网络无法直接访问 http://服务器IP:3000。
如果 Caddy 也运行在 Docker 里,可以把 Caddy 和应用放在同一个 Docker network,然后使用服务名反代。但对大多数单机 VPS 用户来说,系统安装 Caddy、Docker 应用绑定 localhost 更直观,也更容易排错。
多数情况下不需要。Caddy 的 reverse_proxy 会处理常见的 WebSocket 升级请求。
例如 Uptime Kuma、n8n、一些实时面板或 WebSocket API 通常可以直接:
ws.example.com {
reverse_proxy 127.0.0.1:8080
}
如果应用文档要求保留 Host header,Caddy 默认已经会传递合适的 Host 信息。只有遇到特殊上游时,才需要额外改 header。
Caddy 不只能反代,也可以直接托管静态站点。
假设静态文件在 /var/www/site:
example.com {
root * /var/www/site
file_server
}
创建目录并设置权限:
sudo mkdir -p /var/www/site
echo '<h1>Hello Caddy</h1>' | sudo tee /var/www/site/index.html
sudo chown -R caddy:caddy /var/www/site
如果你的网站既有静态文件又有后端 API,也可以组合:
example.com {
root * /var/www/site
file_server
handle_path /api/* {
reverse_proxy 127.0.0.1:4000
}
}
常见站点可以开启 gzip 和 zstd:
app.example.com {
encode zstd gzip
reverse_proxy 127.0.0.1:3000
}
这对文本资源、JSON API、HTML、CSS、JS 通常有帮助。图片和视频一般已经压缩,不会明显受益。
基础安全 header 可以这样加:
app.example.com {
encode zstd gzip
header {
X-Content-Type-Options nosniff
X-Frame-Options DENY
Referrer-Policy strict-origin-when-cross-origin
}
reverse_proxy 127.0.0.1:3000
}
注意:不要盲目复制非常严格的 Content-Security-Policy。CSP 写错后可能导致前端资源、第三方登录、统计脚本或图片加载失败。生产环境建议先从简单 header 开始,再逐步收紧 CSP。
Caddy 默认有系统日志,但你也可以为站点单独写访问日志:
app.example.com {
log {
output file /var/log/caddy/app.example.com.log
format json
}
reverse_proxy 127.0.0.1:3000
}
确保目录存在并属于 Caddy 用户:
sudo mkdir -p /var/log/caddy
sudo chown -R caddy:caddy /var/log/caddy
sudo systemctl reload caddy
查看日志:
sudo journalctl -u caddy -f
sudo tail -f /var/log/caddy/app.example.com.log
每次改 Caddyfile 后,建议固定按这个顺序:
sudo caddy fmt --overwrite /etc/caddy/Caddyfile
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
查看服务状态:
systemctl status caddy --no-pager
查看实时日志:
sudo journalctl -u caddy -f
重启 Caddy:
sudo systemctl restart caddy
通常优先使用 reload,因为它会在配置合法时平滑加载;只有服务状态异常时再用 restart。
按顺序检查:
dig app.example.com +short
sudo ss -lntp | grep -E ':80|:443'
sudo ufw status
systemctl status caddy --no-pager
重点确认:DNS 是否指向当前 VPS,Caddy 是否监听 80/443,防火墙和安全组是否放行。
查看 Caddy 日志:
sudo journalctl -u caddy --since "30 minutes ago"
常见原因:
- DNS 还没生效。
- 80 端口未开放。
- 域名解析到了旧服务器。
- Cloudflare Flexible SSL 导致重定向问题。
- 同一个域名短时间内频繁失败,触发 CA 限制。
修复后不要疯狂重启。先确认 DNS 和端口,再 reload。
502 通常说明 Caddy 能访问,但上游应用不通。
检查应用端口:
curl -I http://127.0.0.1:3000
sudo ss -lntp | grep 3000
如果本机 curl 都不通,问题在应用,不在 Caddy。检查应用进程、Docker 容器、环境变量和启动日志。
Docker 应用可检查:
docker ps
docker logs --tail=100 your-container-name
如果你用了 handle_path,它会剥离匹配前缀。某些应用需要保留路径前缀,这时应该改用 handle 或直接 reverse_proxy。
例如:
example.com {
handle /api/* {
reverse_proxy 127.0.0.1:4000
}
handle {
reverse_proxy 127.0.0.1:3000
}
}
路径问题最容易出现在“一个域名下挂多个应用”的场景。如果应用本身不支持 base path,优先改成不同子域名,而不是强行挂在子路径下。
下面是一个适合大多数单机 VPS 的模板:
{
email [email protected]
}
app.example.com {
encode zstd gzip
header {
X-Content-Type-Options nosniff
Referrer-Policy strict-origin-when-cross-origin
}
log {
output file /var/log/caddy/app.example.com.log
format json
}
reverse_proxy 127.0.0.1:3000
}
api.example.com {
encode zstd gzip
reverse_proxy 127.0.0.1:4000
}
status.example.com {
reverse_proxy 127.0.0.1:3001
}
全局 email 不是必须,但建议填写一个真实邮箱,方便 ACME 证书相关通知。
如果你已经熟悉 Nginx,并且有复杂的企业级配置、历史配置库、Lua/OpenResty 生态需求,继续用 Nginx 没问题。
如果你是下面这些情况,Caddy 往往更省心:
- 单台或少量 VPS。
- 主要需求是 HTTPS 反代和多域名。
- 自托管应用较多,但流量规模不极端。
- 希望减少 Certbot、renew hook、server block 模板维护。
- 团队成员不全是运维背景,希望配置可读性更好。
简单说:Nginx 更像一把功能极多的瑞士军刀,Caddy 更像为现代 HTTPS 站点优化过的自动化入口。个人 VPS 和中小型自托管场景,Caddy 的效率很高。
上线前建议做这些检查:
- 应用端口只绑定
127.0.0.1,不要直接暴露3000、8080等端口。 - 云安全组只开放必要端口,例如 22、80、443。
- SSH 使用密钥登录,关闭密码登录。
- Caddyfile 修改后先 validate 再 reload。
- 对后台类服务增加应用自身登录、强密码或单点登录。
- 重要数据服务不要因为有 Caddy 就直接暴露到公网。
- 定期更新系统和 Caddy。
Caddy 解决的是 Web 入口和 TLS,不等于替你完成应用层权限控制。后台系统、管理面板、数据库面板仍然要有自己的认证和访问控制。
在 VPS 上部署多个应用时,Caddy 可以把“域名、HTTPS、证书续期、反向代理、多站点入口”这几个高频问题合并成一个很短的 Caddyfile。
最小配置只需要:
app.example.com {
reverse_proxy 127.0.0.1:3000
}
但真正稳定上线,还要检查 DNS、80/443 端口、防火墙、安全组、应用监听地址、日志和重载流程。只要按本文的方式配置,你就可以用一台 VPS 同时托管多个 HTTPS 服务,并且不用再为证书续期写额外脚本。