feat(compose): publish on every interface, so the host's own address answers #19

Merged
whitlocktech merged 1 commits from feat/publish-on-all-interfaces into main 2026-08-27 05:07:33 +00:00
Member

One decision of record, D59, amending D55. Count of record fifty-nine.

The site is deployed and healthy on the host as of #18, and it can only be reached from the host. That is D55 working as designed and being the wrong design for right now.

What D55 got wrong

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 right for the host this eventually runs on, and wrong for every step before it: a loopback binding cannot be opened from a browser on another machine. Looking at the site on the VM's own address — before DNS exists, before the proxy exists, from a desktop or a phone that is not the VM — is the first thing anyone wants to do with a fresh deploy, and it was the one thing the compose file forbade.

ports:
  - "${SITE_BIND_ADDR:-0.0.0.0}:${SITE_HOST_PORT:-4321}:4321"

http://<vm-ip>:4321 now answers out of the box, the way a normal bridge publish behaves.

Which addresses it answers on is a variable, not an edit

SITE_BIND_ADDR in .env narrows it without touching a file that docker compose pull replaces:

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

That also retires an instruction DEPLOY.md had to give: D55's advice to a proxy running in another container or on another machine was "change the port line in docker-compose.yml yourself", which is an edit to a file the next pull overwrites. It is a variable now.

What is given up, said plainly

On a host with a public address, the default means port 4321 answers from the internet directly — plain HTTP beside whatever the proxy serves on 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 nothing to steal, so this is untidy rather than dangerous. DEPLOY.md §3.1 states it and names both remedies: firewall the port, or narrow the binding.

What was run

Verified by running it, not only by reading it:

docker compose config accepted (per the YAML trap, always run before believing a compose file)
host_ip, default resolves to 0.0.0.0
host_ip, with SITE_BIND_ADDR=127.0.0.1 resolves to 127.0.0.1 — the narrowing works
Stack up through the real compose file healthy; docker compose port site 43210.0.0.0:4321
http://127.0.0.1:4321/ 200
http://<machine's LAN address>:4321/ 200, still carrying its own per-page CSP

Notes

  • This is the deploy that makes the running container reachable. The rollover happens automatically on merge; nothing to do on the host, and .env needs no new value unless you want to narrow the binding.
  • Recorded in PLAN.md as D59 under "How phase 12 delivered it", with D55's paragraph amended in place rather than rewritten — the same way D54 amended D6.

  • AI-assisted — written with Claude Code (Opus 5); commits carry Co-Authored-By: Claude.

🤖 Generated with Claude Code

One decision of record, **D59**, amending **D55**. Count of record **fifty-nine**. The site is deployed and healthy on the host as of #18, and it can only be reached from the host. That is D55 working as designed and being the wrong design for right now. ## What D55 got wrong 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 right for the host this eventually runs on, and wrong for every step before it: **a loopback binding cannot be opened from a browser on another machine.** Looking at the site on the VM's own address — before DNS exists, before the proxy exists, from a desktop or a phone that is not the VM — is the first thing anyone wants to do with a fresh deploy, and it was the one thing the compose file forbade. ```yaml ports: - "${SITE_BIND_ADDR:-0.0.0.0}:${SITE_HOST_PORT:-4321}:4321" ``` `http://<vm-ip>:4321` now answers out of the box, the way a normal bridge publish behaves. ## Which addresses it answers on is a variable, not an edit `SITE_BIND_ADDR` in `.env` narrows it without touching a file that `docker compose pull` replaces: ```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 ``` That also retires an instruction `DEPLOY.md` had to give: D55's advice to a proxy running in another container or on another machine was "change the port line in `docker-compose.yml` yourself", which is an edit to a file the next `pull` overwrites. It is a variable now. ## What is given up, said plainly On a host with a public address, the default means **port 4321 answers from the internet directly** — plain HTTP beside whatever the proxy serves on 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 nothing to steal, so this is untidy rather than dangerous. `DEPLOY.md` §3.1 states it and names both remedies: firewall the port, or narrow the binding. ## What was run Verified by running it, not only by reading it: | | | |---|---| | `docker compose config` | accepted (per [the YAML trap](https://gitea.whitlocktech.com/RunicGateway/runicgateway.com/pulls/17), always run before believing a compose file) | | `host_ip`, default | resolves to `0.0.0.0` | | `host_ip`, with `SITE_BIND_ADDR=127.0.0.1` | resolves to `127.0.0.1` — the narrowing works | | Stack up through the real compose file | healthy; `docker compose port site 4321` → `0.0.0.0:4321` | | `http://127.0.0.1:4321/` | **200** | | `http://<machine's LAN address>:4321/` | **200**, still carrying its own per-page CSP | ## Notes - **This is the deploy that makes the running container reachable.** The rollover happens automatically on merge; nothing to do on the host, and `.env` needs no new value unless you want to narrow the binding. - Recorded in `PLAN.md` as D59 under "How phase 12 delivered it", with D55's paragraph amended in place rather than rewritten — the same way D54 amended D6. --- - [x] **AI-assisted** — written with Claude Code (Opus 5); commits carry `Co-Authored-By: Claude`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
wtclaude added 1 commit 2026-08-27 04:43:19 +00:00
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
b86f4cabf0
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>
whitlocktech merged commit ce13a44ed7 into main 2026-08-27 05:07:33 +00:00
whitlocktech deleted branch feat/publish-on-all-interfaces 2026-08-27 05:07:33 +00:00
Sign in to join this conversation.
No description provided.