Measured on 2026-10-15: the feed list and item lists went out uncompressed at over 100 KB; ipx read 2.6 GB of feed bodies a day and the directory 3.9 GB. - ipx-web compresses with zstd or gzip, leaving out /api/events, which a compressor would hold back, and /media (#239). - A body identical to the last one (SHA-256 in feeds.body_hash) is taken as a 304, unless something is missing a read would fill or the check is forced; and a feed without a schedule of its own whose body has been the same waits a twelfth of that, up to four hours, beside the failure backoff (#242). 35 Patreon show feeds sent ~300 KB each hourly, about 250 MB a day. - The directory checks each feed every twelve hours, not six (#243). - reqwest accepts brotli and zstd too (#244). #240 (cache the feed list's counts) is left open: at production's size on the test Postgres that count takes 12 ms and All Subscriptions 32 ms, against 366 and 300 ms in production, so the cause is production's database, not the query (#241). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
126 lines
4.0 KiB
Caddyfile
126 lines
4.0 KiB
Caddyfile
# ipx-web (#203): the frontend as static files, and everything else passed to ipx, on one origin,
|
|
# so the session cookie, live updates and Cloudflare Access work as they did when ipx served it.
|
|
# IPX_UPSTREAM where ipx is, host:port (ipx:8099)
|
|
# WEB_BIND where to listen (:8080)
|
|
# WEB_TRACING on: a span for each request to OpenTelemetry (OTEL_EXPORTER_OTLP_ENDPOINT,
|
|
# OTEL_EXPORTER_OTLP_PROTOCOL, OTEL_SERVICE_NAME), continuing the page's traceparent
|
|
# and passing it to ipx, so one trace runs browser, Caddy, ipx (#213). Off: none,
|
|
# and no export failing every few seconds where there is nothing to send to.
|
|
# TUNNEL_PEER the one address whose Cf-Access-* headers are passed on: where cloudflared's
|
|
# requests come from. Nobody else's are, so nobody else can claim to be signed in
|
|
# by Cloudflare. Unset passes none.
|
|
{
|
|
admin off
|
|
auto_https off
|
|
# Everything else Caddy logs, its errors among them, which carry the request too: the same
|
|
# filter as the access log's below.
|
|
log default {
|
|
output stderr
|
|
format filter {
|
|
wrap json
|
|
fields {
|
|
request>uri query {
|
|
delete token
|
|
}
|
|
request>headers delete
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
(tracing-on) {
|
|
tracing {
|
|
span ipx-web
|
|
}
|
|
}
|
|
(tracing-off) {
|
|
}
|
|
|
|
http://{$WEB_BIND::8080} {
|
|
import tracing-{$WEB_TRACING:off}
|
|
|
|
root * {$WEB_ROOT:/srv}
|
|
|
|
# One JSON line a request on stdout, for Loki and the dashboard's Page row (#210). Not the
|
|
# shared token a ?token= link carries, nor the request's headers, which hold the session
|
|
# cookie and Cloudflare Access's token.
|
|
log {
|
|
output stdout
|
|
format filter {
|
|
wrap json
|
|
fields {
|
|
request>uri query {
|
|
delete token
|
|
}
|
|
request>headers delete
|
|
}
|
|
}
|
|
}
|
|
|
|
# Compressed, zstd or gzip as the browser takes them: the feed list and a page of items went
|
|
# out at over 100 KB each (#239). Not the live updates, which a compressor would hold back
|
|
# until it had enough to send, nor /media, which is audio and video already compressed.
|
|
@compress not path /api/events /media/*
|
|
encode @compress zstd gzip
|
|
|
|
# Before anything else, so no page or API call sees them: only ipx sets these.
|
|
@forged not remote_ip {$TUNNEL_PEER:255.255.255.255}
|
|
request_header @forged -Cf-Access-Authenticated-User-Email
|
|
request_header @forged -Cf-Access-Jwt-Assertion
|
|
|
|
@api path /api/* /media/*
|
|
handle @api {
|
|
# Live updates are a stream: flushed as they come, never buffered.
|
|
reverse_proxy {$IPX_UPSTREAM:ipx:8099} {
|
|
flush_interval -1
|
|
}
|
|
}
|
|
|
|
# Signing in with the shared token in the address (?token=): ipx sets the session cookie and
|
|
# sends the browser on to the page without it.
|
|
@tokened {
|
|
path / /admin
|
|
query token=*
|
|
}
|
|
handle @tokened {
|
|
rewrite * /api/signin?{query}
|
|
reverse_proxy {$IPX_UPSTREAM:ipx:8099}
|
|
}
|
|
|
|
# The app and the admin page go to whoever ipx says may have them: a browser not signed in is
|
|
# sent to /login, anyone but an admin asking for /admin to the app.
|
|
@page path / /index.html /admin /admin.html /admin.js
|
|
handle @page {
|
|
forward_auth {$IPX_UPSTREAM:ipx:8099} {
|
|
uri /api/page-check
|
|
}
|
|
rewrite / /index.html
|
|
rewrite /admin /admin.html
|
|
header Cache-Control no-cache
|
|
file_server
|
|
}
|
|
|
|
# The sign-in form, except for someone Cloudflare Access vouches for, who is sent to the app.
|
|
@login path /login /login.html
|
|
handle @login {
|
|
forward_auth {$IPX_UPSTREAM:ipx:8099} {
|
|
uri /api/login-check
|
|
}
|
|
rewrite * /login.html
|
|
header Cache-Control no-cache
|
|
file_server
|
|
}
|
|
|
|
# Scripts and the stylesheet are named by a hash of their contents (web/build.mjs), so a browser
|
|
# keeps them a year; the icons a day, under names that do not change.
|
|
@hashed path /app.js /admin.js /login.js /app.css /hls.js /boot.js
|
|
header @hashed Cache-Control "public, max-age=31536000, immutable"
|
|
# The same PNG as /favicon.png, for a browser that asks for /favicon.ico on its own.
|
|
header /favicon.ico Content-Type image/png
|
|
@icons path *.png *.svg *.ico *.woff2
|
|
header @icons Cache-Control "public, max-age=86400"
|
|
handle {
|
|
file_server
|
|
}
|
|
}
|