Shared build and release automation for the CaddyBuilds Docker images: caddy-cloudflare, caddy-bunny, caddy-namecheap.
Each image repo keeps what makes it different: its Dockerfile (the module list), a small
caddy-image.json, its README and example Caddyfiles. Everything about how an image is
checked, built, tagged, verified and released lives here and is pulled in as reusable
workflows, so a fix lands in every repo on its next run.
| Path | Purpose |
|---|---|
.github/workflows/build-image.yml |
Reusable: multi-platform build, push to GHCR and Docker Hub, verify, GitHub Release, failure issue |
.github/workflows/check-release.yml |
Reusable: daily check for new Caddy releases, module commits and republished official images; dispatches a build |
scripts/check_caddy_status/ |
The checker. Reads the image repo's Dockerfile and caddy-image.json, tracks module commits in module-versions.json |
Dockerfile |
The caddy-base image: Alpine with everything the official caddy image has except the Caddy binary |
.github/workflows/build-base-image.yml |
Builds and publishes ghcr.io/caddybuilds/caddy-base; daily rebuild when the pinned Alpine tag's digest changes |
examples/ |
A complete image repo skeleton: Dockerfile, caddy-image.json, the two caller workflows |
An image repo needs four files besides its README.
Dockerfile, complete and buildable on its own. The checker reads the modules from the
xcaddy build line, so this is the only place they are listed. Name the main stage
default and keep it last, so a plain docker build . produces it. A slim stage is
optional: FROM ghcr.io/caddybuilds/caddy-base:latest and copy the binary from the builder
(see examples/Dockerfile). The checker reads the slim base from that
FROM line. The build workflow builds each stage by name, and slim only when the config
enables it.
ARG CADDY_VERSION=2
FROM --platform=$BUILDPLATFORM caddy:${CADDY_VERSION}-builder AS builder
ARG TARGETOS TARGETARCH TARGETVARIANT
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH GOARM=${TARGETVARIANT#v} CGO_ENABLED=0 XCADDY_PRINT_VERSION=0 \
xcaddy build \
--with github.com/caddy-dns/cloudflare
FROM caddy:${CADDY_VERSION} AS default
COPY --from=builder /usr/bin/caddy /usr/bin/caddyThe builder stage runs on the build host and cross-compiles, so the Go build is never
emulated; only the final COPY stage runs per platform. See examples/Dockerfile.
caddy-image.json, the values the workflows cannot derive:
{
"title": "Caddy Cloudflare",
"description": "Caddy with Cloudflare DNS module",
"verify_module": "dns.providers.cloudflare",
"alpine_alias": true,
"slim": false,
"create_release": true,
"platforms": ["linux/amd64", "linux/arm64", "linux/arm/v7", "linux/ppc64le", "linux/s390x"]
}| Key | Required | Default | Meaning |
|---|---|---|---|
title |
yes | org.opencontainers.image.title |
|
description |
yes | org.opencontainers.image.description, also used in release notes |
|
verify_module |
yes | Module ID that must appear in caddy list-modules after the build |
|
alpine_alias |
no | true |
Also push X.Y.Z-alpine, X.Y-alpine, X-alpine and alpine as aliases of the default image |
slim |
no | false |
Also build the Dockerfile's slim stage and push X.Y.Z-slim, X.Y-slim, X-slim, slim |
create_release |
no | true |
Create a GitHub Release per Caddy version and append rebuild notes to it |
platforms |
no | the five above | Platforms to build, verify and require upstream. Any non-empty subset of what the official image publishes: linux/amd64, linux/arm64, linux/arm/v6, linux/arm/v7, linux/ppc64le, linux/riscv64, linux/s390x |
The schema lives in scripts/check_caddy_status/image_config.py; the build workflow runs
that module to read the file, so the checker and the build always agree on it.
Two caller workflows, copied from examples/.github/workflows/.
A manual build takes an optional note, recorded in the release notes and the
caddy-build.details label so a rebuild can be traced to its cause:
jobs:
build:
uses: CaddyBuilds/caddy-base/.github/workflows/build-image.yml@v1
with:
caddy_version: ${{ github.event.inputs.build_version || github.event.client_payload.caddy_version }}
build_reason: ${{ github.event.client_payload.build_reason || 'manual' }}
build_details: ${{ github.event.inputs.note || github.event.client_payload.build_details || '' }}
secrets: inheritjobs:
check:
uses: CaddyBuilds/caddy-base/.github/workflows/check-release.yml@v1
secrets: inheritImage names come from the repository name (ghcr.io/<owner>/<repo>), so a fork of any
image repo builds and publishes its own image without changing anything here.
Docker Hub is used when DOCKERHUB_USERNAME and DOCKERHUB_TOKEN secrets exist;
DOCKERHUB_REPOSITORY_NAME (secret or variable) overrides the Docker Hub repository.
ghcr.io/caddybuilds/caddy-base is the official caddy image's Alpine layer without the
Caddy binary: the same apk packages (ca-certificates, curl, libcap, mailcap),
directories, default Caddyfile, welcome page, environment, ports, workdir and CMD, built
from the Dockerfile at the repo root for every platform the official image
publishes. An image repo's slim stage copies its custom-built binary onto it, so the slim
image carries one Caddy binary instead of the stock one plus ours.
| Tag | Meaning |
|---|---|
latest |
Floating; what slim stages reference |
alpine3.23 |
The Alpine release pinned in the Dockerfile's FROM line |
The build runs when the Dockerfile changes and daily at 21:00 UTC, three hours before the
image repos' checks. Each image records the alpine digest it was built from
(org.opencontainers.image.base.digest). The daily run rebuilds only when
library/alpine:<pin> now points at a different digest, so Alpine fixes reach the slim
images on the next day's check without pinning package versions. The pin is bumped by
editing the FROM line.
- Fetch the latest Caddy release and confirm the official image has every platform we ship.
- If our image for that version is missing a platform on GHCR, build (
new_caddy_version). - Compare each module's latest commit with
module-versions.json; on change, build (module_update) and commit the new state. - With
slimenabled, if theX.Y.Z-slimtag is missing or incomplete, build (missing_variant). - Compare each variant's base image with our build time, looking only at the platforms we ship:
the default image against
library/caddy:X.Y.Zon Docker Hub, the slim image against the base named in its Dockerfile stage (the caddy-base image on GHCR). If a base was republished, build (base_image_update).
The reason and details are passed to the build and recorded in the image labels
(caddy-build.reason, caddy-build.details, caddy-build.base-workflow) and the release notes.
Tags follow semver. v1 is a floating tag that always points at the latest v1.x.y; image
repos pin @v1 and pick up fixes automatically. A breaking change to inputs, the config
schema or the Dockerfile contract gets a new major and a one-line change in each caller.
The check workflow checks out this repo at the exact commit of the workflow being run, so the Python package always matches the workflow version the caller selected.
cd scripts
python -m unittest discover -s tests -t . -v
# dry run against an image repo checkout
cd /path/to/caddy-cloudflare
PYTHONPATH=/path/to/caddy-base/scripts GITHUB_REPOSITORY=owner/repo \
python -m check_caddy_status --dry-run
# build the caddy-base image locally
docker buildx build --platform linux/arm64 --load -t ghcr.io/caddybuilds/caddy-base:latest .MIT, see LICENSE.