Cloudflare¶
Cloudflare is the edge for everything I expose. It holds the DNS, it fronts the apps, and it's where I do access control — so a misconfiguration here is visible to the whole internet, not just the LAN.
Domain delegation¶
Buy the domain, add it to Cloudflare, then change the nameservers at the registrar to the ones Cloudflare gives you. If the domain is registered through Route 53, that's Registered domains → the domain → edit nameservers. Give it about 24 hours to propagate; until then the zone in Cloudflare is inert and nothing resolves.
Cloudflare scans existing DNS records on import. Check that list before you cut over — a half-imported zone is the usual cause of "it worked yesterday and now it doesn't".
The tunnel¶
The tunnel connects a self-hosted app to a public hostname without opening a port on the home network. Full setup is in Self Hosted → Cloudflare Tunnel, but the part worth remembering: run cloudflared through docker compose rather than the one-off command Cloudflare hands you, so the token survives a reboot.
services:
cloudflare-tunnel:
image: cloudflare/cloudflared:latest
container_name: cloudflare-tunnel
restart: unless-stopped
command: tunnel --no-autoupdate run --token <your-cloudflared-token>
The public hostname points at the app's local IP and port. Cloudflare is connecting to the app exactly the way you would on the LAN — that's why the tunnel works from inside the network too.
Access control: Zero Trust or mTLS¶
Two ways to stop strangers reaching an app, and they solve different problems.
Zero Trust Access — configure an application against the domain, pick an identity provider. The default is a one-time PIN; Google works as an IdP if you'd rather not email yourself codes. Good for apps you want to reach from a browser with a login.
mTLS — require a client certificate instead. Set it up in the domain portal under SSL/TLS → Client Certificates, then — this is the part people miss — add the host under Hosts. Without the host listed, the certificate exists and the rule does nothing. Enforce it with the Enforce mTLS authentication template in Security → Security Rules, changing the trigger from URI Path to Hostname with a wildcard (paperless.domain.com*).
Bundle the cert and key into a .pfx for mobile clients:
The passphrase needs to be long — I used 20 characters and short ones failed. Mobile apps accept the .pfx; browsers don't, so mTLS is a mobile-first control.
CrowdSec at the proxy layer¶
A CrowdSec bouncer on the reverse proxy still lets every attack attempt reach the proxy — you just get a 403 and a log line for each one. A Cloudflare Worker bouncer bans at the edge instead, so the traffic never hits the home network.
The free-tier gotcha: only_include_decisions_from must be set to ["cscli", "crowdsec"], and you must actually list the routes to protect (*.thomaswildetech.com/* for all subdomains). Set the worker route fail mode to Fail Open — a bouncer that fails closed takes the whole site down when it can't reach the CrowdSec agent. Bans land in a Cloudflare KV namespace, visible under Storage & Databases → KV.
Full config: CrowdSec CloudFlare Worker Bouncer.
Pages deploys¶
Frontends deploy to Cloudflare Pages through Git integration, push to deploy. Two lessons that cost me time:
- A monorepo with Python files at the root gets auto-detected as a Python project and Pages runs
pip install ., which fails on a flat layout. SetNIXPACKS_NO_PYTHON=1or point the root directory at the frontend subdirectory. - Static export on Pages does not run Next.js
rewrites(), so every relativefetch()resolves against the Pages origin instead of the API. UseNEXT_PUBLIC_API_URL— and only theNEXT_PUBLIC_prefix reaches browser code.
A pushed commit that never appears on the live site usually means the Pages build failed and the previous successful build is still being served. Check the build log, not the git log.