- Aug 15, 2026
- 5 min read
Debugging Notes from a Week on Cloudflare (and Docker)
Over one intense week I moved three sites to a shared forms Worker, added redirects, a cron Worker, a homelab mirror in Docker and an email pipeline. Most of the time went into the work itself. A surprising amount went into small traps that each cost between ten minutes and an hour.
This is the list I wish I had read first. Every item follows the same shape: what I saw, why it happened, and what fixed it.
Cloudflare Pages
1. A new secret does nothing
- Symptom: I added a secret to a Pages project, but the Function still behaved as if it were missing.
- Cause: Pages bakes environment variables and secrets into a deployment. Existing deployments keep the old values.
- Fix: redeploy after changing secrets. Treat “secret changed” as “deploy needed”.
2. wrangler pages deployment tail refuses to start
- Symptom: the tail command asks for something I didn’t give it.
- Cause: for Pages, tailing works per deployment, not per project.
- Fix: list deployments first, copy the deployment ID, then tail that one.
3. www returns a 522
- Symptom: the apex worked,
www.returned Cloudflare error 522 (connection timed out). - Cause: the DNS record existed, but the hostname was never attached to the Pages project as a custom domain. Cloudflare had nowhere to send it.
- Fix: add
wwwunder the project’s Custom domains, then let a redirect rule pick the canonical host. The full setup is in apex vs www and pages.dev redirects.
Redirects
4. A redirect rule that never matches
- Symptom: a zone Redirect Rule saved fine and never fired.
- Cause: a leading space pasted into the URL field. The rule was valid; it simply described a URL nobody requests.
- Fix: retype the field instead of pasting, and test with
curl -sI https://example.com/right after saving.
5. Bulk redirects “don’t work”
- Symptom: a new Bulk Redirect for a
*.pages.devhostname still served the site for a while. - Cause: propagation. Bulk Redirects take time to reach every edge.
- Fix: wait a few minutes before debugging. I now keep a small script that
checks every expected status code and
Locationheader, and I run it again after the change has settled.
Workers
6. A warning about a header value
- Symptom: the Worker logged a warning when sending an ntfy alert.
- Cause: I put an em dash in the
Titleheader. HTTP header values should stay ASCII. - Fix:
New enquiry - site.cominstead ofNew enquiry — site.com, plus a test asserting every header value matches/^[\x20-\x7e]*$/.
7. 403 from my own site, only from Python
- Symptom: a Python script calling one of my endpoints got
403 Forbidden.curland the browser got the expected response. - Cause: Cloudflare rejects the default
Python-urllibuser agent before the Worker runs (most likely Browser Integrity Check; I didn’t pin down which feature). It happened on all my zones. - Fix: send a normal
User-Agentfrom scripts. Mail clients and real browsers were never affected, but any test script must be.
8. A test that fails only when the whole suite runs
- Symptom: a confirm-link test failed with “subscription not found”, but passed alone.
- Cause: the previous test didn’t await its
ctx.waitUntil()promises. A late email send from that test landed in the next test’s mocks, which then read the wrong confirmation link. - Fix: drain the pending promises in
beforeEach:
beforeEach(async () => {
await Promise.allSettled(pending ?? []);
pending = [];
});
9. Type checking fights itself
- Symptom:
tscerrors about duplicate or conflicting global types. - Cause: Node’s types (for
node:test) and@cloudflare/workers-typesboth define globals such asRequestandfetch. - Fix: type-check only
src/(Worker code). The tests run under Node with--experimental-strip-typesand don’t go throughtsc.
wrangler and local tooling
10. 401 with a token that “works”
- Symptom: wrangler commands worked, but the API returned 401 when I reused wrangler’s login token in a script.
- Cause: I read the token from
~/.config/.wrangler/config/default.toml, a copy that had expired months earlier. The live login was in~/.wrangler/config/default.toml. - Fix: for anything long-running, create a scoped API token (for example Workers KV Storage: Read) and never reuse the OAuth login.
11. The wheel that wasn’t there
- Symptom: a freshly built Python wheel didn’t show up in
git status. - Cause:
uv build --out-dir <dir>writes a.gitignorecontaining*into the output directory. - Fix: delete that
.gitignorewhen the wheel is meant to be committed (my Pyodide app pins one).
12. Auditing code that isn’t the code
- Symptom: my audit of a repo listed problems that had already been fixed.
- Cause: the local clone was weeks behind GitHub.
- Fix:
git fetch, then readorigin/<branch>instead of the working copy. Never reset a clone that has uncommitted work; it may be someone’s (often my own) work in progress.
Docker and Python
13. The container can’t write its database
- Symptom:
unable to open database fileon first start. - Cause: the bind-mounted
./datadidn’t exist, so Docker created it, owned by root. The container runs as uid 1000. - Fix:
mkdir -p databeforedocker compose up, and say so in the README.
14. Empty logs from a working service
- Symptom:
docker compose logsshowed nothing, although the service was doing its job. - Cause: Python block-buffers stdout when it isn’t a terminal. My loop printed a line and slept for 15 minutes; the line stayed in the buffer. Errors still appeared because stderr isn’t buffered the same way.
- Fix:
ENV PYTHONUNBUFFERED=1in the Dockerfile. I proved it with two identical containers, one with the variable and one without.
The pattern behind the list
| Category | Items | Common thread |
|---|---|---|
| Configuration timing | 1, 5 | The change is saved but not yet active |
| Configuration content | 3, 4, 6 | It looks right in the dashboard, but isn’t |
| Environment | 7, 10, 13 | The code is fine; the context around it isn’t |
| Observability | 2, 12, 14 | I was looking at the wrong thing |
| Tests | 8, 9 | Shared state between test runs or type systems |
Takeaways
- After any change at the edge, verify from outside with
curl -sIbefore assuming the change failed or succeeded. - When code is right and behaviour is wrong, check what is actually running: the deployment, the clone, the token, the user ID.
- Keep a gotchas file per project. Each line here took minutes to find and seconds to read.