项目实战 # 网络通信已发布

Caddy :轻量级 Web 服务器与反向代理实战

在部署个人博客、家庭服务器和各种自托管服务时,我们经常会遇到一个问题:不同的服务运行在不同的端口上,如何通过统一的域名访问它们?例如,博客运行在 `3000` 端口,NAS 管理界面运行在 `5000` 端口,另一个服务运行在 `8080` 端口。如果直接对外暴露这些端口,不仅访问地址不够美观,也会增加安全管理和证书维护的复杂度...

2025.10.2014 分钟阅读

在进行部署时,我以前一直使用的是 Nginx, 在使用过程中我发现 Nginx 配置较为繁琐重复,https 还要单独续签,有没有一种更简单轻便使用的 Web 服务器, 于是我发现了 Caddy。

部署个人博客、家庭服务器和各种自托管服务时,我们经常会遇到一个问题:不同的服务运行在不同的端口上,如何通过统一的域名访问它们?例如,博客运行在 3000 端口,NAS 管理界面运行在 5000 端口,另一个服务运行在 8080 端口。如果直接对外暴露这些端口,不仅访问地址不够美观,也会增加安全管理和证书维护的复杂度。

解决这类问题的常见方式是部署一个反向代理服务器。Nginx、Apache 和 Caddy 都可以完成这项工作,而 Caddy 的一个突出特点是:默认集成 HTTPS 自动化管理,配置简单,适合个人项目、家庭服务器和中小型 Web 服务。

本文从 Caddy 的基本原理开始,介绍它的核心功能、配置语法、反向代理、自动 HTTPS、Docker 部署,以及在家庭服务器和内网穿透场景中的应用。

一、Caddy 是什么

Caddy 是一个使用 Go 语言编写的 Web 服务器,支持 HTTP、HTTPS、反向代理、静态文件服务等功能。它的一个重要设计目标是让安全的 Web 服务更容易部署,尤其是通过自动 HTTPS 降低证书申请、续期和配置的维护成本。

传统部署方式通常需要自行准备 TLS 证书,配置证书路径、私钥、监听端口和续期脚本。Caddy 则可以在满足域名解析、网络连通性和证书颁发机构验证条件时,自动申请并续期证书。

例如,只需要一段简单的配置:

example.com {
    reverse_proxy localhost:3000
}

Caddy 就可以将发往 example.com 的请求转发到本机的 3000 端口,并自动处理 HTTPS 相关配置。

这里有几个关键点:

  • example.com 是客户端访问的域名。
  • reverse_proxy 表示将请求转发给后端服务。
  • localhost:3000 是后端应用的地址。
  • Caddy 会根据配置自动处理 HTTPS,但前提是域名和网络环境满足证书签发要求。

如果使用 Node.js、NestJS、Nuxt 或其他 Web 框架开发应用,通常不需要让应用自己承担公网 TLS 终止工作。可以由 Caddy 统一处理 HTTPS,再通过内部网络访问应用。

二、正向代理与反向代理的区别

理解 Caddy,首先要弄清楚代理服务器的两种常见工作方式。

正向代理主要代表客户端访问外部资源。客户端知道自己在使用代理,代理通常帮助客户端转发访问请求。

反向代理则位于服务器一侧,客户端访问的是代理服务器,代理再将请求转发给后端应用。客户端不需要知道真实后端的地址和端口。

例如,有一个运行在 3000 端口的 Nuxt 博客:

浏览器
   │
   │ https://blog.example.com
   ▼
Caddy :443
   │
   │ HTTP
   ▼
Nuxt 应用 :3000

浏览器只需要访问 https://blog.example.com,Caddy 就会将请求转发给 Nuxt。

这种架构的优势是,多个服务可以共用公网的 80 和 443 端口,通过域名区分不同应用:

blog.example.com      → Nuxt 博客 :3000
api.example.com       → NestJS API :4000
nas.example.com       → NAS 管理服务 :5000
monitor.example.com   → 监控服务 :8080

不需要为每个服务单独开放一个公网端口,也不必让每个后端应用分别管理公网证书。

三、安装与基础配置

Caddy 支持 Linux、macOS、Windows 和容器化部署。实际生产环境建议优先使用操作系统提供的官方安装方式或官方 Docker 镜像,并根据部署方式管理配置文件和数据目录。

在 Linux 环境中,安装完成后通常可以通过以下命令检查版本:

caddy version

检查配置文件是否存在语法或配置问题:

caddy validate --config /etc/caddy/Caddyfile

格式化 Caddyfile:

caddy fmt --overwrite /etc/caddy/Caddyfile

如果使用 systemd 管理 Caddy,常见操作如下:

sudo systemctl status caddy
sudo systemctl reload caddy
sudo systemctl restart caddy
sudo journalctl -u caddy -f

其中,reload 通常适合在修改配置后平滑加载新配置;restart 则会重新启动服务。实际使用时,应根据安装方式和服务管理方式选择对应命令。

Caddy 最常见的配置格式是 Caddyfile。它采用相对简洁的语法,不需要编写大量嵌套的 JSON 配置,就可以完成常用的 Web 服务部署。

四、Caddyfile 核心语法

1. 反向代理

假设本机运行着一个 NestJS 服务,监听 4000 端口:

api.example.com {
    reverse_proxy localhost:4000
}

当用户访问 https://api.example.com/users 时,Caddy 会将请求转发给后端服务。

通常情况下,请求路径会保留。也就是说,后端收到的仍然是 /users,而不是被自动替换成其他路径。

如果后端服务运行在另一台服务器上,也可以指定内网地址:

api.example.com {
    reverse_proxy 192.168.2.100:4000
}

这种方式适用于将 Caddy 与应用服务分开部署的场景。

2. 多域名代理

Caddy 可以在一个配置文件中配置多个站点:

blog.example.com {
    reverse_proxy localhost:3000
}

api.example.com {
    reverse_proxy localhost:4000
}

monitor.example.com {
    reverse_proxy localhost:9090
}

Caddy 会根据请求的主机名选择对应的站点配置。这使得一台服务器可以承载多个 Web 服务。

如果后端运行在 Docker 容器中,需要注意 localhost 的含义:它指的是 Caddy 自己所在的网络命名空间,而不是其他容器。跨容器访问时,通常应使用同一 Docker 网络中的服务名。

3. 静态文件服务

除了反向代理,Caddy 也可以直接提供静态文件。

例如,将 /srv/www 目录作为网站根目录:

static.example.com {
    root * /srv/www
    file_server
}

如果目录中存在 index.html,访问网站时就可以直接返回静态页面。

对于纯静态博客、文档站点、图片资源和下载文件服务,这种方式非常轻量,不需要额外运行 Node.js 应用。

4. 路径匹配与重写

有时候,我们希望把一个域名下的不同路径转发给不同服务:

example.com {
    handle /api/* {
        reverse_proxy localhost:4000
    }

    handle {
        reverse_proxy localhost:3000
    }
}

上面的配置将 /api/* 请求转发给后端 API,其余请求转发给前端应用。

需要注意,handle 主要负责匹配并选择处理逻辑,不会默认删除匹配到的路径前缀。如果后端只接受 /users 而不接受 /api/users,就需要根据实际需求使用 handle_path 或 uri strip_prefix 等方式调整路径。

五、自动 HTTPS 的工作原理

自动 HTTPS 是 Caddy 最有代表性的功能之一。

在传统 Nginx 部署中,通常需要先通过证书颁发机构获得证书,再在配置中指定证书和私钥路径,之后还要确保续期任务正常执行。

Caddy 将这套流程整合到了服务器中。在适用的配置下,它可以自动申请证书、配置 HTTPS,并在证书到期前自动续期。

其工作流程大致如下:

配置域名
   │
   ▼
Caddy 检查 HTTPS 配置
   │
   ▼
验证域名控制权
   │
   ▼
申请 TLS 证书
   │
   ▼
监听 HTTPS 请求
   │
   ▼
自动续期证书

但是,自动 HTTPS 并不意味着任何网络环境都能直接申请证书。实际部署时需要满足证书颁发机构要求的验证条件。

常见的验证方式包括:

  • HTTP-01:通常要求域名的公网 80 端口能够访问验证服务。
  • TLS-ALPN-01:通常要求公网 443 端口能够完成相应的 TLS 验证。
  • DNS-01:通过 DNS TXT 记录验证域名控制权,适用于通配符证书以及无法直接从公网访问验证端口的部分环境。

如果使用 DNS-01,还需要配置对应的 DNS Provider 模块及 API 凭据。并不是所有 Caddy 安装包都默认包含所有 DNS 插件。

使用通配符证书

如果希望多个子域名共用一张通配符证书,例如:

*.example.com

通常需要通过 DNS-01 验证完成证书申请。

Caddyfile 可以采用类似下面的结构:

*.example.com {
    @blog host blog.example.com
    handle @blog {
        reverse_proxy localhost:3000
    }

    @api host api.example.com
    handle @api {
        reverse_proxy localhost:4000
    }
}

这段配置演示的是通配符站点的请求分流思路。要让通配符证书正常签发和使用,还需要配置 DNS Provider 插件、凭据和对应的 DNS 验证流程。

另外,通配符证书通常只覆盖一层子域名。例如,blog.example.com 匹配 *.example.com,但 a.blog.example.com 不匹配。

六、Docker 部署 Caddy

在家庭服务器和自托管服务中,使用 Docker 部署 Caddy非常方便。配置文件可以通过挂载的方式管理,证书与运行状态则保存在持久化目录中。

一个常见的目录结构如下:

caddy/
├── compose.yaml
├── Caddyfile
└── data/

其中,data 目录用于保存 Caddy 的持久化数据。生产环境还应根据需要为配置目录和状态目录设置适当的权限。

compose.yaml 示例:

services:
  caddy:
    image: caddy:2
    container_name: caddy
    restart: unless-stopped

    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"

    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - ./data:/data
      - ./config:/config

    networks:
      - web

networks:
  web:
    external: true

这里假设已经创建了一个名为 web 的 Docker 网络:

docker network create web

如果后端应用也接入该网络,就可以使用容器服务名进行代理。例如,后端容器名或 Compose 服务名为 blog,监听 3000 端口,那么 Caddyfile 可以写成:

blog.example.com {
    reverse_proxy blog:3000
}

Caddy 与后端应用需要处于能够互相通信的网络中。仅仅在 Caddy 的配置里写入容器名称,并不能自动让不同 Docker 网络中的容器互通。

启动服务:

docker compose up -d

查看日志:

docker compose logs -f caddy

修改配置后,可以尝试平滑加载:

docker compose exec caddy \
  caddy reload --config /etc/caddy/Caddyfile

为什么必须持久化 /data

Caddy 会在数据目录中保存证书、私钥及其他运行数据。如果容器被删除或重建,而这些数据没有持久化,Caddy 可能需要重新申请证书或重新建立相关状态。

因此,Docker 部署时至少应该确保 /data 目录被正确持久化,并对其中的敏感内容做好权限控制和备份。

上面的示例使用官方镜像标签 caddy:2,适合演示基本部署。实际生产环境建议根据升级策略固定到经过测试的具体版本,并定期更新以获得安全修复。

七、Caddy 与 frp:家庭服务器的外网访问方案

对于没有公网 IPv4 地址的家庭网络,单独部署 Caddy 并不能直接解决外网访问问题。

例如,家庭网络中的服务运行在:

192.168.2.100:3000

如果路由器没有公网 IPv4,外部客户端通常无法直接访问这个地址。此时可以使用 frp、VPN 或具有公网入口的云服务器等方式建立访问通道。

一种常见架构是:

公网客户端
    │
    ▼
域名 DNS 解析
    │
    ▼
阿里云 ECS 公网 IP
    │
    ▼
Caddy :80 / :443
    │
    ▼
frps / frpc 建立的转发通道
    │
    ▼
家庭服务器内网应用

在这种架构中,各个组件承担不同职责:

  • DNS:将域名解析到云服务器的公网地址。
  • Caddy:处理 HTTP/HTTPS 请求、证书和反向代理。
  • frp:建立云服务器与家庭网络之间的转发通道。
  • 家庭服务器:实际运行博客、NAS 周边应用和其他自托管服务。

这里有两种常见的部署方式。

方式一:Caddy 部署在云服务器上。

云服务器上的 Caddy 负责 HTTPS,随后将请求转发到 frp 暴露的本地转发端口。这种方式适合将公网入口集中管理在云服务器上。

方式二:Caddy 部署在家庭服务器上。

云服务器通过 frp 转发公网请求到家庭网络,家庭服务器上的 Caddy 再将请求分发给不同应用。这种方式便于在家庭网络内部管理多个服务,但需要仔细设计公网端口转发和 TLS 终止位置。

两种方式都可以使用,但不要在没有必要的情况下重复设置多个反向代理层。每增加一层,就需要额外考虑请求头、超时、日志、TLS 和故障排查。

使用阿里云 DNS

如果域名托管在阿里云 DNS,通常可以添加 A 记录,将域名指向云服务器公网 IPv4:

blog.example.com    A    云服务器公网 IPv4
api.example.com     A    云服务器公网 IPv4

当多个域名指向同一个公网 IP 时,Caddy 可以根据域名将请求分发给不同服务。

如果使用 DNS-01 申请证书,则还需要通过 DNS API 创建 TXT 验证记录。此时需要安装与所用 DNS Provider 对应的 Caddy 插件,并使用权限尽可能小的 API 凭据。

不要把 DNS API 密钥直接写入公开仓库,也不要为了申请证书而授予不必要的账号权限。

八、生产环境中的安全与运维注意事项

Caddy 配置简单,但并不意味着部署后就可以忽略安全问题。

首先,不要将所有内部管理服务直接暴露到公网。NAS 管理界面、数据库、监控面板和管理后台应根据使用场景设置访问控制,优先考虑 VPN、IP 限制、身份认证或专门的访问网关。

其次,要理解反向代理后的客户端 IP。后端应用可能需要从 X-Forwarded-For、X-Forwarded-Proto 等请求头中获取客户端信息,但这些信息必须结合可信代理配置进行判断。不要直接信任任意客户端传入的转发头,否则可能导致访问日志、IP 限制或安全审计出现错误。

再次,Caddy 的管理 API 不应该随意暴露到公网。默认管理接口通常在本地监听,修改监听地址时应明确访问边界,避免未授权用户更改代理配置。

最后,应持续监控 Caddy 的访问日志和运行日志。证书申请失败、DNS 解析异常、后端连接拒绝、超时和权限问题,都可能导致服务无法正常访问。对于重要服务,建议配合健康检查、告警和配置备份。

九、Caddy 与 Nginx 如何选择

Caddy 和 Nginx 都是成熟的 Web 服务器与反向代理方案,适用场景有所不同。

对比项CaddyNginx
默认 HTTPS 自动化内置自动 HTTPS通常需要额外配置证书与续期
配置风格简洁的 Caddyfile灵活的指令与配置块
反向代理原生支持原生支持
静态文件服务支持支持
复杂代理规则可以实现生态成熟、使用广泛
DNS 插件扩展需要对应模块通常通过外部工具或集成方案
常见使用场景个人站点、家庭服务器、快速部署 HTTPS传统 Web 基础设施、复杂代理和已有运维体系

如果目标是快速部署几个个人站点,且希望减少证书维护工作,Caddy 往往更省心。如果现有基础设施已经围绕 Nginx 建立了成熟的配置管理、监控和运维流程,也没有必要仅仅为了自动 HTTPS 而迁移。

十、总结

Caddy 的核心价值在于将 Web 服务、反向代理和 HTTPS 自动化整合到一个相对简单的配置体系中。

对于个人开发者来说,可以用它为 Nuxt 博客配置 HTTPS,为 NestJS API 统一入口,也可以在 Docker 环境中通过服务名代理不同容器。对于家庭服务器用户,则可以进一步结合云服务器、DNS 和 frp,让没有公网 IPv4 的内网服务通过域名访问。

实际部署时,可以从最简单的反向代理开始,再逐步增加多域名路由、静态文件服务、DNS-01 证书、访问控制和监控能力。

需要记住的是:Caddy 解决的是 Web 入口、代理和 HTTPS 等问题;DNS 负责域名解析,frp 负责内外网之间的转发通道,后端应用负责业务逻辑。明确各组件的职责,才能构建一套容易维护、可靠且安全的服务架构。

参考资料:

#网络通信
ZZC

ZZC

写代码,也写一些生活。