- Dockerfile 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .forgejo/workflows | ||
| compose.yaml | ||
| Dockerfile | ||
| README.md | ||
Caddy (hardened custom build)
Caddy is a fast, extensible web server with automatic HTTPS. This repo builds a custom Caddy image — stock Caddy plus a handful of plugins — on top of Docker's Hardened Images.
Image: scm.nullnetwork.cc/erestodo/caddy:latest
This image is built nightly automatically and every time any changes are pushed to the Dockerfile — it is up to whomever uses this image to update as often as they desire.
Why Docker Hardened Images (DHI)
Built and run on Docker Hardened Images:
- Build:
dhi.io/golang(alpine dev) — compiles a fully static binary. - Run:
dhi.io/static— distroless, no shell, no package manager, runs as nonroot65532.
The result is a minimal, near-zero-CVE runtime with a tiny attack surface: no
shell to exploit, nothing to apt/apk, and no root. The build stage is thrown
away — only the static caddy binary ships.
Modules
Built with xcaddy:
| Module | Purpose |
|---|---|
| caddy-cbrotli → caddy-brotli | Pure-Go Brotli (encode br) — no CGO |
| caddy-dns/cloudflare | Cloudflare DNS-01 ACME challenge |
| caddy-cloudflare-ip | Cloudflare IP ranges for trusted_proxies |
| caddy-combine-ip-ranges | Merge multiple IP-range sources |
| caddy-geoblock | Allow/deny requests by country (GeoIP) |
| caddy-crowdsec-bouncer | Block IPs flagged by CrowdSec |
Usage
Provide a Caddyfile, then:
docker compose up -d
Config is read from /config/Caddyfile and all state (LE certs, etc.) persists
in /data. See compose.yaml for the full, commented setup.
Configuration
A Caddyfile that exercises every bundled module. Secrets come from the
environment (set them in compose.yaml); never hard-code them here.
Global options — ACME email, trusted proxies, and the CrowdSec Local API:
{
email [email protected]
# Trust Cloudflare's ranges so X-Forwarded-For carries the real client IP.
# `combine` lets you fold in more sources later (e.g. static LAN ranges).
trusted_proxies combine {
cloudflare
}
crowdsec {
api_url {env.CROWDSEC_URL}
api_key {env.CROWDSEC_API_KEY}
ticker_interval 15s
}
}
Trusting the proxy ranges matters: without it, CrowdSec and GeoBlock would see
Cloudflare's edge IPs instead of the actual visitor, and every decision would be
wrong. combine + cloudflare keep that list auto-refreshed.
Site block — compression, filtering, upstream, and DNS-01 certs:
example.com {
# Brotli (pure-Go module) + zstd, most-preferred first.
encode zstd br
# `route` preserves written order: drop bad IPs, then geo-filter, then serve.
route {
crowdsec
geoblock {
db_path /data/GeoLite2-Country.mmdb
deny_countries CN RU
blocked_status 403
blocked_message "Not available in your region."
}
reverse_proxy backend:8080
}
# DNS-01 via Cloudflare — works behind a proxy and issues wildcards.
tls {
dns cloudflare {env.CF_API_TOKEN}
}
}
Notes:
- Order is deliberate. Putting
crowdsecandgeoblockinsideroute {}runs them in the order written — bad IPs are rejected before geo-filtering or proxying, and you avoid fiddling with globalorderdirectives. - GeoBlock needs a database. Supply a MaxMind
GeoLite2-Country.mmdb; drop it in the/datavolume (already owned by65532) so it's readable. Swapdeny_countriesforallow_countriesto flip to an allowlist. - DNS-01 is optional. Remove the
tlsblock for default HTTP/TLS-ALPN challenges; keep it if you're behind Cloudflare or want wildcard certs.
Environment
Pass these to the container (via environment: / an .env file in
compose.yaml) — they're referenced above as {env.NAME}:
| Variable | Used by |
|---|---|
CF_API_TOKEN |
Cloudflare DNS-01 ACME |
CROWDSEC_URL |
CrowdSec Local API endpoint |
CROWDSEC_API_KEY |
CrowdSec bouncer auth |
Hardening notes (in compose.yaml)
sysctls: net.ipv4.ip_unprivileged_port_start=0— lets the nonroot user bind:80/:443without capabilities or root.cap_drop: [ALL],no-new-privileges,read_only— defense in depth; the container needs none of it, and/data//configstay writable as volumes.- Named volumes inherit
65532:65532ownership on first init — no chown, no init container. Seed theCaddyfilebefore first start.
Thanks
To the Caddy team, every module author above, CrowdSec, and Docker for the hardened images — this build is just standing on your work.