Xcaddy 构建自定义 Caddy:插件选型到 CI/CD 自动发布
为什么需要自定义 Caddy
Caddy 官方提供的二进制和 Docker 镜像仅包含最基本的模块。一旦你需要以下功能:
- Cloudflare DNS 质询获取通配符证书
- 频率限制
- MaxMind GeoIP 地理定位
- WebDAV 文件管理
- Layer4 TCP/UDP 四层转发
- 响应内容替换
- Nginx 配置兼容
- CGI 支持
你就需要自行用 xcaddy 构建一个定制版本。
xcaddy 是什么
xcaddy 是 Caddy 官方提供的构建工具。它本质上是对 Go 构建过程的封装——Caddy 的模块体系基于 Go 的导入机制,xcaddy 负责下载指定版本的 Caddy 源码、注入自定义模块(--with 参数)、然后编译出完整的二进制。
插件选型
我的自定义 Containerfile:
FROM docker.io/library/caddy:2-builder AS builder
RUN xcaddy build \
--with github.com/caddyserver/nginx-adapter \
--with github.com/caddy-dns/cloudflare \
--with github.com/caddyserver/replace-response \
--with github.com/mholt/caddy-webdav \
--with github.com/mholt/caddy-ratelimit \
--with github.com/WeidiDeng/caddy-cloudflare-ip \
--with github.com/porech/caddy-maxmind-geolocation \
--with github.com/mholt/caddy-l4 \
--with github.com/aksdb/caddy-cgi/v2
FROM docker.io/library/caddy:latest
RUN apk update && apk add --no-cache git git-daemon cgit python3 py3-pygments py3-markdown py3-docutils groff curl dcron
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
COPY update_geodb.sh /usr/local/bin/update_geodb.sh
RUN chmod +x /usr/local/bin/entrypoint.sh \
&& chmod +x /usr/local/bin/update_geodb.sh \
&& echo "0 0 */3 * * /usr/local/bin/update_geodb.sh > /proc/1/fd/1 2>/proc/1/fd/2" | crontab -
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]每个插件的作用:
| 插件 | 用途 |
|---|---|
caddy-dns/cloudflare |
通过 Cloudflare API 完成 DNS-01 质询,获取通配符 TLS 证书 |
caddy-ratelimit |
为站点添加请求频率限制,防止滥用 |
caddy-maxmind-geolocation |
基于 MaxMind GeoIP 数据库的地理位置访问控制 |
caddy-l4 |
四层 TCP/UDP 代理,转发非 HTTP 协议的流量 |
caddy-webdav |
在站点子路径下提供 WebDAV 文件管理 |
caddy-cloudflare-ip |
获取 Cloudflare 的真实 IP 范围,用于 trusted_proxies |
caddy-replace-response |
修改响应内容(替换字符串) |
nginx-adapter |
兼容 Nginx 配置迁移场景 |
caddy-cgi |
在 Caddy 中运行 CGI 脚本 |
为什么用多阶段构建
两个阶段各有分工:
- builder 阶段:基于
caddy:2-builder镜像,这个镜像包含了完整的 Go 工具链和 Caddy 源码依赖。xcaddy build在这里执行,产物是带所有插件的caddy二进制。 - runtime 阶段:基于官方的
caddy:latest(Alpine 基础镜像),体积小。额外安装了git、cgit、python3、groff、curl、dcron——这些是为了在容器内运行 cgit(Git Web 界面)和定时更新 GeoIP 数据库所需。
这种模式的好处是:builder 镜像动辄 1GB+,但最终运行镜像只有 ~80MB,且不包含任何构建时的临时文件和源码。
entrypoint 做了什么
启动流程由 entrypoint.sh 控制:
#!/bin/sh
# 首次执行 GeoLite2 数据库更新脚本,确保 Caddy 启动前文件可用
/usr/local/bin/update_geodb.sh
# 启动 cron 服务
crond -b -L /var/log/cron.log
# 执行原始的 Caddy ENTRYPOINT
exec caddy run --config /etc/caddy/Caddyfile --adapter caddyfile顺序很重要:
- 先下载 GeoIP 数据库(如果不存在或 3 天过期)
- 启动 crond 后台进程(用于后续定时更新 GeoIP)
exec替换 shell 进程为 Caddy 主进程,确保信号正确传递
exec 是关键——如果没有它,Caddy 会作为子进程运行,当容器收到 SIGTERM 时信号可能无法正确传递到 Caddy 进程。
GeoIP 数据库自动更新
#!/bin/sh
GEODB_DIR="/config/geodb"
GEODB_FILE="$GEODB_DIR/GeoLite2-Country.mmdb"
GEODB_URL="https://github.com/P3TERX/GeoLite.mmdb/releases/latest/download/GeoLite2-Country.mmdb"
INTERVAL_SECONDS=259200 # 3 * 24 * 60 * 60
mkdir -p "$GEODB_DIR"
if [ ! -f "$GEODB_FILE" ]; then
echo "GeoLite2-Country.mmdb 不存在,正在下载..."
curl -sSL "$GEODB_URL" -o "$GEODB_FILE"
else
FILE_MOD_TIME=$(stat -c %Y "$GEODB_FILE")
CURRENT_TIME=$(date +%s)
AGE=$((CURRENT_TIME - FILE_MOD_TIME))
if [ "$AGE" -ge "$INTERVAL_SECONDS" ]; then
echo "GeoLite2-Country.mmdb 正在下载新版本..."
curl -sSL "$GEODB_URL" -o "$GEODB_FILE"
else
echo "GeoLite2-Country.mmdb 跳过下载."
fi
fi数据库来源我用的是 P3TERX/GeoLite.mmdb 镜像。MaxMind 官方虽然提供免费版,但需要注册账号获取 License Key,P3TERX 的镜像定期同步并自动发布 Release,省去了这个步骤。
定时更新通过容器内的 crond 实现(在 Dockerfile 中已配置 crontab):
0 0 */3 * * /usr/local/bin/update_geodb.sh > /proc/1/fd/1 2>/proc/1/fd/2每 3 天凌晨 0 点检查一次,文件超过 3 天未更新就重新下载。
GitHub Actions 自动构建
我使用的 workflow 配置如下
name: docker-caddy
on:
schedule:
- cron: "0 0 * * 1"
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v7
with:
push: true
tags: your-username/caddy:latest
platforms: linux/amd64
context: ./caddy
file: ./caddy/Containerfile触发方式:
- 定时触发:每周一零点自动构建一次。Caddy 本身和各个插件都在持续更新,每周构建可以保证镜像中包含最新的安全修复。
- 手动触发:
workflow_dispatch允许在 GitHub Actions 页面点击按钮手动触发,适合需要立即发布某个插件新版本的场景。
secrets.DOCKERHUB_USERNAME 和 secrets.DOCKERHUB_TOKEN 需要在 GitHub 仓库的 Settings → Secrets and variables → Actions 中配置。
总结
通过 xcaddy + 多阶段构建 + GitHub Actions,你只需要维护一个 Containerfile 和一个 Workflow 文件,就能得到定期更新的自定义 Caddy 镜像。这套流程:
- 每次构建都基于最新的 Caddy 和插件版本
- 运行镜像干净小巧,不包含构建工具链
- 通过 crond 自动维护 GeoIP 数据库
- 配合 Podman Quadlet 的 AutoUpdate,实现容器层面的全自动更新
后续文章会逐个深入介绍各个插件的实际配置和踩坑记录。