# Xcaddy 构建自定义 Caddy：插件选型到 CI/CD 自动发布


## 为什么需要自定义 Caddy

Caddy 官方提供的二进制和 Docker 镜像仅包含最基本的模块。一旦你需要以下功能：

- Cloudflare DNS 质询获取通配符证书
- 频率限制
- MaxMind GeoIP 地理定位
- WebDAV 文件管理
- Layer4 TCP/UDP 四层转发
- 响应内容替换
- Nginx 配置兼容
- CGI 支持

你就需要自行用 `xcaddy` 构建一个定制版本。

## xcaddy 是什么

[xcaddy](https://github.com/caddyserver/xcaddy) 是 Caddy 官方提供的构建工具。它是对 Go 构建过程的封装——Caddy 的模块体系基于 Go 的导入机制，`xcaddy` 负责下载指定版本的 Caddy 源码、注入自定义模块（`--with` 参数）、然后编译出完整的二进制。

## 插件选型

我的自定义 Containerfile：

```dockerfile
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` 控制：

```bash
#!/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 数据库自动更新

```bash
#!/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](https://github.com/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 配置如下

```yaml
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 中配置。

## 我的镜像

如果你懒得构建镜像，也可以直接用我的 `docker.io/nite07/caddy:latest`。

仓库链接： https://git.nite07.com/nite/custom-containerfile

## 参考链接

- [xcaddy GitHub](https://github.com/caddyserver/xcaddy)
- [Caddy 模块市场](https://caddyserver.com/download)
- [P3TERX/GeoLite.mmdb](https://github.com/P3TERX/GeoLite.mmdb)


---

> 作者: Nite  
> URL: https://www.nite07.com/zh-cn/posts/custom-caddy-build/  

