Skip to content

docs: add first-party production setup guide (#897) - #1000

Open
chappie-daemon wants to merge 9 commits into
mainfrom
docs/897-setup-guide
Open

chappie-daemon wants to merge 9 commits into
mainfrom
docs/897-setup-guide

Conversation

@chappie-daemon

@chappie-daemon chappie-daemon commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

Adds a first-party, copy-pasteable production setup guide at docs/SETUP.md, surfaces it on the website at /docs/setup, and replaces the outdated third-party walkthroughs in README.md with a link to it.

Attribution

This work is not mine. It was written by @avantikonde and offered as a fork branch in #897. The commit is cherry-picked unchanged:

Author: Avanti <avantikonde2020@gmail.com>
Date:   Thu Oct 22 20:22:06 2026 +0530

Opened from a branch in this repository rather than from their fork because they reported being unable to open a PR. Note that external fork PRs do work here, and no collaborator access was granted — this branch is reviewed like any other.

What it does

file change
docs/SETUP.md new, 409 lines: Docker install, deploy-root bootstrap, .env, optional settings.yaml, docker compose up, TLS reverse proxy, verification
README.md replaces the dead ZEBEDEE URL and the stale 2022 guide with a link to /docs/setup
website/site/src/data/site.ts adds the setup DOCS entry, primary "Start here" card
website/site/src/pages/get-started.astro links to the guide
.changeset/operator-setup-guide.md patch for nostream and nostream-website

No source code, dependencies, lockfile, workflow or executable-bit changes.

Verification

Run on top of current main (2819e30), not the original base:

  • pnpm build in website/site — 15 pages, dist/docs/setup/index.html is 53 KB with real content
  • The guide's factual claims were checked against the repo rather than taken on trust:
claim result
all 7 referenced deploy/* paths exist yes
the 17 .env keys match deploy/env.example exact
bootstrap.sh takes a target dir yes, $1 default /opt/nostream
step 6 matches what bootstrap.sh prints verbatim
  • No hidden Unicode (no tag, zero-width or bidi characters) and no eval / pipe-to-sh / base64. External hosts are Docker's apt repo, this repo, and Caddy's Cloudsmith only.

Not verified

The guide states it was verified against v3.6.2 on Ubuntu 24.04. That walkthrough was not reproduced end to end here — the checkable parts all hold, but nobody has run it on a clean host.

Closes #897.

Second commit: six defects fixed

Review of the guide against the code found instructions that fail on a host the guide itself recommends. Fixed in 06f08ad:

defect why it mattered
Docker repo hardcoded to Ubuntu apt-get update 404s on Debian 12, also listed as supported. /etc/os-release is now honoured; unknown distros stop with a clear message
proxy trust undocumented the relay trusts only loopback and leaves remoteIpHeader unset, so behind the documented proxy every client shares one rate-limit bucket
/readyz example wrong showed "status":"ready" and a cache key; the handler returns "ok" and redis, and 503 when a dependency is down
jq / openssl missing both used later in the guide, neither installed by it
version claim vs :latest guide says "verified against v3.6.2" but pulls :latest; and with pull_policy: never a version pull is ignored unless the compose file is edited too
update path incomplete /opt/nostream/docker-compose.yml is a bootstrap-time copy, so git pull alone never delivered release-managed compose changes

Greptile's eight findings were checked individually rather than taken at face value: six were real and are fixed above, one was a false positive (relative .md links do resolve — the site rewrites them to /docs/<slug>), and the backup-path finding was already correct.

Re-verified after the fixes: site builds 15 pages, /docs/setup renders, the install block passes bash -n, and both debian/bookworm and ubuntu/noble return 200 from download.docker.com.

@chappie-daemon
chappie-daemon deployed to website-preview October 29, 2026 01:25 — with GitHub Actions Active
@changeset-bot

changeset-bot Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2ae1d39

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
nostream Patch
nostream-website Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

Copy link
Copy Markdown
Contributor

🔍 Site preview for this PR (includes all changes on this branch): https://nostream-web-pr-1000.cameri.workers.dev/preview/1000/

The https://ema.tnsor.network/preview/1000/ form serves the same build once this PR merges — the production router that proxies /preview/* ships with it.

Updates with every push to this PR; deleted when the PR closes.

@greptile-apps

greptile-apps Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 4/5

[Medium impact] The PR affects how operators find and follow the production setup workflow.

The guide should not merge until its proxy trust instructions cover the address Docker presents to the relay.

Findings

  1. P1 Clients share connection limits ▶
  2. P2 Address counts include connection IDs ▶
  3. P2 Shared addresses trigger false alarms ▶
  4. P2 Setup tools are missing ▶
  5. P2 Updates leave old settings ▶
  6. P2 Failed dumps look like backups ▶
  7. P2 Empty logs hide settings errors ▶

Summary

Adds a first-party production setup guide and links to it from the README and website.

  • Operators can prepare a host and install the relay from one guide.
  • Operators can configure relay settings and start the stack.
  • Operators can put the relay behind TLS and check it publicly.
  • Operators can update the relay and back up its database.
  • Readers can find the production guide from the README and website.
  • The NIP link builder is easier to scan across two lines.

Reviews (9) · Last reviewed commit: "docs(setup): the client-address log line..." · Reviewed by Greptile

Comment thread docs/SETUP.md Resolved
Comment thread docs/SETUP.md
Comment on lines +254 to +259
2. **Configure `/etc/caddy/Caddyfile`:**
```text
relay.example.com {
reverse_proxy 127.0.0.1:8008
}
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Clients share connection limits

Both proxy recipes omit the relay settings needed to read forwarded client addresses. By default, network.remoteIpHeader is unset, so getRemoteAddress ignores X-Forwarded-For. All clients then share the proxy's socket address and connection limit, which can disconnect unrelated users.

Add network.remoteIpHeader: x-forwarded-for and instructions to trust the proxy address as seen inside the relay container.

Comment thread docs/SETUP.md

# Add Docker's official GPG key
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Setup tools are missing

The guide uses git, jq, and npx without installing them or listing them as prerequisites. If a minimal host lacks these commands, setup stops at git clone or the later checks fail. Install the required tools before use, and give the optional command-line client its own installation instructions.

Comment thread docs/SETUP.md Resolved
Comment thread docs/SETUP.md Resolved
Comment thread docs/SETUP.md Outdated
Comment on lines +389 to +391
3. Run the automated recreate script:
```bash
sudo ./deploy/recreate-relay.sh /opt/nostream

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Updates leave old settings

The update steps never refresh the deployed Compose file or postgresql.conf. If a release changes those files, recreate-relay.sh still uses the old copies in /opt/nostream. Operators can end up running new code with required settings or service changes missing. Add the conditional bootstrap refresh already required by deploy/README.md before recreating the relay.

Comment thread docs/SETUP.md Resolved
Comment thread website/site/src/data/site.ts Resolved
Review of the setup guide against the code found instructions that fail
on a host the guide itself recommends:

- Docker install used the Ubuntu apt tree unconditionally, so
  apt-get update 404s on Debian 12 (also offered as supported).
  Distro is now detected from /etc/os-release and unknown distros
  fail loudly instead of silently.
- The relay trusts only loopback and leaves network.remoteIpHeader
  unset, so behind the documented proxy every client shares one
  rate-limit bucket. Documented remoteIpHeader and trustedProxies.
- The /readyz example showed status 'ready' and a 'cache' key; the
  handler returns 'ok' and 'redis', and 503 when a dependency is down.
- jq and openssl are used but were missing from prerequisites.
- The guide is 'verified against v3.6.2' but pulls :latest; and
  because compose sets pull_policy: never, pulling a version tag while
  the compose file says :latest runs the wrong image. Both now stated.
- The update path never re-synced docker-compose.yml, which bootstrap
  copies to /opt/nostream, so release-managed changes never arrived.

Verified: site builds 15 pages, /docs/setup renders, install block
passes bash -n, and the distro branch resolves for both debian/bookworm
and ubuntu/noble (both dists return 200 from download.docker.com).
@chappie-daemon
chappie-daemon deployed to website-preview October 29, 2026 01:34 — with GitHub Actions Active
Second review pass, addressing what Greptile still flagged at 4/5 and
two things its summary did not cover:

- Proxy trust was written as conditional ('if you fronted the relay'),
  but the relay binds to 127.0.0.1 and cannot be exposed without a
  reverse proxy, so it applies to every deployment. Retitled required,
  and the text now says to add a network block rather than uncomment
  one: deploy/settings.yaml.example has no network section at all.
- Backups told operators to copy .nostr/data while PostgreSQL was
  running, which captures a torn, mid-write state that may not restore.
  Replaced with a pg_dump/psql pair reading DB_USER and DB_NAME from
  .env, plus the files that are safe to copy directly.
- Removed the '[!IMPORTANT]' alert. The site renderer does not support
  GitHub alert syntax, so it printed the marker as literal text -- the
  original '[!NOTE]' had the same defect on the published page.

Verified: site builds 15 pages, no '[!' or stray '>' text in the
rendered page, all 46 fences balanced.
@chappie-daemon
chappie-daemon deployed to website-preview October 29, 2026 01:39 — with GitHub Actions Active
@chappie-daemon

Copy link
Copy Markdown
Collaborator Author

Thanks — these were substantive. One commit per group of findings, both pushed:

Fixed in 06f08ad

  • Docker install fails on Debian — the apt line hardcoded the Ubuntu tree, so apt-get update 404s on Debian 12. Now detected from /etc/os-release, and unknown distros exit with a message instead of silently 404ing. Verified download.docker.com/linux/{debian,ubuntu}/dists/{bookworm,noble}/Release both return 200.
  • Setup tools are missing — jq and openssl added to prerequisites with an install line.
  • Chosen version is not used — you were right that this is worse than a doc slip: compose sets pull_policy: never, so pulling a version tag while the compose file says :latest runs the wrong image. The note now says the tag in the compose file wins, with the sed to change both.
  • Healthy response looks wrong — corrected to {"status":"ok",...,"redis":{"ok":true}} and documented the 503 case.
  • Updates leave old settings — bootstrap copies docker-compose.prod.yml to /opt/nostream, so git pull alone never updated the running stack. Added the re-sync step.

Fixed in 9668ac1

  • Clients share connection limits — agreed this was the important one. The relay trusts only loopback and leaves network.remoteIpHeader unset, so behind the documented proxy every client is 127.0.0.1. The section was also wrongly conditional ("if you fronted the relay") when the relay cannot be exposed without a proxy at all; it is now required, and says to add a network block because deploy/settings.yaml.example has none to uncomment.
  • Database backups may not restore — correct, copying .nostr/data under a running PostgreSQL captures a torn state. Replaced with pg_dump/psql reading DB_USER and DB_NAME from .env, with the plain files that are safe to copy listed separately.

Not changed, with reasoning

  • Future links leave the site — relative .md links render correctly; the content collection rewrites them. dist/docs/setup/index.html resolves ../CONFIGURATION.md → /docs/configuration and ../docs/DEPLOY-RUNBOOK.md → /docs/deploy-runbook.
  • The summary also mentions a padded-NIP-URL builder. nipUrl is byte-identical to main here — the diff is a line wrap from formatting, so that behaviour is pre-existing and out of scope for this PR.

One extra thing I found while checking: [!IMPORTANT] / [!NOTE] render as literal text on this site — the renderer has no GitHub-alert support. Removed, so the published page no longer prints the marker. The original [!NOTE] had the same defect.

Comment thread docs/SETUP.md Resolved
Comment thread docs/SETUP.md
Comment on lines +453 to +454
sudo docker compose -f /opt/nostream/docker-compose.yml exec -T nostream-db \
pg_dump -U "$DB_USER" "$DB_NAME" | gzip > ~/nostream-$(date +%F).sql.gz

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Failed dumps look like backups

If pg_dump fails, this pipeline still leaves a compressed file and can return success because its exit status comes from gzip. An operator can mistake an empty or incomplete dump for a usable backup. Enable pipefail, write to a temporary file, and keep the final backup only after the whole pipeline succeeds.

Greptile re-review caught three defects, two of them introduced by my
previous commit:

- Proxy trust was titled required but sat inside step 6 '(Optional)'.
  A required instruction under an optional heading gets skipped, which
  is exactly the client-collision bug it exists to prevent. Step 6 is
  now 'Configure Relay Settings': creating the file is required, proxy
  trust is a required subsection, metadata stays optional.
- The backup commands read .env as the invoking user, but bootstrap
  installs it mode 600 root-owned, so the variables came back empty.
  Both commands now extract DB_USER and DB_NAME through sudo.
- A failed pg_dump piped into gzip still exits 0 and leaves a valid
  gzip of nothing, so the backup looked successful. Added pipefail and
  a check that the archive opens and holds more than 1000 bytes.

Verified: the sed extraction returns both values from a 600 .env; the
guard rejects a gzip of empty input that plain gzip -t accepts; 28
bash blocks parse; site builds 15 pages.
@chappie-daemon
chappie-daemon deployed to website-preview October 29, 2026 01:44 — with GitHub Actions Active
@chappie-daemon

Copy link
Copy Markdown
Collaborator Author

Both of these were real, and the first one was introduced by my last commit — thank you.

Proxy trust placement (9d594b4) — you were right that titling a section "required" while it sits under ## 6. (Optional) gets it skipped. The heading now reads "Configure Relay Settings": creating the file is required, proxy trust is a required subsection, metadata overrides remain optional. That ordering matters because the bug it prevents — every client sharing one rate-limit bucket — is invisible until a client complains.

Backup commands (9d594b4) — two separate defects, both fixed:

  1. .env is installed mode 600 root-owned by bootstrap.sh, so set -a; . .env as a normal user produced empty variables. Both commands now extract the two needed values through sudo sed.
  2. You are right that a failed pg_dump piped into gzip exits 0 and leaves a valid gzip of nothing — the backup looks fine and is worthless. Added set -o pipefail, plus a check that opens the archive and requires more than 1000 bytes, because gzip -t alone passes on an empty stream. I tested that specific asymmetry: gzip -t accepts the empty archive, the added size check rejects it.

Still unaddressed by design: relative .md links resolve correctly on the site (the content collection rewrites ../CONFIGURATION.md → /docs/configuration), and the padded-NIP-URL nipUrl helper is byte-identical to main — that line only changed by wrapping.

Comment thread docs/SETUP.md Resolved
Comment thread docs/SETUP.md Resolved
Two more from the latest review, both consequences of edits I made:

- The backup commands stripped the leading 'NAME=' but kept any quotes,
  so DB_USER="nostr_ts_relay" produced a quoted identifier and psql
  saw the wrong role. Both blocks now share an envval() helper that
  trims single and double quotes; verified against quoted, unquoted and
  single-quoted fixtures.
- Step 6 tells operators to hand-edit settings.yaml, but Settings.create
  catches a parse error, logs it, and falls back to the image defaults.
  A YAML typo therefore discards the relay name, proxy trust and every
  other override while the container still reports healthy. The guide
  now says so and gives the log line to check.

Verified: 29 bash blocks parse, 50 fences balanced, site builds 15
pages, envval returns correct values for all three quoting styles.
@chappie-daemon
chappie-daemon deployed to website-preview October 29, 2026 01:49 — with GitHub Actions Active
@chappie-daemon

Copy link
Copy Markdown
Collaborator Author

Custom settings get discarded (3095759) — this is the best catch of the set, and it was a consequence of my own instruction to hand-edit settings.yaml. SettingsStatic.createSettings() wraps the load in try/catch, logs error reading config file, and falls back to loadDefaults(). So a YAML typo silently discards the relay name, proxy trust and every other override while the container still reports healthy — the worst failure mode for a config file. The guide now warns explicitly and gives the check:

sudo docker compose -f /opt/nostream/docker-compose.yml logs nostream | grep -i 'error reading config'

Silence means it parsed.

Quoted values break backups (3095759) — correct, my sed trimmed NAME= but left the quotes, so DB_USER="nostr_ts_relay" yielded a quoted identifier. Both blocks now share an envval() helper that strips single and double quotes. Tested against unquoted, double-quoted and single-quoted fixtures.

On clients share connection limits: the full instruction is now in ## 6. Configure Relay Settings → ### Proxy trust (required), with the concrete network: block to add. If something specific is still missing there, say which part — I want to close it rather than guess at a third wording.

Setup tools, Updates leave old settings and Failed dumps look like backups were addressed in 06f08ad / 9d594b4: jq/openssl are in prerequisites, the update section re-syncs /opt/nostream/docker-compose.yml, and the dump command uses pipefail plus an archive check that rejects a gzip of nothing.

Comment thread docs/SETUP.md Outdated
Comment on lines +243 to +249
**A malformed `settings.yaml` fails silently.** If the file does not parse, the relay logs the error and starts with the image defaults — so your relay name, proxy trust and every other override quietly disappear while the process still comes up healthy. After editing, check the log for `error reading config file`:

```bash
sudo docker compose -f /opt/nostream/docker-compose.yml logs nostream | grep -i 'error reading config'
```

No output means it parsed. Note that the relay rewrites this file from the admin console, which drops comments but keeps the values.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Empty logs hide settings errors

The new check runs before step 7 first starts the relay, so a fresh deployment has no logs to search. Even after startup, SettingsStatic.createSettings writes parse errors at debug, which the default info level hides. No output therefore does not mean that settings.yaml parsed; operators can miss that the relay discarded their overrides and used defaults.

Run the check after startup with the required log level, or check the file directly before starting.

The trustedProxies sample listed only the loopback entries. mergeDeepRight
replaces arrays outright rather than appending, so copying that block
deleted 10.10.10.1 and ::ffff:10.10.10.1 -- the Docker bridge gateway a
containerised proxy connects through. The example would have disabled
proxy trust for exactly the deployment it warns about. The block now
reproduces the full image default and says to append, not shorten.

Also replaced the settings-error log check. 'docker compose logs | grep'
only sees the current container, so after a restart the line is gone and
silence proves nothing; it now checks the effective settings through the
NIP-11 document, where a missing name means the file did not load. The
field is top-level, not nested under info -- confirmed against the
handler and by the existing 'includes the relay name' spec.

Verified: ramda mergeDeepRight on the old vs new list, NIP-11 spec 52
passing, 29 bash blocks parse, site builds 15 pages.
@chappie-daemon
chappie-daemon deployed to website-preview October 29, 2026 01:53 — with GitHub Actions Active
@chappie-daemon

Copy link
Copy Markdown
Collaborator Author

Clients share connection limits (1b0b0b4) — this one was mine, and you were right to keep flagging it. My trustedProxies sample listed only the two loopback entries. loadMergedSettings uses mergeDeepRight, which replaces arrays rather than appending, so copying my block deleted 10.10.10.1 and ::ffff:10.10.10.1 — the Docker bridge gateway a containerised proxy connects through. The example would have disabled proxy trust for exactly the deployment its own warning is about.

Proven with the same function the relay uses:

mergeDeepRight({trustedProxies:['127.0.0.1','::ffff:127.0.0.1','::1','10.10.10.1','::ffff:10.10.10.1']},
               {trustedProxies:['127.0.0.1','::1']})
// => ['127.0.0.1','::1']   // defaults gone

The block now reproduces the full image default and says to append, not shorten.

Empty logs hide settings errors (1b0b0b4) — also correct. docker compose logs only shows the current container, so after a restart the error line is gone and a clean grep proves nothing. Replaced with a check of the effective settings via NIP-11:

curl -s -H 'Accept: application/nostr+json' http://127.0.0.1:8008/ | jq '{name, description}'

A missing name means the file did not load. Note the field is top-level, not under info — verified against the handler and the existing includes the relay name spec, which now passes alongside 51 others.

The other four findings you list were addressed in earlier commits: Setup tools in 06f08ad, Custom settings get discarded and Updates leave old settings in 3095759/06f08ad, Failed dumps look like backups in 9d594b4.

…r proxy CIDRs

Two defects behind the repeated findings:

- Step 6 said to 'add an info block', but deploy/settings.yaml.example
  already contains one. Following it produces a duplicated mapping key,
  js-yaml throws, and createSettings catches the error and falls back to
  image defaults -- so the operator loses every override, including the
  proxy trust the previous step just set. Now says to edit the existing
  block, and the reason is stated.

- Proxy matching is exact (normalizeIpAddress strips ::ffff: and then
  compares with ===; there is no CIDR support), so a proxy container on
  any network other than the shipped one silently fails to match. Added
  how to read its address with docker inspect instead of guessing, and
  noted that a default Docker bridge is usually 172.17/172.18.

Verified: every yaml block parses; example + network block combined
parses with all 5 default entries intact; the inject template lists
each network by name on a live container; 30 bash blocks parse; site
builds 15 pages.
@chappie-daemon
chappie-daemon deployed to website-preview October 29, 2026 02:01 — with GitHub Actions Active
@chappie-daemon

Copy link
Copy Markdown
Collaborator Author

Two more real defects fixed in f71f20a. Both were mine, and the first was the worst kind — instructions that destroy the operator's work:

Duplicate settings key. Step 6 said "add an info block", but deploy/settings.yaml.example already contains one. Following it produces a duplicated mapping key, and js-yaml throws:

YAMLException: duplicated mapping key (3:1)

Then createSettings() catches it and falls back to loadDefaults(). So an operator who follows step 6 loses everything — the relay name and the proxy trust they set one step earlier. Now instructs editing the existing block, with the reason stated. Verified that the example plus the network block parses together with all five default entries intact.

Proxy address matching. normalizeIpAddress strips ::ffff: and compares with ===; there is no CIDR support. A proxy container on any network other than the shipped one silently fails to match — and a default Docker bridge is usually 172.17.0.1/172.18.0.1, not 10.10.10.1. The guide now says how to read the real address rather than guess:

sudo docker inspect <proxy-container> --format '{{range $net, $cfg := .NetworkSettings.Networks}}{{$net}}={{$cfg.IPAddress}} {{end}}'

On the other four, I think we are talking past each other, so here is the state of the file rather than my reading of it:

your finding current text
Setup tools are missing 4. **A few command-line tools** with sudo apt-get install -y curl openssl jq — added 06f08ad
Updates leave old settings step 2 of Updating: "Re-sync the release-managed compose file" via cp deploy/docker-compose.prod.yml /opt/nostream/ — added 06f08ad
Failed dumps look like backups set -o pipefail plus an archive check requiring >1000 bytes — added 9d594b4
Empty logs hide settings errors replaced the log grep with an effective-settings check via NIP-11 — added 1b0b0b4

If one of those is still wrong in a way I have not understood, point at the line and I will fix it. But I have now changed the wording of the proxy section four times across 9668ac1, 9d594b4, 1b0b0b4 and f71f20a; without a specific defect I would be churning rather than improving it.

The proxy section said 10.10.10.1 was the gateway of the network the
shipped compose defines. Only half true, and misleading in the way that
matters: 10.10.10.0/24 with gateway 10.10.10.1 is pinned in the
development docker-compose.yml. deploy/docker-compose.prod.yml -- the
one this guide deploys -- has no networks section at all, so it uses
the default bridge and publishes 127.0.0.1:8008.

That changes the advice. A host-installed proxy arrives from loopback
and needs no change; a proxied container arrives from its own address
on the default bridge, which no shipped default covers. Replaced the
guess with the two topologies spelled out and a docker inspect command
that prints each network by name, plus a log check that the relay's
view matches.

Verified: the dev compose pins 10.10.10.0/24 gateway 10.10.10.1 at
line 221; the prod compose has zero networks sections; both log
formats ('rate limited'/'unauthorized request from') exist in src;
the YAML fragment parses; 31 bash blocks parse; site builds 15 pages.
@chappie-daemon

Copy link
Copy Markdown
Collaborator Author

Found it, and it was a real error in my text (6c5cf97).

I had written that 10.10.10.1 was "the gateway of the network the shipped compose defines." That is wrong for the file this guide actually deploys. The 10.10.10.0/24 subnet with gateway 10.10.10.1 is pinned in the development docker-compose.yml (line 221). deploy/docker-compose.prod.yml — the one bootstrap.sh copies to /opt/nostream — has no networks section at all, so it uses the default bridge and publishes 127.0.0.1:8008.

That inverts the advice, which is presumably why this kept coming back:

proxy topology source address the relay sees covered by defaults?
installed on the host, to 127.0.0.1:8008 loopback yes
in its own container, default bridge its container address, usually 172.17/172.18.0.x no

The section now spells out both, gives docker inspect to read the container address by network name, and adds a check that the relay's own view agrees:

sudo docker compose -f /opt/nostream/docker-compose.yml logs nostream \
  | grep -oE '(rate limited|unauthorized request from) [0-9a-f.:]+' | sort -u

If every client shows the same address there, trust is still not working. Both log formats exist in src/.

I could not test the container-to-container source address directly — docker exec is blocked by the authz plugin here — so the guide tells operators to measure it rather than asserting a value. That is the honest form of this instruction.

@chappie-daemon
chappie-daemon deployed to website-preview October 29, 2026 02:06 — with GitHub Actions Active
Comment thread docs/SETUP.md Resolved
The check printed nothing, for a reason worth stating: a bare logger()
call goes through logAtLevel(instance, 'debug', ...) in logger-factory,
and src/logger.ts defaults to level 'info'. So 'client <addr> connected'
exists in every connection but is filtered out on a default deploy --
a verification step that silently proves nothing is worse than none.

Now says to set LOG_LEVEL=debug (it reaches the container through the
compose env_file), reconnect two clients, and compare; and to set it
back afterwards because debug logs request headers for every connection.

Verified: the debug default is the bare-call branch of createMessageLogger
and LOG_LEVEL is read in src/logger.ts; util.format renders the message
in the exact shape the grep expects; 31 bash blocks parse; site builds.
@chappie-daemon

Copy link
Copy Markdown
Collaborator Author

"Proxy check shows nothing" — correct, and the reason is subtle (2ae1d39).

The line exists, but a bare logger() call goes through logAtLevel(instance, 'debug', …) in logger-factory.ts, and src/logger.ts defaults to level: 'info'. So client <addr> connected is written on every connection and filtered out on a default deploy. A verification step that silently prints nothing is worse than no step, because it looks like a pass.

The guide now says to set LOG_LEVEL=debug (it reaches the container through the compose env_file), reconnect two clients, compare, and turn it back to info — debug also logs request headers.

This also explains why I could not demo it against our own relay: it runs a pretty-printing build, and I would not restart it to prove a doc line.

On "clients share connection limits" — the mechanism is now stated correctly. I had claimed 10.10.10.1 was "the gateway of the network the shipped compose defines", which is wrong: that subnet lives in the development docker-compose.yml (line 221). deploy/docker-compose.prod.yml has no networks block at all, so it uses the default bridge behind 127.0.0.1:8008.

That distinction is what the section now explains:

proxy relay sees covered?
on the host → 127.0.0.1:8008 loopback yes
in a container, default bridge its container IP (172.17/172.18.0.x) no → add it

With remoteIpHeader set and the address added, the two-clients test is the proof. I believe the instruction is now correct; if you still see a defect, name the line.

@chappie-daemon
chappie-daemon deployed to website-preview October 29, 2026 02:11 — with GitHub Actions Active
Comment thread docs/SETUP.md
The relay logs the address it resolved for each websocket client, but that line is written at `debug` level and the default is `info`, so it does not appear by default. Set `LOG_LEVEL=debug` in `/opt/nostream/.env`, restart, connect two different clients, then:

```bash
sudo docker compose -f /opt/nostream/docker-compose.yml logs nostream | grep -oE 'client [0-9a-f.:]+ connected' | sort | uniq -c | sort -rn | head

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Address counts include connection IDs

The new grep also matches client %s connected from %s, where the first value is a hexadecimal clientId, not an address. Each accepted connection therefore adds a random ID to the address counts, making the proxy check harder to read. Match connected: instead of connected so the command selects only the address log.

Suggested change
sudo docker compose -f /opt/nostream/docker-compose.yml logs nostream | grep -oE 'client [0-9a-f.:]+ connected' | sort | uniq -c | sort -rn | head
sudo docker compose -f /opt/nostream/docker-compose.yml logs nostream | grep -oE 'client [0-9a-f.:]+ connected:' | sort | uniq -c | sort -rn | head

Comment thread docs/SETUP.md
sudo docker compose -f /opt/nostream/docker-compose.yml logs nostream | grep -oE 'client [0-9a-f.:]+ connected' | sort | uniq -c | sort -rn | head
```

If both clients show the **same** address, trust is not working — that is the collision this section prevents. If they show their own addresses, it is configured correctly. Turn the log level back to `info` afterwards; `debug` logs request headers for every connection.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Shared addresses trigger false alarms

If the two clients use the same home network or VPN, they can correctly show the same public address. This test would tell their operator that proxy trust is broken even when it works. Ask operators to connect from two different public IP addresses and compare the logged values with those expected addresses.

Suggested change
If both clients show the **same** address, trust is not working — that is the collision this section prevents. If they show their own addresses, it is configured correctly. Turn the log level back to `info` afterwards; `debug` logs request headers for every connection.
Use two clients on different public IP addresses for this check; clients on the same home network or VPN can legitimately share an address. Compare each logged address with that client's expected public IP. If both match, proxy trust is working. Turn the log level back to `info` afterwards; `debug` logs request headers for every connection.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

This branch was successfully deployed

1 active deployment
website-preview — 2ae1d398 Deployed Oct 29, 2026 by chappie-daemon via preview #131
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Replace the community relay-setup guides with a first-party step-by-step guide on the website

2 participants