docs: document NPM dedicated-port jail convention and zsh $path footgun

Two real gotchas from today's goddy->gringo Laravel migration: every jail
on an NPM-fronted host needs a dedicated listen port (8000+last-octet),
not just the shared port 80, or NPM cutover silently can't reach it even
though every direct/Host-header test passes. Separately, zsh ties a
variable literally named `path` to $PATH itself - overwriting it broke
every subsequent command in a config-editing script with no clear error.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-07 19:44:23 +02:00
co-authored by Claude Sonnet 5
parent 27e3a2975f
commit 106e61cfbb
2 changed files with 59 additions and 0 deletions
@@ -130,6 +130,45 @@ curl -H 'Host: <domain>' http://<jail-ip>/
Only switch to testing via the host's `127.0.0.1` (or the public domain)
once the `sites-enabled` vhost for this specific site actually exists.
## Every jail needs a dedicated NPM-facing port, not just `listen 80`
**Confirmed real gap, 2026-08-07**: on jail hosts fronted by a shared
Nginx Proxy Manager (NPM) instance, every existing jail's host-level
nginx vhost has **two** `listen` directives — the shared port 80 (for
same-host/Host-header testing) **and a dedicated port unique to that
jail**, following the convention `8000 + <jail's last IP octet>` (e.g. a
jail at `10.20.0.43` gets `listen 8043;`, one at `.50` gets `listen
8050;`). This dedicated port is what NPM's own proxy-host config actually
targets when routing the public domain to this jail host — **NPM does
not rely on Host-header-based routing through the shared port 80 for
this fleet**, it connects to a specific port per site.
This was missed migrating 5 Laravel apps from a Linux host to a FreeBSD
jail host (gringo) in one session: all 5 new vhosts were created with
only `listen 80;`, matching the *shared* port but missing the dedicated
one entirely. Every functional test the delegate ran (raw jail IP, and
via the shared port 80 with the right `Host:` header) passed cleanly —
the gap was invisible until specifically checked against sibling jails'
configs, at which point every other jail on the host turned out to
follow the two-port pattern without exception. **Before considering any
new jail's nginx vhost complete, diff its `listen` directives against at
least one working sibling jail's config on the same host** — don't just
confirm the site loads via a manual test, confirm the *routing surface*
matches the established convention, since NPM (external, not scriptable
from this sandbox) is what will actually determine reachability once
cut over, and a working manual test doesn't prove NPM's real path works.
```bash
# find the convention on any host that uses it
for f in /usr/local/etc/nginx/sites-available/*.conf; do
echo "=== $(basename $f) ==="; grep -E '^\s*listen' "$f"
done
```
If a host doesn't show this pattern on ANY existing jail, it likely
doesn't use per-jail dedicated ports — don't assume the convention
applies fleet-wide without checking the specific host first.
## Stock stack includes Valkey, not just nginx/php/MariaDB
The fleet's standard per-host stack (see `docs/server-funky.md`) is
@@ -60,6 +60,26 @@ decode step per real filesystem boundary crossed is enough.
special characters — check for quoting corruption before looking
elsewhere.
## zsh footgun: never name a variable `path` (or a few other reserved names)
If the remote host's shell is zsh (several boxes in this fleet default to
it), assigning a plain string to a variable literally named `path` does
not create a normal local variable — zsh ties the lowercase `path` array
to `$PATH` (colon-joined ⇄ array elements) automatically. Overwriting it
with a single string **replaces `$PATH` itself**, and every subsequent
command in that shell (including ones on later lines of the same script)
fails with `command not found: cp` / `command not found: awk` / etc. —
confirmed live 2026-08-07 editing nginx configs over SSH into a zsh
remote shell, where a loop using `path="/usr/local/etc/nginx/..."` broke
every command after the assignment with no explanation beyond
`command not found`. **Rename to anything else** (`cfgpath`, `target`,
`dest`, ...) — this isn't specific to nginx configs, it'll happen with
any script that happens to pick `path` as a variable name on a zsh
remote. A few other zsh-reserved lowercase names exist for the same
reason (`status`, `pipestatus`) — when in doubt, avoid single common
English words as variable names in scripts meant to run under an
unknown/remote shell, or check `typeset -p <name>` first if unsure.
## What NOT to do
Don't respond to a quoting failure by adding more layers of escaping