Custom Caddy build for Null Network
  • Dockerfile 100%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-08-06 16:48:29 -05:00
.forgejo/workflows DHI images need login too 2026-08-06 16:06:12 -05:00
compose.yaml Example Docker compose file to match new Dockerfile 2026-08-06 15:38:47 -05:00
Dockerfile And FIPS are paid sub tags, so normal -dev tags it is... 2026-08-06 16:27:28 -05:00
README.md Add build cadence note to README 2026-08-06 16:48:29 -05:00

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 nonroot 65532.

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 crowdsec and geoblock inside route {} runs them in the order written — bad IPs are rejected before geo-filtering or proxying, and you avoid fiddling with global order directives.
  • GeoBlock needs a database. Supply a MaxMind GeoLite2-Country.mmdb; drop it in the /data volume (already owned by 65532) so it's readable. Swap deny_countries for allow_countries to flip to an allowlist.
  • DNS-01 is optional. Remove the tls block 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/:443 without capabilities or root.
  • cap_drop: [ALL], no-new-privileges, read_only — defense in depth; the container needs none of it, and /data//config stay writable as volumes.
  • Named volumes inherit 65532:65532 ownership on first init — no chown, no init container. Seed the Caddyfile before 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.