Single-server self-hosting: instance-config refactor, selfhost deployment profile, fresh-install fixes - #2
Single-server self-hosting: instance-config refactor, selfhost deployment profile, fresh-install fixes#2flotob wants to merge 12 commits into
Conversation
- docker/.env: default MEDIASOUP_DISABLE_LIBURING=true — io_uring is blocked under recent kernels/Docker seccomp, crashing all mediasoup workers on startup (and compose does not restart mediasoup) - docker/hardhat: pin ts-node/typescript — unpinned ranges with no lockfile resolve to an incompatible pair, crash-looping the hardhat container (ts-node readConfig 'fileExists' TypeError) - contracts/scripts/deploy.ts: TokenSale dev-chain end timestamp was hardcoded to 2025-12-30 (now in the past), making the constructor revert OnlyFutureEndTimestampAllowed; use now+10y on cgstack Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The app previously derived its deployment mode and all URLs from a hardcoded domain list (app.cg / staging.app.cg) — any other domain silently ran with dev semantics, and several prod code paths were outright broken on other domains (session cookie pinned to .app.cg, WebAuthn rpID pinned to id.app.cg, sitemap URLs). Self-hosted instances now declare their identity at serve time via an injected window.__CG_INSTANCE__ script (deployment mode, app URL, CG ID URL, reCAPTCHA site key): - src/common/instance.ts: typed accessor for the injected config - src/common/config.ts, src/data/util/urls.ts: instance config takes precedence; legacy domain detection unchanged as fallback - srv/util/instanceConfig.ts + srv/api/getRoutes.ts: the API server injects the config into the index.html it serves for share links; the selfhost nginx image does the same for static serving - srv/util/express.ts: session cookie domain derived from BASE_URL (.app.cg / .staging.app.cg unchanged) - srv/api/cgid.ts: WebAuthn rpID/origin derived from CGID_URL so passkeys work on any domain - srv/repositories/communities.ts: sitemap uses the instance URL - srv/api/emails.ts: EMAIL_FROM env overrides the default sender - reCAPTCHA is now optional: with no site/secret key configured, signup and trust verification run without captcha (frontend hides the widget, backend skips verification with a warning) app.cg, staging.app.cg and local dev behave exactly as before when no instance config is injected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New production deployment path for running the whole stack on one
machine (16 GB / 4-8 cores is enough — the stack idles well under 2 GB):
- docker/docker-compose.selfhost.yml: prod-semantics compose profile.
Caddy terminates TLS (automatic Let's Encrypt) for the app, the CG ID
subdomain and mediasoup signalling (wss :4443, internal self-signed
hop). No hardhat/test-contract services, no orphan redis-blockscout,
postgres actually loads the tuned postgresql.conf, redis capped at
512mb each (was 2GB), seaweedfs volume limit raised to 1GB, mediasoup
restarts automatically.
- docker/nginx/{Dockerfile_selfhost,nginx_selfhost.conf}: nginx image
parameterized for arbitrary domains, with a CSP equivalent to prod
and instance-config injection into the served index.html at container
start (inject-instance-config.sh).
- docker/selfhost/init.sh: one-shot instance initialization — generates
fresh secrets (.env.selfhost), matching S3 credentials and the
internal mediasoup cert; never overwrites existing files.
- docker/selfhost/selfhost.sh: build/up/down/logs/stats/update wrapper.
- docker/SELFHOST.md: setup guide, optional-integration matrix
(SendGrid, reCAPTCHA, RPC keys, Twitter, SumSub — all optional),
resource tuning and backup notes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- docker-compose.yml: pass -c config_file so postgres actually loads the mounted postgresql.conf (it was silently ignored — none of its tuning, including max_connections=200, ever applied) - updateFrontend.sh: the craco build ran without NODE_OPTIONS and hit Node's default ~2GB heap limit — webpack needs ~3GB (measured), so the script always died with heap OOM; give it 4GB like build.sh - comment out redis-blockscout: its only consumer (blockscout) is commented out, and it ran unpassworded with a 2GB cap - drop the obsolete compose `version` attribute (warning spam) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Prevents container/volume collisions with the dev stack when both profiles are used from the same checkout. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- communities.ts: creating a community silently rolled back entirely on
fresh instances — the welcome-article template (a hardcoded prod
article UUID) doesn't exist there, and its failed INSERT aborted the
whole creation transaction despite being caught. A savepoint now
isolates the optional article copy. This also protects app.cg: if the
template article is ever deleted, community creation would break the
same way.
- index.tsx: don't load CG's matomo analytics on self-hosted instances
(the CSP blocked it anyway; now it isn't attempted)
- nginx_selfhost.conf:
- allow WebAssembly compilation (script-src 'wasm-unsafe-eval') —
web3 dependencies compile WASM at startup
- serve the SPA shell for unknown paths so deep links work before
the service worker controls navigation
- proxy /api/ws/ to wsapi (websocket upgrade headers)
- manifest-src 'self' for the CG ID web-app manifest
Verified on cg.mogged.eu: signup (captcha-less), login, community
creation, socket.io, and the full WebAuthn passkey ceremony
(rp.id id.cg.mogged.eu, verified=true, cross-origin session shared).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Self-hosted instances had all onchain features broken: the frontend's Alchemy key is domain-locked to app.cg (every request fails CORS — this also left Spark purchases stuck on "confirm ownership"), and the backend's QUIKNODE_* endpoints were placeholders. Worse, the onchain service crash-looped at startup: the premium-token bootstrap in startEventListener runs in an unhandled async IIFE, and with dead RPC endpoints its NOT_SUPPORTED throw killed the process on every boot. - onchain robustness: the premium-token bootstrap and the event-listener setup can no longer crash the process; failures log and degrade (premium payments unavailable) instead - public RPC defaults: init.sh prefills the QUIKNODE_*/INFURA_* vars (they take any JSON-RPC URL — the names are historical) with verified free public endpoints, so token gating, balances and premium payments work out of the box; operators swap in paid endpoints for scale - CG_ACTIVE_CHAINS: one knob for which chains an instance offers. Backend services read it from env, the frontend receives the resolved list via the instance config — chain workers and every chain picker in the UI (token gating, wallets) stay consistent - frontend: self-hosted instances skip the Alchemy provider and use each chain's default public RPC (wagmi publicProvider) - selfhost CSP: allow the default public RPC hosts Verified on cg.mogged.eu: onchain runs stably and fetched the premium token contract data (USDT/DAI/USDC on eth + xdai) via public endpoints. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
nginx resolved the api/wsapi container IPs once at startup, so every backend restart (updates, crashes) left it proxying to dead IPs — 502s and "Disconnected" toasts for everyone until nginx itself restarted. Use docker's DNS with a short TTL and variable proxy_pass targets (the set must come before the rewrite ... break, which ends rewrite-phase processing) so backend containers can restart freely. Verified: api and websocket routes keep working across an api container restart with no nginx restart. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Two more commits from continued live testing on cg.mogged.eu:
🤖 Generated with Claude Code |
The server now derives capability flags from which secrets it actually has (features: email/twitterAuth/kyc, plus per-instance giphyApiKey and walletConnectProjectId) and ships them in the instance config; the UI hides or honestly labels what the instance can't do. Official instances are unchanged — every flag defaults to enabled without an instance config. Email (SendGrid) unconfigured: - OTP login option hidden; endpoint returns EMAIL_DISABLED instead of pretending a code was sent (password/passkey/wallet login unaffected) - verification modals never open; email-change skips the "check your inbox" promise; event RSVP no longer requires unverifiable email - newsletter UI (settings toggles, community management) hidden or labeled unavailable - email-notification/newsletter jobs skip cleanly instead of process.exit(1) Robustness fixes that also protect app.cg: - event RSVP/update/delete endpoints no longer fail after the primary operation succeeded just because the notification email failed - SumSub webhook ack survives email failures - sendEmail error handler no longer crashes on errors without .response Twitter/X keys unconfigured: X login/link buttons hidden everywhere (previously a 500 in a popup). SumSub unconfigured: KYC steps show "not available on this instance" (previously an infinite spinner). Mailchimp unconfigured: newsletter preference stored locally instead of throwing UNKNOWN. Giphy: self-hosted instances no longer piggyback on CG's shared key — picker hidden unless the operator sets CG_GIPHY_API_KEY. WalletConnect: project id per-instance (CG's is origin-allowlisted). Verified on cg.mogged.eu: flags derived and injected correctly, X buttons gone, zero console errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The server now derives capability flags from which secrets it actually has ( Highlights:
Verified live on cg.mogged.eu: flags derived/injected correctly, X buttons gone from onboarding, zero console errors. 🤖 Generated with Claude Code |
All *.publicnode.com endpoints started rejecting unfiltered eth_getLogs (code -32701 "Please specify an address in your request") in July 2026, which broke the onchain event listener on every chain that used them — errors and cancelled tasks in a retry loop, no event processing. Replace them with endpoints verified against the listener's actual access pattern (unfiltered getLogs over each chain's BLOCK_BATCHSIZE, up to 40 blocks on arbitrum/nova): - eth/matic/avax/optimism/linea/celo -> drpc.org (allows ranges <=10; all these chains batch <=8) - arbitrum/arbitrum_nova -> official arb1/nova.arbitrum.io (40-block batches; drpc caps at 10) - bsc -> bsc.blockrazor.xyz (dataseed limits getLogs) - xdai/base/fantom/zkevm/scroll/zksync unchanged (still work) Documented the getLogs requirement in init.sh and SELFHOST.md so operators test candidate endpoints before swapping them in. Verified on cg.mogged.eu: all six active chains process events cleanly (eth handled 7135 watched-contract events on first sync, no errors over several minutes of runtime). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The full self-hoster journey from One regression surfaced since yesterday's validation, fixed here: every
After the swap, all six active chains sync cleanly on cg.mogged.eu (eth processed 7135 watched-contract events on first sync, zero errors since). Free public endpoints changing their policies under our feet is exactly the operational risk the optional-integrations matrix predicted — worth keeping the "test with an unfiltered eth_getLogs call" note in mind whenever a chain goes quiet. 🤖 Generated with Claude Code |
The backend image's HEALTHCHECK reads healthcheck.txt, but the jobs entrypoint was the only backend service that never wrote it — the job-runner container reported permanently unhealthy in every deployment (invisible in dev, where nothing gates on container health). Call fakeHealthcheck() like api/wsapi/onchain/memberlist do. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Scheduled calls (every event call) are created with callServerId NULL —
a call server is only assigned when the call starts. Updating such a
call with changed slots/stageSlots/highQuality/audioOnly (e.g. editing
an event and switching Broadcast <-> Group call, which changes
stageSlots) made the notify_call_change() trigger build its notify
channel from the NULL callServerId:
'callservercallupdate_' || NULL -> NULL
pg_notify(NULL, ...) -> ERROR: channel name cannot be empty
aborting the whole UPDATE, so saving the event failed. This also
explains why the failure looked intermittent: it only triggers when one
of those four columns actually changes.
The migration adds NEW."callServerId" IS NOT NULL to the condition —
without an assigned call server there is no listener on that channel,
and the server receives the full call state when it is assigned at
call start.
Reproduced and verified on a live instance: the failing UPDATE now
succeeds with callServerId NULL, and the notification still fires with
the correct payload when a call server is assigned.
Fixes #1
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Goal
Make the complete Common Ground stack runnable on a single machine — 32 GB for a full production workload, 16 GB / 4–8 cores for a fresh instance — and make it easy for anyone to run CG locally or host their own instance.
Everything in this PR was validated on a real deployment: a fresh 8-core / 15 GiB Hetzner VPS now serves a fully working instance at
https://cg.mogged.eu(built from this branch). Measured results below.What's in here
1. Fresh-install fixes (
0b6e48e)Three things any new self-hoster hits on a clean machine:
io_uring_queue_init(): Operation not permittedunder Docker's seccomp.MEDIASOUP_DISABLE_LIBURING=trueis now the dev default (and compose gives mediasoup no restart policy, so this failure was silent and permanent).docker/hardhat/node/package.jsonhad unpinnedts-node/typescriptranges and no lockfile; a fresh image build resolves an incompatible pair (readConfig 'fileExists'TypeError). Pinned.deploy.tshardcoded an end timestamp of 2025-12-30, which is now in the past (OnlyFutureEndTimestampAllowed). Uses now+10y on the dev chain.2. Instance identity decoupled from deployment mode (
51e1d69)Previously the app derived its mode and URLs from a hardcoded domain list — any domain other than
app.cg/staging.app.cgsilently ran with dev semantics, and several prod code paths were hard-broken on other domains:.app.cg→ login impossible on other domainsid.app.cg→ passkeys impossibleSelf-hosted instances now declare their identity at serve time via an injected
window.__CG_INSTANCE__script (deployment mode, app URL, CG ID URL, reCAPTCHA site key). The API server injects it into theindex.htmlit serves for share links; the selfhost nginx image injects it into the statically served copies.src/common/instance.tsis the typed accessor;config.ts/urls.tsconsult it first and fall back to the legacy domain detection.app.cg, staging.app.cg and local dev behave exactly as before when no instance config is injected — cookie domain, rpID and sitemap all derive to their previous values.
reCAPTCHA is now optional: with no keys configured, the frontend hides the widget and the backend skips verification (with a warning). Registration works out of the box on private instances; instances that want captcha set their own keys.
3. Self-hosting profile (
51897aa,dba7dd7)A production deployment path for one machine:
docker/docker-compose.selfhost.yml— Caddy terminates TLS (automatic Let's Encrypt) for the app, theid.subdomain and mediasoup signalling (:4443); prod semantics on any domain; no hardhat/test contracts; postgres actually loads the tuned config; Redis capped at 512 MB each (was 2 GB); mediasoup auto-restarts.docker/selfhost/init.sh— one-shot init: fresh secrets, matching S3 credentials, internal certs. Never overwrites.docker/selfhost/selfhost.sh—build/up/down/logs/stats/update.docker/SELFHOST.md— setup guide, optional-integrations matrix, tuning, backups.A self-hoster's whole journey: two DNS records →
init.sh <domain> <email>→selfhost.sh build→selfhost.sh up.4. Dev-stack fixes (
d972d8b)postgresql.confwas mounted but never loaded (no-c config_file=) — all tuning in that file, includingmax_connections=200, was silently inert. Now wired up.updateFrontend.shalways died with a heap OOM: it ran craco withoutNODE_OPTIONS, and webpack needs ~3 GB (measured) against Node's ~2 GB default. Now 4 GB.redis-blockscout(its only consumer is commented out); dropped obsolete composeversionattributes.5. Bugs found by testing the live instance (
a62f40d)'wasm-unsafe-eval'— web3 deps need it), SPA-shell fallback for deep links,/api/ws/proxy,manifest-src 'self'for the CG ID manifest.Measured results (8-core / 15 GiB VPS)
build.shis 2.5× headroom; selfhost build uses 4 GB + no sourcemaps)cg.mogged.eu: Let's Encrypt issuance, email signup (captcha-less), login, session cookie on the instance domain, community creation, socket.io, full WebAuthn passkey ceremony (rp.idcorrect for the instance,verified: true, cross-origin session shared with the CG ID popup) — the passkey flow was driven headless with a CDP virtual authenticator.Not in scope / follow-ups
previewImageUpdatejob fails withcolumn "imageId" does not exist🤖 Generated with Claude Code