feat(compose): publish on every interface, so the host's own address answers
All checks were successful
PR checks / checks (pull_request) Successful in 9m56s

D55 bound the published port to 127.0.0.1, on the reasoning that TLS terminates
at a proxy on the same host and nothing else has business reaching the container.
That is right for the host this ends up on and wrong for every step before it: a
loopback binding cannot be opened from a browser on another machine, which is the
first thing an operator wants to do — look at the site on the VM's own address,
before DNS exists, before the proxy exists, from a desktop or a phone.

The port line is now "${SITE_BIND_ADDR:-0.0.0.0}:${SITE_HOST_PORT:-4321}:4321",
so http://<vm-ip>:4321 answers out of the box, the way a normal bridge publish
behaves. Which addresses it answers on is a variable rather than an edit:
SITE_BIND_ADDR narrows it to one interface, or back to loopback, without touching
a file that `docker compose pull` replaces. That also retires the "change the port
line yourself" instruction DEPLOY.md had to give a proxy running in another
container or on another machine.

What is given up, said plainly in DEPLOY.md §3.1: on a host with a public address
the default answers on port 4321 from the internet, plain HTTP beside the proxy's
443, with no proxy in the path to set X-Forwarded-For — so signups arriving that
way share one rate-limit bucket. There is no login and no secret behind it, so it
is untidy rather than dangerous, and both remedies are named (firewall the port,
or narrow the binding).

Verified by running it, not only by reading it: `docker compose config` accepts
both bindings and resolves host_ip 0.0.0.0 and 127.0.0.1 respectively; the stack
came up healthy, `docker compose port site 4321` reported 0.0.0.0:4321, and the
site answered 200 on both 127.0.0.1 and the machine's LAN address, still carrying
its own per-page CSP.

Recorded as D59, amending D55. Count of record fifty-nine.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-26 23:40:46 -05:00
parent e60812fb34
commit b86f4cabf0
4 changed files with 88 additions and 27 deletions

37
PLAN.md
View File

@@ -236,7 +236,7 @@ Taken by the org lead (Colby Whitlock) on 2026-08-19. Recorded so they are not r
**Decisions after D13 are recorded where they were taken**, in the section describing the phase that
raised them, rather than appended here — a decision is only re-litigated when its reasoning is
somewhere other than the thing it decided. The count of record is **fifty-seven**:
somewhere other than the thing it decided. The count of record is **fifty-nine**:
| # | Where | What it settled |
|---|---|---|
@@ -252,6 +252,7 @@ somewhere other than the thing it decided. The count of record is **fifty-seven*
| D51D53 | §6, "How phase 11 validated it" | The chrome and the head follow the brand mount while the consent sentence does not, the documentation half gets phase 10's skip-link fix, and no twelfth check |
| D54D57 | §6, "How phase 12 delivered it" | A merge deploys (amending D6), the proxy is documented by its requirements rather than by an example, the container trusts the forwarded headers with nothing to configure, and the operator note is its own file while the two policy files are pointers |
| D58 | §6, "How phase 12 delivered it" | `node_modules` ships in three layers because Cloudflare refuses a request body over 100 MB, and the workflow counts layers before it pushes |
| D59 | §6, "How phase 12 delivered it" | The container publishes on every interface (amending D55), with `SITE_BIND_ADDR` to narrow it |
---
@@ -522,8 +523,8 @@ The last phase, and the one that turns a repository into a deployment: a two-sta
pull-only `docker-compose.yml` carrying both bind mounts, `.env.example`, the publishing workflow,
`CONTRIBUTING.md` with the AI-disclosure requirement, the community-health files this repository was
the only one in the organisation to lack, and `DEPLOY.md`. Four decisions, **D54D57**, taking the
count of record to **fifty-seven**and a fifth, **D58**, added when the merge that shipped the
phase could not publish its own image.
count of record to **fifty-seven**plus **D58**, added when the merge that shipped the phase could
not publish its own image, and **D59**, which amends D55's loopback binding. **Fifty-nine.**
It also found a defect that would have made the closed beta impossible, and it is the only phase
that could have found it. Everything before this ran the site the way a developer runs it: one
@@ -555,8 +556,9 @@ Docker socket and no compose directory.
The site runs on its own host, behind whatever reverse proxy the org lead puts there. So
`DEPLOY.md` does not carry a worked Caddyfile or nginx block that would be wrong for three readers
out of four; it states the four things the proxy must do, and the container binds to **`127.0.0.1`**
by default so that the safe configuration is the default one.
out of four; it states the four things the proxy must do. The container originally bound to
`127.0.0.1` by default so that the safe configuration was the default one; **amended by D59
(2026-08-26)** — it publishes on every interface, and which interfaces is a variable.
Two of the four are worth repeating here because they are silent when wrong:
@@ -670,6 +672,31 @@ number it reports for the broken layer (108 MB) agrees with what the registry re
This is a workflow step and not a twelfth check script: it needs a built image rather than a source
tree, which is the one thing the eleven never have. D53 holds.
#### D59 — the container publishes on every interface, which amends D55
D55 bound the published port to `127.0.0.1`, reasoning that TLS terminates at a proxy on the same
host and nothing else has business reaching the container. That is the right default for the host
this eventually runs on, and the wrong one for every step before it: a loopback binding cannot be
opened from a browser on another machine, which is exactly what an operator wants to do first —
look at the thing on the VM's own address, before DNS exists, before the proxy exists, from a
desktop or a phone that is not the VM.
The org lead settled it on 2026-08-26: **publish on all interfaces**, the way a normal bridge
publish behaves. `docker-compose.yml` now reads
`"${SITE_BIND_ADDR:-0.0.0.0}:${SITE_HOST_PORT:-4321}:4321"`, so the site answers on
`http://<vm-ip>:4321` out of the box.
**Which addresses it answers on is a variable, not an edit.** `SITE_BIND_ADDR` in `.env` narrows it
to one interface or back to loopback without touching a file that `docker compose pull` replaces,
which also removes the "change the port line" instruction D55 had to give proxies running in another
container or on another machine.
What is honestly given up: on a host with a public address, port 4321 answers from the internet
directly — plain HTTP beside the proxy's 443, and with no proxy in the path to set `X-Forwarded-For`,
so signups arriving that way share one rate-limit bucket. The site has no login and no secret behind
it, so this is untidy rather than dangerous, and `DEPLOY.md` §3.1 says so and gives both remedies
(firewall the port, or narrow the binding).
---
## 7. Branding is bind-mounted data