Full rewrite of README.md, MODULE.md, caddyfile.example, and docs/ to match the actual code in caddywaf.go, handler.go, config.go, rules.go, ratelimiter.go, blacklist.go, geoip.go, request.go, and types.go. Notable corrections: - docs/configuration.md now lists every directive recognised by the Caddyfile parser (directiveHandlers in config.go) and separates JSON-only fields (MaxRequestBodySize, GeoIPFailOpen, Tor.CustomTORExitNodeURL). - docs/rules.md documents the JSON tag mismatch on Rule.Action: the struct tag is "mode" while the bundled rule files commonly use "action". - docs/ratelimit.md corrects the match_all_paths semantics to match ratelimiter.go (true rate-limits all paths; false + non-empty paths rate-limits only matching paths; false + empty paths is a no-op). - docs/dynamicupdates.md adds a reload matrix distinguishing what fsnotify reloads from what requires caddy reload. - docs/metrics.md aligns the JSON schema with handleMetricsRequest and notes that all counters are process-local. - docs/prometheus.md uses Gauge.set instead of Counter.inc to match the process-local monotonic counter semantics. - caddyfile.example no longer references inexistent directives (country_block, custom_response block form). Also adds the missing CHANGELOG entry for v0.3.0 (duplicate response headers fix and CIDR support in IP blacklist) and removes emoji from all user-facing documentation.
4.6 KiB
Docker
The repository ships a Dockerfile and a docker-compose.yml. This document describes both, and a few common operational patterns.
Image build
docker build -t caddy-waf .
The build is a two-stage build:
Stage 1 — builder (golang:1.24-alpine)
- Installs
gitandwget, plusxcaddy. - Clones
https://github.com/fabriziosalmi/caddy-waf.git. - Runs
go mod tidy. - Downloads the GeoLite2 Country database from
https://git.io/GeoLite2-Country.mmdb. - Compiles a Caddy binary with the WAF module via
xcaddy build --with github.com/fabriziosalmi/caddy-waf=./.
The Dockerfile clones from GitHub regardless of the build context. To use a local checkout (e.g. with uncommitted changes) modify the Dockerfile to
COPY . /app/caddy-wafinstead ofgit clone, or build withxcaddyoutside Docker andCOPYthe binary in.
Stage 2 — runtime (alpine:latest)
- Copies
/app/caddy-waf/caddyto/usr/bin/caddy. - Copies
GeoLite2-Country.mmdb,rules.json,ip_blacklist.txt,dns_blacklist.txtinto/app/. - Copies the local
Caddyfileinto/app/. - Creates a
caddy:caddynon-root user,chowns/app, andUSER caddy. - Exposes port
8080. - Runs
caddy run --config /app/Caddyfile.
Running
docker run --rm -p 8080:8080 caddy-waf
| Flag | Effect |
|---|---|
-p 8080:8080 |
Map host port 8080 to the container's HTTP port. Adjust the host side as needed. |
-d |
Detach. |
--name caddy-waf |
Name the container for easier management (docker logs caddy-waf). |
-v $(pwd)/Caddyfile:/app/Caddyfile:ro |
Mount a custom Caddyfile read-only. |
-v $(pwd)/rules.json:/app/rules.json |
Mount a custom rule set (read-write so fsnotify can detect updates). |
-v $(pwd)/ip_blacklist.txt:/app/ip_blacklist.txt |
Mount a custom IP blacklist. |
-v $(pwd)/dns_blacklist.txt:/app/dns_blacklist.txt |
Mount a custom DNS blacklist. |
-v $(pwd)/logs:/app/logs |
Persist the JSON log file (point log_path to /app/logs/debug.json in the Caddyfile). |
A typical production run, with all configuration mounted from the host:
docker run -d --name caddy-waf \
-p 80:8080 \
-v $(pwd)/Caddyfile:/app/Caddyfile:ro \
-v $(pwd)/rules.json:/app/rules.json \
-v $(pwd)/ip_blacklist.txt:/app/ip_blacklist.txt \
-v $(pwd)/dns_blacklist.txt:/app/dns_blacklist.txt \
-v $(pwd)/GeoLite2-Country.mmdb:/app/GeoLite2-Country.mmdb:ro \
-v $(pwd)/logs:/app/logs \
caddy-waf
Docker Compose
The shipped docker-compose.yml is a minimal example. A more complete file:
services:
caddy-waf:
build: .
image: caddy-waf:latest
container_name: caddy-waf
restart: unless-stopped
ports:
- "80:8080"
volumes:
- ./Caddyfile:/app/Caddyfile:ro
- ./rules.json:/app/rules.json
- ./ip_blacklist.txt:/app/ip_blacklist.txt
- ./dns_blacklist.txt:/app/dns_blacklist.txt
- ./GeoLite2-Country.mmdb:/app/GeoLite2-Country.mmdb:ro
- ./logs:/app/logs
healthcheck:
test: ["CMD", "wget", "-q", "-O-", "http://localhost:8080/"]
interval: 30s
timeout: 5s
retries: 3
docker compose up -d
docker compose logs -f caddy-waf
Hot reload inside a container
fsnotify works inside containers as long as the watched files are on a normal mount (bind mount, named volume, or tmpfs). Edit the mounted rules.json from the host and the watcher inside the container will see the WRITE event and reload — see dynamicupdates.md for the reload matrix.
For settings that require a full Provision cycle (rate limiter, GeoIP, Tor, custom responses, log paths) you must reload Caddy itself:
docker exec caddy-waf caddy reload --config /app/Caddyfile
Operational checklist
- Pin the base image. The shipped Dockerfile uses
golang:1.24-alpineandalpine:latest. For reproducible builds pinalpine:<version>. - Run as non-root. Already done by the Dockerfile (
USER caddy). Do not switch torootfor convenience. - Mount configuration read-only when possible. Only the rule files and blacklists need to be writable for
fsnotifyto fire. - Expose
metrics_endpointonly on a private network. Use a separate Caddy site / route or a sidecar to apply authentication. - Persist the log file. The JSON log written to
log_pathis otherwise lost when the container is replaced. - Set resource limits.
--memory,--cpus(or the equivalent Compose / Kubernetes settings). - Keep the GeoLite2 database fresh. Re-build periodically or mount a managed copy from the host.