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

View File

@@ -136,32 +136,47 @@ curl -sI http://127.0.0.1:4321/ | head -n 1
curl -sI http://127.0.0.1:4321/ | grep -i content-security-policy | cut -c1-120
```
The port is published on every interface by default ([§3.1](#31-forward-to-the-published-port)), so
the same two commands work from any other machine on the network with the host's address in place of
`127.0.0.1` — which is the quickest way to look at the site in a real browser before DNS or the
proxy exists.
The second command matters more than the first. The site sends its **own** Content-Security-Policy,
per page, built from the hashes of that page's inline scripts and styles. If it is missing, do not
add one at the proxy — see below.
## 3. Putting a proxy in front of it
The container publishes on **`127.0.0.1:4321`** by default and speaks plain HTTP. Any reverse proxy
will do; the site has no opinion about which. What it does have is four requirements, and the third
is the one that is easy to get wrong and quiet when you do.
The container publishes on **port 4321 of every interface** by default and speaks plain HTTP, so it
answers both on `http://127.0.0.1:4321` and on the host's own address — `http://<vm-ip>:4321`. Any
reverse proxy will do; the site has no opinion about which. What it does have is four requirements,
and the third is the one that is easy to get wrong and quiet when you do.
### 3.1 Forward to the published port
Whatever your proxy calls it: forward `runicgateway.com` (and `www.` if you want it) to
`http://127.0.0.1:4321`. There are no WebSockets, no long-polling, no streaming responses and no
upload larger than a form field, so no timeout or buffering setting needs changing.
`http://<host>:4321` — loopback if the proxy runs on this same machine, the host's address if it
runs in another container or on another machine. There are no WebSockets, no long-polling, no
streaming responses and no upload larger than a form field, so no timeout or buffering setting needs
changing.
**If your proxy is itself in a container, or on another machine,** it cannot reach the host's
loopback. Either change the port line in `docker-compose.yml` to publish on all interfaces —
A proxy on a shared Docker network can address the service as `site:4321` instead and skip the host
port entirely.
```yaml
ports:
- "${SITE_HOST_PORT:-4321}:4321"
**Narrowing the binding.** `SITE_BIND_ADDR` in `.env` decides which addresses the port answers on,
and nothing else in the site changes with it:
```bash
SITE_BIND_ADDR=0.0.0.0 # every interface — the default
SITE_BIND_ADDR=192.168.1.10 # one interface: the LAN, but not a public NIC
SITE_BIND_ADDR=127.0.0.1 # loopback only: a proxy on THIS host and nothing else
```
— and firewall the port so only the proxy reaches it, or put the proxy on a shared Docker network
and address the service as `site:4321`, publishing no host port at all.
**On a host with a public address, the default means port 4321 answers from the internet directly**,
beside whatever the proxy serves on 443 — plain HTTP, no TLS, and no proxy in the path to set
`X-Forwarded-For` ([§3.2](#32-set-x-forwarded-for)), so signups arriving that way share one
rate-limit bucket. There is no login and nothing to steal, so this is untidy rather than dangerous —
but on a public host, firewall the port or narrow the binding.
### 3.2 Set `X-Forwarded-For`