Skip to content
v0.3

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.

In the caddy_logs volume, mounted in the Caddy container at /var/log/caddy:

Terminal window
docker compose exec caddy tail -f /var/log/caddy/access.log # live
docker 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 ones

A 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.

RuleValue
Rolls at100 MiB
Keepsthe 5 newest rolled files, gzipped
Nothing older than30 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 email query parameter (the admin’s sign-in code step puts the operator’s e-mail in the URL);
  • the code and state parameters of a social or Google sign-in callback (single-use secrets);
  • the whole Referer header (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.

Terminal window
# Who asked the most, in the current file
docker 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 for
docker 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 | head

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.

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.