Edge access log
Since v0.3.3 the template’s caddy/Caddyfile writes one JSON line per request to a file with a size
ceiling. Before, the edge logged nothing, and after an incident there was no way to tell who had generated the
traffic.
Where it is
Section titled “Where it is”In the caddy_logs volume, mounted in the Caddy container at /var/log/caddy:
docker compose exec caddy tail -f /var/log/caddy/access.log # livedocker compose exec caddy ls -la /var/log/caddy # current + rolled (.log.gz)docker compose exec caddy sh -c 'zcat /var/log/caddy/access-*.log.gz' # the rolled onesA file, not stdout, because Docker’s default log driver keeps a container’s stdout with no size limit unless the daemon was configured otherwise — a busy edge on stdout is the next disk to fill. Caddy rolls the file itself, with a ceiling that does not depend on the daemon, and the volume survives the container being recreated by a deploy, which is exactly when someone needs to read it.
How big it gets
Section titled “How big it gets”| Rule | Value |
|---|---|
| Rolls at | 100 MiB |
| Keeps | the 5 newest rolled files, gzipped |
| Nothing older than | 30 days |
Worst case: ~100 MiB for the current file plus 5 compressed ones. It is a record: keep it across a rebuild if you are diagnosing, never back it up (see Volumes & backups).
What a line keeps — and what personal-data law asks
Section titled “What a line keeps — and what personal-data law asks”Each line is what Caddy logs by default: the client IP (client_ip, already resolved through
FORGE_TRUSTED_PROXY_CIDRS when a CDN sits in front), method, host, URI, status, duration, size and the request
headers. Caddy already writes Cookie, Authorization and Proxy-Authorization as REDACTED.
On top of that, three things are removed because they carry a person or a secret and diagnosis does not need them:
- the
emailquery parameter (the admin’s sign-in code step puts the operator’s e-mail in the URL); - the
codeandstateparameters of a social or Google sign-in callback (single-use secrets); - the whole
Refererheader (it repeats the previous URL, e-mail and all).
The IP is personal data under the LGPD. It is kept on purpose — it is the “who” — and what makes keeping it
proportionate is the short, size-bounded retention above. If you need a longer retention, or want the IP
anonymised, change the (access_log) snippet in your instance’s Caddyfile — for example
request>client_ip ip_mask 16 32 inside its format filter.
Questions it answers
Section titled “Questions it answers”# Who asked the most, in the current filedocker compose exec caddy cat /var/log/caddy/access.log \ | jq -r '.request.client_ip' | sort | uniq -c | sort -rn | head
# The paths one address asked fordocker compose exec caddy cat /var/log/caddy/access.log \ | jq -r 'select(.request.client_ip=="203.0.113.7") | .request.uri' | sort | uniq -c | sort -rn | head
# User agents (crawlers give themselves away here)docker compose exec caddy cat /var/log/caddy/access.log \ | jq -r '.request.headers["User-Agent"][0]' | sort | uniq -c | sort -rn | headSites of your own
Section titled “Sites of your own”A site block you add under caddy/extra/ should import access_log (and import forwarded_client) too — the
snippets belong to the main Caddyfile and apply to what it imports.
What is not here
Section titled “What is not here”Caddy’s own runtime log (certificates, upstream errors) stays on the container’s stdout, under the Docker daemon’s log settings — as do the kernel’s, the storefront’s, the checkout’s and the admin’s.
Upgrading from v0.3.2: the template’s Caddyfile gained the (access_log) snippet, imported by both site
blocks, and the caddy service gained the caddy_logs volume. Copy both, then
docker compose up -d --force-recreate caddy — see Upgrade & roll back.