Repository navigation
docs: add first-party production setup guide (#897) - #1000
chappie-daemon wants to merge 9 commits into
Conversation
🦋 Changeset detectedLatest commit: 2ae1d39 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
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 |
|
🔍 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. |
[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
|
| 2. **Configure `/etc/caddy/Caddyfile`:** | ||
| ```text | ||
| relay.example.com { | ||
| reverse_proxy 127.0.0.1:8008 | ||
| } | ||
| ``` |
There was a problem hiding this comment.
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.
|
|
||
| # Add Docker's official GPG key | ||
| sudo apt-get update | ||
| sudo apt-get install -y ca-certificates curl gnupg |
There was a problem hiding this comment.
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.
| 3. Run the automated recreate script: | ||
| ```bash | ||
| sudo ./deploy/recreate-relay.sh /opt/nostream |
There was a problem hiding this comment.
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.
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).
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.
|
Thanks — these were substantive. One commit per group of findings, both pushed: Fixed in
Fixed in
Not changed, with reasoning
One extra thing I found while checking: |
| 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 |
There was a problem hiding this comment.
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.
|
Both of these were real, and the first one was introduced by my last commit — thank you. Proxy trust placement ( Backup commands (
Still unaddressed by design: relative |
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.
|
Custom settings get discarded ( sudo docker compose -f /opt/nostream/docker-compose.yml logs nostream | grep -i 'error reading config'Silence means it parsed. Quoted values break backups ( On clients share connection limits: the full instruction is now in
|
| **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. |
There was a problem hiding this comment.
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.
|
Clients share connection limits ( 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 goneThe block now reproduces the full image default and says to append, not shorten. Empty logs hide settings errors ( 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 The other four findings you list were addressed in earlier commits: |
…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.
|
Two more real defects fixed in Duplicate settings key. Step 6 said "add an Then Proxy address matching. 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:
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 |
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.
|
Found it, and it was a real error in my text ( I had written that That inverts the advice, which is presumably why this kept coming back:
The section now spells out both, gives sudo docker compose -f /opt/nostream/docker-compose.yml logs nostream \
| grep -oE '(rate limited|unauthorized request from) [0-9a-f.:]+' | sort -uIf every client shows the same address there, trust is still not working. Both log formats exist in I could not test the container-to-container source address directly — |
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.
|
"Proxy check shows nothing" — correct, and the reason is subtle ( The line exists, but a bare The guide now says to set 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 That distinction is what the section now explains:
With |
| 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 |
There was a problem hiding this comment.
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.
| 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 |
| 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. |
There was a problem hiding this comment.
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.
| 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!
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 inREADME.mdwith 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:
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
docs/SETUP.md.env, optionalsettings.yaml,docker compose up, TLS reverse proxy, verificationREADME.md/docs/setupwebsite/site/src/data/site.tssetupDOCS entry, primary "Start here" cardwebsite/site/src/pages/get-started.astro.changeset/operator-setup-guide.mdnostreamandnostream-websiteNo source code, dependencies, lockfile, workflow or executable-bit changes.
Verification
Run on top of current
main(2819e30), not the original base:pnpm buildinwebsite/site— 15 pages,dist/docs/setup/index.htmlis 53 KB with real contentdeploy/*paths exist.envkeys matchdeploy/env.examplebootstrap.shtakes a target dir$1default/opt/nostreambootstrap.shprintseval/ 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:apt-get update404s on Debian 12, also listed as supported./etc/os-releaseis now honoured; unknown distros stop with a clear messageremoteIpHeaderunset, so behind the documented proxy every client shares one rate-limit bucket/readyzexample wrong"status":"ready"and acachekey; the handler returns"ok"andredis, and 503 when a dependency is downjq/opensslmissing:latest:latest; and withpull_policy: nevera version pull is ignored unless the compose file is edited too/opt/nostream/docker-compose.ymlis a bootstrap-time copy, sogit pullalone never delivered release-managed compose changesGreptile'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
.mdlinks 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/setuprenders, the install block passesbash -n, and bothdebian/bookwormandubuntu/noblereturn 200 fromdownload.docker.com.