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 基础镜像),体积小。额外安装了 gitcgitpython3groffcurldcron——这些是为了在容器内运行 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

顺序很重要:

  1. 先下载 GeoIP 数据库(如果不存在或 3 天过期)
  2. 启动 crond 后台进程(用于后续定时更新 GeoIP)
  3. 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_USERNAMEsecrets.DOCKERHUB_TOKEN 需要在 GitHub 仓库的 Settings → Secrets and variables → Actions 中配置。

总结

通过 xcaddy + 多阶段构建 + GitHub Actions,你只需要维护一个 Containerfile 和一个 Workflow 文件,就能得到定期更新的自定义 Caddy 镜像。这套流程:

  • 每次构建都基于最新的 Caddy 和插件版本
  • 运行镜像干净小巧,不包含构建工具链
  • 通过 crond 自动维护 GeoIP 数据库
  • 配合 Podman Quadlet 的 AutoUpdate,实现容器层面的全自动更新

后续文章会逐个深入介绍各个插件的实际配置和踩坑记录。

参考链接

添加评论