Platform, Caddy custom build
How the Caddy image with the rate_limit module is built
Olympus's Caddy needs the caddy-ratelimit module. The official Caddy image doesn't include it. Olympus ships a custom build via caddy-build.yml.
Why custom
Caddy's plugin model: plugins are compiled into the binary, not loaded at runtime. To use rate_limit, you must build a Caddy binary that includes it.
Olympus's [Security, Rate Limiting](/docs/security/web-attacks/rate-limiting) uses rate_limit extensively. Required.
The build workflow
.github/workflows/caddy-build.yml:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Build Caddy with modules
run: |
docker run --rm -v "$(pwd)":/output \
caddy:builder \
xcaddy build \
--with github.com/mholt/caddy-ratelimit \
--output /output/caddy
- name: Smoke test
run: |
./caddy version
./caddy list-modules | grep ratelimit
- name: Build image
run: |
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t ghcr.io/olympusoss/caddy:$VERSION \
--push .
- name: Pin digest
run: |
DIGEST=$(docker buildx imagetools inspect ghcr.io/olympusoss/caddy:$VERSION --raw | jq -r .manifests[0].digest)
echo "DIGEST=$DIGEST" >> $GITHUB_ENV
- name: Update platform compose
run: |
sed -i "s|ghcr.io/olympusoss/caddy@sha256:[a-f0-9]*|ghcr.io/olympusoss/caddy@sha256:$DIGEST|g" prod/compose.prod.yml
git add prod/compose.prod.yml
git commit -m "ci(caddy): pin to $DIGEST"
git pushThe workflow:
- Compiles Caddy with
caddy-ratelimit. - Smoke-tests that the module is included.
- Builds a multi-arch container image.
- Pushes to GHCR.
- Updates
compose.prod.ymlto reference the new digest. - Commits and pushes the digest bump, which triggers
deploy.yml.
End-to-end: ~15 minutes.
When to re-build
- New upstream Caddy release.
- New
caddy-ratelimitrelease. - Security advisory affecting any of the above.
Triggered manually via gh workflow run caddy-build.yml or on a weekly schedule.
Why reproducible
The build inputs:
- Caddy version (pinned by SHA in the
xcaddy buildcommand). caddy-ratelimitversion (pinned by Go module version).caddy:builderbase image (pinned by digest).
Same inputs → same Caddy binary (modulo Go's build determinism). The output image digest is the verifiable artifact.
Verifying
To verify what's running matches what was built:
ssh prod 'podman inspect olympus-caddy --format "{{.ImageDigest}}"'
# Compare against compose.prod.yml
grep caddy prod/compose.prod.ymlThese must match. If they don't, someone (or something) pulled a different image.
Smoke test in the workflow
The build workflow includes:
- name: Verify rate_limit module loaded
run: |
./caddy adapt --config dev/Caddyfile.test --pretty | grep ratelimitCatches the case where the module compiled but isn't actually accessible.