Caddy:自动 HTTPS 的 Go Web 服务器与反向代理

它是什么

Caddy 是一个开源、以 Go 编写的 HTTP Web 服务器。它以简洁的 Caddyfile 为常用配置入口,并以自动申请和续期 TLS 证书、HTTP/2、静态文件服务、反向代理及模块扩展为主要特点。它既可作为静态站点入口,也可为后端服务提供反向代理、负载均衡、压缩和基础认证等能力。其 Go 运行时基础与 Go GMP 调度模型有关,但本文不展开 Caddy 的运行时性能分析。

在本知识库中,Caddy 最直接的实践关联是 FRP:可由 Caddy 在 HTTPS/WSS 入口终止 TLS 并转发到 frps,但这并不会把 UDP 业务流量转换为 HTTPS。

版本边界:原始笔记使用 caddy:2.5.2caddy:2.6.2。以下配置用于保留其思路和字段;Caddy 指令、模块和镜像标签会随版本演进,投入生产前必须对目标版本执行 caddy fmtcaddy validate

Docker Compose 快速开始

下面的 Compose 以 Caddyfile 启动 Caddy,并持久化 /data。端口 80/udp443/udp 用于支持 QUIC/HTTP/3;2019 为 Admin API,限定在宿主机 loopback,不能直接暴露到公网。

version: "3.5"
services:
  caddy-server:
    image: caddy:2.5.2
    restart: always
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - ./data:/data
    ports:
      - "80:80"
      - "80:80/udp"
      - "443:443"
      - "443:443/udp"
      - "127.0.0.1:2019:2019"
    command: ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]

为避免配置文件和运行时状态因容器重建丢失,实际部署通常还应按镜像文档挂载 /config;证书申请也要求域名解析、入站端口和外部网络条件正确。

Caddyfile:常见路由模式

下列片段保留原始笔记的主要用法:动态响应、静态文件、JSON、反向代理、IP 白名单、按路径分流、WebSocket、自有证书与基础认证。

# 返回访问者 IP
ip.test.work {
    templates
    header Content-Type text/plain
    respond "{{.RemoteIP}}"
    encode zstd gzip
}
 
# 静态文件
file.test.work {
    file_server {
        root /www/html
    }
}
 
# 返回 JSON 文本
test.test.com {
    templates
    header Content-Type application/json
    respond {"name":"bryan","male":"yes","age":45}
}
 
# 反向代理
vault.roky.work {
    reverse_proxy vault:80
}
 
# HTTPS 上游与 IP 白名单
test.domain.com {
    @ip_whitelist {
        remote_ip 6.6.6.6
    }
 
    route @ip_whitelist {
        reverse_proxy 1.2.3.4:6666 {
            transport http {
                tls
                tls_insecure_skip_verify
            }
        }
    }
}
 
# 使用 handle,实现类似 Nginx location 的路径分流
http://127.0.0.1:7892 {
    @api {
        path /api /ping
        method GET
    }
 
    handle @api {
        route {
            reverse_proxy host.docker.internal:1234
        }
    }
 
    handle {
        respond "not found" 404
    }
}
 
# WebSocket 代理
ws.roky.work {
    @websockets {
        header Connection Upgrade
        header Upgrade websocket
    }
    reverse_proxy @websockets 127.0.0.1:7000
}
 
# 使用自有证书
test.roky.work {
    tls test.roky.work.crt test.roky.work.key
    reverse_proxy vault:80
}
 
# 基础认证(指令名和语法应以运行版本为准)
basicauth /secret/* {
    Bob $2a$14$UPT8R.QFnkMA6fRYetI.LeqMu.SyKpEcItP8pJdeM7rLQniefCDLG
}

这里有三项不能忽略的边界:

  • tls_insecure_skip_verify 会跳过对 HTTPS 上游证书的校验,只应作为临时排障手段;生产环境应改用受信任证书或配置正确的信任链。
  • remote_ip 是入口侧的网络限制,不替代应用认证、最小权限和审计;代理后若经过 CDN、负载均衡器或其他反代,还需按部署链路处理真实客户端 IP。
  • WebSocket 在 Caddy 中通常可由 reverse_proxy 自动处理 Upgrade;显式 matcher 可保留为表达意图的写法,但要以实际版本验证。

配置生命周期:格式化、校验与热加载

每次提交 Caddyfile 前先格式化和验证,再执行 reload。下面命令假定容器名为 caddy-server

# 格式化配置文件
docker exec -ti caddy-server caddy fmt --overwrite /etc/caddy/Caddyfile
 
# 验证配置文件有效性
docker exec -ti caddy-server caddy validate --config /etc/caddy/Caddyfile
 
# reload 配置
docker exec -ti caddy-server caddy reload --adapter caddyfile --config /etc/caddy/Caddyfile
 
# 创建基础认证密码哈希
docker exec -ti caddy-server caddy hash-password

validate 成功只说明配置可被当前二进制解析,不等价于域名、证书、上游连通性、访问控制或实际请求路径均已正确;热加载后应使用预期域名、状态码和日志做回读验证。

Metrics 与 Prometheus

原始笔记通过全局选项启用 metrics:

{
    servers {
        metrics
    }
}

并给出一个用 EndpointSliceServiceServiceMonitor 让 Kubernetes Prometheus 抓取 2019/TCP 的示例:

apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: caddy-service-1
  namespace: kubesphere-monitoring-system
  labels:
    kubernetes.io/service-name: caddy-service
addressType: IPv4
ports:
  - name: ''
    appProtocol: http
    protocol: TCP
    port: 2019
endpoints:
  - addresses:
      - "10.1.2.3"
---
apiVersion: v1
kind: Service
metadata:
  name: caddy-service
  namespace: kubesphere-monitoring-system
  labels:
    app: caddy-service
spec:
  ports:
    - protocol: TCP
      port: 2019
      targetPort: 2019
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: caddy-servicemonitor
  namespace: kubesphere-monitoring-system
spec:
  selector:
    matchLabels:
      app: caddy-service
  jobLabel: app
  endpoints:
    - targetPort: 2019
      interval: 10s

由于 2019 同时承载 Admin API,不能把上述 Service 当作无认证的公网 metrics 服务。只有在 Admin 监听地址、NetworkPolicy、防火墙与 Prometheus 到目标的网络路径都被显式限制时,才可让监控网络访问它;否则应保持 loopback 绑定。该示例中的 EndpointSlice IP、命名空间和端口须替换为实际集群值。

Admin API 与 JSON 配置

Admin API 可加载、停止或读取活动配置,并按路径增删改配置对象。常用接口包括:

接口用途
POST /load替换活动配置。
POST /stop停止活动配置并退出进程。
GET /config/[path]导出指定路径的配置。
POST / PUT / PATCH / DELETE /config/[path]追加、创建、替换或删除配置对象或数组元素。
POST /adapt将配置适配为 JSON,但不运行。
GET /pki/ca/GET /pki/ca//certificates查询 CA 与证书链信息。
GET /reverse_proxy/upstreams查询上游当前状态。

可通过 @id 给 JSON 路由或 handler 标记稳定标识,以便对指定配置节点操作。下面 JSON 会为 localhost 定义一个返回 Hello, world! 的静态响应:

{
  "apps": {
    "http": {
      "servers": {
        "srv0": {
          "routes": [{
            "@id": "route0",
            "match": [{"host": ["localhost"]}],
            "handle": [{
              "handler": "subroute",
              "routes": [{"handle": [{
                "@id": "handler0",
                "handler": "static_response",
                "body": "Hello, world!"
              }]}]
            }]
          }]
        }
      }
    }
  }
}

完整 JSON 结构应以 Caddy JSON 文档 为准。并发自动化修改时应遵循 API 对并发配置变更的约束:先读取当前配置、指定明确路径或 @id,并在变更后回读验证,避免多个控制器互相覆盖。

自定义构建与插件

当官方镜像未包含需要的模块时,可用 xcaddy 在 builder 阶段构建二进制,再复制到运行镜像。原始笔记的示例如下:

FROM caddy:2.6.2-builder AS builder
 
RUN xcaddy build \
    --with github.com/caddyserver/nginx-adapter \
    --with github.com/hairyhenderson/caddy-teapot-module@v0.0.3-0
 
FROM caddy:2.6.2
 
COPY --from=builder /usr/bin/caddy /usr/bin/caddy

自定义构建应同时固定 Caddy 版本、每个模块版本及镜像摘要,并在 CI 中运行配置校验和镜像安全扫描。插件引入会扩展可执行代码与配置面,不能只因 Caddyfile 能通过解析就视作安全可用。

实践清单

  1. 把 Caddyfile、证书/数据目录和镜像版本纳入版本与备份策略;不要将私钥或密码哈希以可读形式提交到仓库。
  2. 使用 fmt → validate → reload → 请求与日志回读 的闭环发布配置。
  3. 不对公网开放 Admin API;监控抓取应采用最小网络暴露面。
  4. 移除 tls_insecure_skip_verify,并为反向代理上游配置可验证的证书与名称。
  5. FRP 等组合部署中,区分 Caddy 的 HTTPS/WSS 入口与业务协议实际监听的端口。

来源与范围

本文以用户提供的单份配置笔记为基础,保留其主要配置、命令、监控清单、Admin API 和自定义构建示例。文中没有对这些旧镜像版本进行实时兼容性验证,因此涉及指令语法、模块或 API 行为时应以目标版本的官方文档和本地 caddy validate 结果为准。