Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

42 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Caddy with Rate Limiting

Test Rate Limiting Build and Push

A production-ready Caddy web server Docker image with the mholt/caddy-ratelimit plugin pre-installed for HTTP rate limiting capabilities.

Features

  • Base: Official Caddy 2 image
  • Rate Limiting: mholt/caddy-ratelimit plugin included
  • Multi-arch: Supports both amd64 and arm64
  • Auto-updates: Weekly automated builds on Sundays to get latest security patches
  • Production-ready: Built with xcaddy using stable, tested versions
  • Quantified verification: Every build runs 7 load-test scenarios with hard tolerance bands and cross-checks the plugin's own Prometheus counters against externally-observed behaviour (see Verified behavior)

Usage

Quick Start

Pull the image:

docker pull ghcr.io/coders-compass/caddy-with-ratelimit:latest

With Docker Compose

Create a docker-compose.yml:

services:
  caddy:
    image: ghcr.io/coders-compass/caddy-with-ratelimit:latest
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - ./caddy/data:/data
      - ./caddy/config:/config
    restart: unless-stopped

Example Caddyfile with Rate Limiting

Create a Caddyfile:

{
    # Order directive is important for non-standard modules
    order rate_limit before basicauth
}

example.com {
    # Rate limit based on client IP
    rate_limit {
        zone dynamic_zone {
            key {http.request.remote.host}
            events 10
            window 1m
        }
    }

    # Your backend or static files
    reverse_proxy backend:8080
}

Rate Limiting Configuration

The rate_limit directive supports various configurations:

By IP address (most common):

rate_limit {
    zone by_ip {
        key {http.request.remote.host}
        events 100
        window 1m
    }
}

By header (e.g., API key):

rate_limit {
    zone by_api_key {
        key {http.request.header.X-API-Key}
        events 1000
        window 1h
    }
}

Static (global across all clients):

rate_limit {
    zone global {
        key static
        events 10000
        window 1m
    }
}

Multiple zones:

rate_limit {
    zone strict_endpoint {
        key {http.request.remote.host}
        events 5
        window 1m
    }
    zone general {
        key {http.request.remote.host}
        events 100
        window 1m
    }
}

Distributed Rate Limiting

For multi-instance deployments sharing rate limit state:

{
    order rate_limit before basicauth

    # Configure storage backend (e.g., Redis)
    storage redis {
        address "redis:6379"
    }
}

example.com {
    rate_limit {
        zone shared {
            key {http.request.remote.host}
            events 100
            window 1m
        }
        distributed {
            read_interval 100ms
            write_interval 100ms
        }
    }

    reverse_proxy backend:8080
}

Rate Limit Response

When rate limit is exceeded:

  • HTTP Status: 429 Too Many Requests
  • Header: Retry-After (seconds until rate limit resets)

Verified behavior

Every push, PR, and weekly rebuild runs an automated load-test suite against the built image and asserts the following with hard tolerance bands. Any deviation outside the bands fails CI.

Scenario What it verifies
Light load (under limit) Under-limit traffic returns HTTP 200; no false positives.
Burst at limit Exactly at the configured budget, every request succeeds.
Burst over limit A 10-request burst against a 5/10s zone returns 4–6 × 200 + 4–6 × 429.
Sustained 30s @ 1 rps Sustained over-limit traffic returns a mix of 200/429; sliding window enforces the average rate.
Multi-IP isolation Three IPs (via trusted X-Forwarded-For) each receive their own budget; one IP exhausting its budget does not affect the others.
Multi-zone isolation Two zones with different events/window move independently; exhausting one does not trip the other.
Long sustained 60s @ 1 rps Over multiple windows, accepted RPS converges within tolerance of the configured events/window rate.

The single strongest assertion across every scenario: the plugin's own caddy_rate_limit_declined_requests_total counter delta must match the externally-observed 429 count within ±1. This catches any regression where the limiter stops firing but a 429 is still returned (or vice versa) by something else in the request path.

Each CI run uploads:

  • summary.json — machine-readable per-scenario JSON with timestamps, commit SHA, Caddy and plugin versions, status-code distribution, latency percentiles (p50–p99.99), RPS, and plugin counter deltas. Suitable for archival or aggregation.
  • summary.md — human-readable Markdown, also rendered into the workflow Step Summary and posted as a sticky comment on PRs.

Artifacts live in the workflow run under "Artifacts" with 90-day retention.

Image Details

  • Registry: GitHub Container Registry (GHCR)
  • Image: ghcr.io/coders-compass/caddy-with-ratelimit:latest
  • Base: Official Caddy 2
  • Plugin: github.com/mholt/caddy-ratelimit
  • Updates: Automatically rebuilt weekly (Sundays)
  • Source: GitHub Repository

Supply-chain and security

Every published image carries:

  • SPDX SBOM generated by BuildKit and attached as an OCI artifact on the manifest. Inspect with:
    docker buildx imagetools inspect ghcr.io/coders-compass/caddy-with-ratelimit:latest --format '{{ json .SBOM }}'
  • SLSA build provenance (mode=max) attached via BuildKit plus a signed Sigstore attestation via actions/attest-build-provenance. Verify with gh attestation verify or cosign verify-attestation.
  • Trivy vulnerability scan runs on every build before push. Builds fail on CRITICAL findings; HIGH/MEDIUM/LOW are reported. Results are uploaded to the repository's Code Scanning tab and attached to each workflow run as a trivy-results-* artifact (SARIF + table, 90-day retention). Suppressions live in .trivyignore.

Available Tags

  • latest - Latest build from main branch
  • main-<sha> - Specific commit from main branch
  • YYYYMMDD - Weekly scheduled builds

Testing Rate Limiting

To verify rate limiting is working:

# Make rapid requests
for i in {1..15}; do
  curl -i http://localhost/
done

You should see 200 OK for the first requests, then 429 Too Many Requests once the limit is exceeded.

Building Locally

git clone https://github.com/Coders-Compass/caddy-with-ratelimit.git
cd caddy-with-ratelimit
docker build -t caddy-with-ratelimit:local .

Testing Locally

The test/ directory contains the same setup CI runs:

  • Docker Compose stack with two rate-limit zones and a /metrics endpoint
  • test.sh — quick functional smoke tests (curl-based)
  • load-test.sh — the full 7-scenario load-test suite that emits test/results/summary.json + summary.md and exits non-zero on any band miss

Prerequisites: Docker, curl, jq, and oha on your PATH.

cd test
docker compose up -d
./test.sh           # functional smoke tests
./load-test.sh      # numeric verification + JSON/Markdown artefacts

Results land in test/results/. The script exits 0 only when every scenario falls inside its tolerance band, with a per-scenario fail reason explaining which band was missed.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

MIT License - See LICENSE file for details

Maintainer

@hungrybluedev

Acknowledgments

Resources

About

A production-ready Caddy web server Docker image with the mholt/caddy-ratelimit plugin pre-installed for HTTP rate limiting capabilities.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages