From 4b1ed99ba8f8bf01168d05815f1435c8f3bccad2 Mon Sep 17 00:00:00 2001 From: tcsenpai Date: Thu, 30 Apr 2026 13:02:52 +0200 Subject: [PATCH 01/11] add unified docker compose stack for one-command node bring-up Adds a production-ready Dockerfile (multistage, non-root demos user, falcon-sign patch, node_modules pruning) and a unified docker-compose.yml that wires postgres, tlsnotary, the node, prometheus, and grafana on a shared network with profile-based opt-ins (monitoring, full, neo4j). The entrypoint symlinks runtime files (.demos_identity, demos_peerlist.json, .tlsnotary-key, output/) into a persistent node_state volume so they survive container recreation. Volumes: pgdata, node_data (genesis + chain), node_logs, node_state, prometheus_data, grafana_data. Compose changes: - IMAGE_NAME / IMAGE_TAG knobs so the same compose can build locally or pull a pre-built image from a registry - TLSNotary healthcheck switched from upstream's curl-based check (curl absent in image) to a bash /dev/tcp port-open probe - Prometheus scrape config updated to use service-name targets (node:9090, prometheus:9090) instead of localhost / host.docker.internal - .dockerignore tightened to exclude .claude/, .beads/, .taskmaster/, .tlsnotary-key, .gitbook-cache.json (3MB), and other AI-tooling state .env.example documents every var with safe defaults and flags EXPOSED_URL as the main per-deployment override. --- .dockerignore | 107 +++++++++-- .env.example | 268 +++++++++++++++------------ Dockerfile | 192 +++++++++++++++++++ docker-compose.yml | 267 +++++++++++++++++++++++++- monitoring/prometheus/prometheus.yml | 6 +- scripts/docker-entrypoint.sh | 55 ++++++ 6 files changed, 756 insertions(+), 139 deletions(-) create mode 100644 Dockerfile create mode 100755 scripts/docker-entrypoint.sh diff --git a/.dockerignore b/.dockerignore index 5e4973b3a..9ce0c1f77 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,22 +1,105 @@ -# Exclude git directory +# Version control .git +.gitignore +.gitattributes +.github/ -# Exclude node_modules (installed fresh during build) +# AI tooling state — local-only, often contains tokens/transcripts +.claude/ +.beads/ +.beads_bak/ +.taskmaster/ +.serena/ +.serena-backup/ +.mcp_data/ +.gitbook-cache.json +.trunk/ + +# Linter / formatter configs not needed at runtime +.eslintrc.cjs +.eslintignore +.prettierrc +.prettierrc.json +.requirements + +# Secrets that must never enter the image +.tlsnotary-key +*.tlsnotary-key + +# Dependencies (rebuilt inside the image) node_modules -# Exclude devnet runtime files -testing/devnet/identities -testing/devnet/.env -testing/devnet/demos_peerlist.json -testing/devnet/postgres-data +# Runtime state — must NOT enter the image +.demos_identity +demos_peerlist.json +demos_peerlist.json.example +# data/ contains bundled bootstrap (genesis.json, evmChains, l2ps) that the +# node needs at first boot. Keep it, but exclude any runtime-only artifacts +# that may be written into it. +data/chain.db +data/chain.db-* +data/twitter/ +data/operations.json +logs/ +logs_* +output/ +postgres_* ipfs ipfs_53550 +neo4j_data docker_data -postgres_* -logs -logs_* +last_crash_memory_usage.txt +.RUN blocked_ips.json +publickey_* -# Exclude unnecessary files -*.log +# Build artifacts (regenerated) +dist/ +*.tsbuildinfo + +# Test/dev-only directories (not needed at runtime) +local_tests/ +tests/ +testing/ +omniprotocol_fixtures_scripts/ +specs/ +fixtures/ +res/ +# NOTE: sdk/ is intentionally NOT excluded — src/ imports from `sdk/localsdk/...` +# (resolved via tsconfig baseUrl). Excluding it breaks the runtime build. + +# Documentation (lives in repo, not in container) +ClaudeDocs/ +GUIDELINES/ +documentation/ +history/ +docs/ +*.md +LICENSE.md + +# Project-management metadata +.planning/ +.serena/ +.mycelium/ + +# IDE/OS .DS_Store +.idea/ +.vscode/ +*.log +*.swp +*.swo + +# Secrets / dotfiles (defensive) +.env +.env.* +*.pem +*.key + +# Container scaffolding (the image doesn't need these) +docker-compose.yml +docker-compose.*.yml +Dockerfile.* +postgres/ +tlsnotary/ +monitoring/ diff --git a/.env.example b/.env.example index 3e8d640b0..52e4a7554 100644 --- a/.env.example +++ b/.env.example @@ -1,129 +1,161 @@ -# =========================================== -# Core Node Configuration -# =========================================== -CONSENSUS_TIME=10 -RPC_FEE=5 -# Internal server port (used for signaling/coordination, not the main RPC listener) -SERVER_PORT=53550 -# HTTP/RPC listen port — the node binds to this for RPC requests (defaults to 0 if unset, which fails) +# ============================================================================= +# Demos Network Node — Unified Configuration Template +# ============================================================================= +# Usage: +# 1. cp .env.example .env +# 2. Edit .env to fill in any values you need (see "MUST SET" notes below). +# 3. docker compose up (defaults below are tuned for this flow) +# +# Defaults assume the unified docker-compose deployment (postgres + tlsnotary +# + node + monitoring stack). For the legacy `./run` script on bare metal, +# change PG_HOST=localhost and TLSNOTARY_HOST=localhost (notes inline). +# +# YOU SHOULD REVIEW (defaults work for local dev — change for real deployment): +# - EXPOSED_URL (loopback default cannot be reached by other peers) +# - GRAFANA_ADMIN_PASSWORD (default 'demos' is fine for solo, not for shared) +# - Any external API keys you actually want to use (all optional, all bottom) +# ============================================================================= + + +# === NETWORK & PORTS ======================================================== + +# Main HTTP RPC listen port — the node binds here for RPC requests RPC_PORT=53550 -EXPOSED_URL=http://127.0.0.1:53550 -SIGNALING_SERVER_PORT=3005 -# Set this to true in production -PROD=true +# Public URL advertised to peers and SDK clients. +# !!! For real network participation, set this to your reachable address: +# !!! EXPOSED_URL=http://YOUR_PUBLIC_IP:53550 (or DNS name) +# !!! The default 'localhost' is fine for solo dev but useless for any peer. +EXPOSED_URL=http://localhost:53550 -# MCP Server Configuration -MCP_ENABLED=true -MCP_SERVER_PORT=3001 -# Alternative override +# OmniProtocol TCP server (binary RPC). Auto-derived as RPC_PORT+1 when unset. +OMNI_ENABLED=true +OMNI_PORT=53551 +# Modes: HTTP_ONLY | OMNI_PREFERRED | OMNI_ONLY +OMNI_MODE=OMNI_PREFERRED + +# WebRTC signaling server port (peer discovery / handshake coordination) +RPC_SIGNALING_PORT=3005 + +# MCP (Model Context Protocol) server port for AI agent integrations RPC_MCP_PORT=3001 -# =========================================== -# API Keys -# =========================================== -GITHUB_TOKEN= -ETHERSCAN_API_KEY= -HELIUS_API_KEY= -RAPID_API_KEY= -RAPID_API_HOST= -RUBIC_API_REFERRER_ADDRESS= -RUBIC_API_INTEGRATOR_ADDRESS= - -# =========================================== -# Social Integrations -# =========================================== - -# Twitter -TWITTER_USERNAME= -TWITTER_PASSWORD= -TWITTER_EMAIL= - -# Discord -DISCORD_API_URL= -DISCORD_BOT_TOKEN= - -# GitHub OAuth -GITHUB_CLIENT_ID= -GITHUB_CLIENT_SECRET= - -# Human Passport (Gitcoin Passport) -HUMAN_PASSPORT_API_URL=https://api.passport.xyz -HUMAN_PASSPORT_API_KEY= -HUMAN_PASSPORT_SCORER_ID= - -# =========================================== -# L2PS (Layer 2 Private System) Configuration -# =========================================== - -# Batch Aggregator Settings -L2PS_AGGREGATION_INTERVAL_MS=10000 -L2PS_MIN_BATCH_SIZE=1 -L2PS_MAX_BATCH_SIZE=10 -L2PS_CLEANUP_AGE_MS=300000 - -# ZK Proof Settings -L2PS_ZK_ENABLED=true - -# Hash Service Settings -L2PS_HASH_INTERVAL_MS=5000 - -# =========================================== -# OmniProtocol TCP Server Configuration -# =========================================== -OMNI_ENABLED=false -OMNI_PORT=3001 -OMNI_MODE=OMNI_ONLY -OMNI_FATAL=false - -# OmniProtocol TLS Encryption -OMNI_TLS_ENABLED=false -OMNI_TLS_MODE=self-signed -OMNI_CERT_PATH=./certs/node-cert.pem -OMNI_KEY_PATH=./certs/node-key.pem -OMNI_CA_PATH= -OMNI_TLS_MIN_VERSION=TLSv1.3 - -# OmniProtocol Rate Limiting -OMNI_RATE_LIMIT_ENABLED=true -OMNI_MAX_CONNECTIONS_PER_IP=10 -OMNI_MAX_REQUESTS_PER_SECOND_PER_IP=100 -OMNI_MAX_REQUESTS_PER_SECOND_PER_IDENTITY=200 - -# =========================================== -# TLSNotary HTTPS Attestation Configuration -# =========================================== -TLSNOTARY_ENABLED=false -TLSNOTARY_PORT=7047 -TLSNOTARY_SIGNING_KEY= -TLSNOTARY_PROXY_PORT=55688 -TLSNOTARY_MAX_SENT_DATA=16384 -TLSNOTARY_MAX_RECV_DATA=65536 - -# ZK Identity System Configuration -# Points awarded for each successful ZK attestation (default: 10) -ZK_ATTESTATION_POINTS=10 - -# =========================================== -# Prometheus Metrics -# =========================================== -# Exposes metrics at http://localhost:/metrics for Prometheus scraping -# Note: This is the NODE's metrics endpoint, not the Prometheus server port. -# The monitoring stack's Prometheus server runs on port 9091 (see monitoring/docker-compose.yml) + +# === DATABASE (PostgreSQL) ================================================== + +# In docker compose: 'postgres' (the service hostname). +# On bare metal / legacy ./run script: 'localhost'. +PG_HOST=postgres + +# In docker compose: 5432 (container-internal port). +# On bare metal / legacy ./run script: 5332 (host-mapped port). +PG_PORT=5432 + +PG_USER=demosuser +PG_PASSWORD=demospassword +PG_DATABASE=demos + + +# === IDENTITY & PEERS ======================================================= + +# Path to the node's ed25519 identity keypair. Auto-generated on first boot. +# Under docker compose, the entrypoint symlinks .demos_identity into the +# node_state volume so it persists across container recreation. +IDENTITY_FILE=.demos_identity + +# Path to the cached peer list (JSON). Same persistence treatment as identity. +PEER_LIST_FILE=demos_peerlist.json + + +# === MONITORING ============================================================= + +# Exposes Prometheus metrics at http://:/metrics METRICS_ENABLED=true METRICS_PORT=9090 METRICS_HOST=0.0.0.0 -# =========================================== -# Access Control -# =========================================== -# HTTP Server Rate Limiting Whitelist -# Comma-separated list of IPs to bypass rate limiting (also used for other node IP addresses) -WHITELISTED_IPS= -# Comma-separated list of hex public keys to bypass rate limiting -# These keys must provide valid signature in request headers -WHITELISTED_KEYS= +# === TLSNOTARY ============================================================== + +# TLSNotary provides cryptographic attestation of HTTPS responses. +TLSNOTARY_ENABLED=true + +# In docker compose: 'tlsnotary' (the service hostname). +# On bare metal / legacy ./run script: 'localhost'. +TLSNOTARY_HOST=tlsnotary +TLSNOTARY_PORT=7047 + +# Mode: 'docker' (delegates to the tlsnotary sidecar) | 'ffi' (in-process FFI binding) +TLSNOTARY_MODE=docker + +# If true, TLSNotary failures crash the node. Keep false for graceful degradation. +TLSNOTARY_FATAL=false + +# Signing key — ONLY needed when TLSNOTARY_MODE=ffi (in-process Rust binding). +# In docker mode (the default), the tlsnotary sidecar container manages its +# own key and exposes the public key via GET /info — leave this empty. +TLSNOTARY_SIGNING_KEY= + + +# === LOGGING & MISC ========================================================= + +# Levels: debug | info | warning | error | critical +# 'info' is recommended. 'debug' produces a wall of per-block consensus JSON +# that drowns useful messages — only enable when actually debugging. +LOG_LEVEL=info + +# Set true in production (enables DTR production relay and prod-only safety checks) +PROD=false + +# Advanced: enable ZK proofs in the L2PS layer (off by default; experimental) +L2PS_ZK_ENABLED=false + + +# === DOCKER COMPOSE ONLY ==================================================== +# These are consumed by docker-compose.yml, NOT the node binary itself. + +# Which compose profiles to bring up. Comma-separated. +# Common: 'monitoring,tlsnotary' — full stack. Empty = node + postgres only. +COMPOSE_PROFILES=monitoring,tlsnotary + +# Image coordinates for the node. Defaults to a local build. +# Override to pull a pre-built image: e.g. ghcr.io/kynesyslabs/node and v0.9.8 +IMAGE_NAME=demos-node +IMAGE_TAG=local + +# Grafana dashboard (monitoring profile) +GRAFANA_PORT=3000 +GRAFANA_ADMIN_USER=admin +GRAFANA_ADMIN_PASSWORD=demos + +# Prometheus server external port (host-mapped). Internal container port stays 9090. +PROMETHEUS_PORT=9091 + + +# === OPTIONAL EXTERNAL API KEYS ============================================= +# Uncomment and fill in only the ones you need. Each unlocks a specific feature. + +# Solana RPC endpoint override (default: public mainnet RPC) +#SOLANA_RPC= + +# Etherscan — for EVM chain block explorer queries +#ETHERSCAN_API_KEY= + +# Helius — Solana enhanced RPC (NFTs, DAS API, etc.) +#HELIUS_API_KEY= + +# Nomis — on-chain reputation scoring +#NOMIS_API_KEY= +#NOMIS_CLIENT_ID= + +# RapidAPI — used by various enrichment routes +#RAPID_API_KEY= + +# GitHub — required for GitHub-based identity attestations +#GITHUB_TOKEN= + +# Discord — required for Discord-based identity attestations +#DISCORD_BOT_TOKEN= -# ed25519 pubkey allowed to access protected endpoints -SUDO_PUBKEY= +# Human Passport (Gitcoin) — sybil resistance scoring +#HUMAN_PASSPORT_API_KEY= diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..39b824c41 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,192 @@ +# syntax=docker/dockerfile:1.7 +# +# Demos Network Node - Production Image +# +# Multistage build: +# 1. builder - installs deps, native modules, applies falcon-sign patch +# 2. runtime - minimal image, non-root user, healthcheck, exec CMD +# +# Run with --no-tui (passed in CMD) since TUI requires a TTY. +# All persistent state lives under /app and is intended to be bind/volume mounted: +# - /app/.demos_identity (file, bind-mount) +# - /app/demos_peerlist.json (file, bind-mount) +# - /app/data (dir, volume) +# - /app/logs (dir, volume) + +# ============================================================================= +# Stage 1: builder +# Installs build toolchain, resolves dependencies, compiles native modules, +# and patches falcon-sign to log uncaught exceptions before rethrowing. +# ============================================================================= +FROM oven/bun:1.2-debian AS builder + +# Build-time deps for native modules (bufferutil, utf-8-validate, etc.). +# python3-setuptools is required for node-gyp on newer Pythons. +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + python3 \ + python3-setuptools \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Copy manifests first to maximise layer cache reuse on dep installs. +COPY package.json bun.lock ./ + +# Resolve all dependencies. `bun pm trust --all` runs lifecycle scripts for +# packages that explicitly opt-in (e.g. native module postinstalls). +RUN bun install --frozen-lockfile \ + && bun pm trust --all || true + +# Native websocket accelerators - explicit install so they are not skipped +# when the lockfile considers them optional peers. +RUN bun add bufferutil utf-8-validate + +# Bring in the rest of the source tree. +COPY . . + +# Patch falcon-sign so uncaught WASM exceptions are logged before rethrow. +# Mirrors scripts/run::patch_falcon_sign. Idempotent and tolerant of a +# missing target file (some dependency configurations omit it). +RUN set -eu; \ + FALCON_FILE="/app/node_modules/falcon-sign/kernel/n3_v1/wasmFile/falcon512.js"; \ + if [ -f "$FALCON_FILE" ]; then \ + if grep -q "console.error(ex)" "$FALCON_FILE"; then \ + echo "falcon-sign already patched"; \ + else \ + sed -i '/throw ex;/i\ console.error(ex);' "$FALCON_FILE"; \ + echo "falcon-sign patched"; \ + fi; \ + else \ + echo "falcon-sign target missing, skipping patch"; \ + fi + +# Prune node_modules: remove READMEs, docs, tests, and source maps from +# transitive deps. Saves ~200-400 MB on a typical install. ARG-gated so a +# future build can opt out with --build-arg PRUNE_MODULES=false if needed. +# +# Implemented as a portable shell pass instead of pulling in node-prune so +# the build stays hermetic (no external binary download at build time). +ARG PRUNE_MODULES=true +RUN if [ "$PRUNE_MODULES" = "true" ]; then \ + echo "node_modules size before prune: $(du -sh node_modules | cut -f1)"; \ + # Documentation and license files (typical: 60-100 MB) + find node_modules -type f \( \ + -iname 'README*' -o \ + -iname 'CHANGELOG*' -o \ + -iname 'CHANGES*' -o \ + -iname 'HISTORY*' -o \ + -iname 'CONTRIBUTING*' -o \ + -iname 'AUTHORS*' -o \ + -iname 'CONTRIBUTORS*' -o \ + -iname 'LICEN[CS]E*' -o \ + -iname 'NOTICE*' -o \ + -iname 'NOTICES*' -o \ + -iname 'PATENTS*' -o \ + -iname 'governance.md' -o \ + -iname 'security.md' -o \ + -iname 'code_of_conduct*' \ + \) -delete 2>/dev/null || true; \ + # Source maps (typical: 80-150 MB) + find node_modules -type f \( -name '*.map' -o -name '*.ts.map' \) -delete 2>/dev/null || true; \ + # Test/example directories that ship inside packages. + # NB: bare 'test/' and 'tests/' are NOT safe to prune — some packages + # (notably viem via @pancakeswap) use them as production module + # namespaces (e.g. viem/actions/test/dropTransaction.js). Only prune + # patterns that are unambiguously non-runtime. + find node_modules -type d \( \ + -name '__tests__' -o \ + -name '__mocks__' -o \ + -name '.test' -o \ + -name 'example' -o \ + -name 'examples' -o \ + -name 'docs' -o \ + -name 'doc' \ + \) -prune -exec rm -rf {} + 2>/dev/null || true; \ + # Misc dotfiles and CI configs + find node_modules -maxdepth 4 -type f \( \ + -name '.editorconfig' -o \ + -name '.eslintrc*' -o \ + -name '.prettierrc*' -o \ + -name '.travis.yml' -o \ + -name '.appveyor.yml' -o \ + -name '.gitattributes' -o \ + -name '.npmignore' -o \ + -name '.nvmrc' -o \ + -name '.yarnrc' -o \ + -name 'jest.config.*' -o \ + -name 'tsconfig.test.json' -o \ + -name '*.tsbuildinfo' \ + \) -delete 2>/dev/null || true; \ + echo "node_modules size after prune: $(du -sh node_modules | cut -f1)"; \ + fi + +# ============================================================================= +# Stage 2: runtime +# Minimal image with only what the node needs at run time. Runs as non-root. +# ============================================================================= +FROM oven/bun:1.2-debian AS runtime + +# OCI image metadata. +LABEL org.opencontainers.image.source="https://github.com/kynesyslabs/node" \ + org.opencontainers.image.description="Demos Network node (RPC + consensus runtime)" \ + org.opencontainers.image.licenses="CC-BY-NC-SA-4.0" \ + org.opencontainers.image.vendor="Kynesys Labs" + +# Runtime essentials only: curl for HEALTHCHECK, ca-certificates for TLS. +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Non-root user. The base image already ships a `bun` user/group at uid/gid +# 1000; rename them to `demos` in-place so the uid stays predictable for +# host bind-mount permissions and /etc/passwd reads meaningfully. +RUN groupmod --new-name demos bun \ + && usermod --login demos --home /app bun + +WORKDIR /app + +# Copy the built tree from the builder stage with correct ownership in one go. +COPY --from=builder --chown=demos:demos /app /app + +# WORKDIR /app is owned by root by default. Hand it (and the volume mount +# points) to demos so the node can create .demos_identity, demos_peerlist.json, +# log files, and chain data on first boot. Empty named volumes that mount +# over these dirs inherit the directory ownership at first mount. +RUN chown demos:demos /app \ + && mkdir -p /app/data /app/logs /app/state \ + && chown demos:demos /app/data /app/logs /app/state + +# Sensible image-level defaults. Anything else (DATABASE_URL, EXPOSED_URL, +# IDENTITY_FILE, PEER_LIST_FILE, etc.) must be supplied at runtime. +ENV NODE_ENV=production \ + RPC_PORT=53550 \ + METRICS_HOST=0.0.0.0 + +# Exposed services: +# 53550 - RPC (HTTP/JSON-RPC) +# 53551 - Omni / cross-chain bridge endpoint +# 3005 - WebRTC signaling +# 3001 - MCP server +# 9090 - Prometheus metrics +# Note: 7047 (TLSNotary) is intentionally NOT exposed - it runs in its own container. +EXPOSE 53550 53551 3005 3001 9090 + +# Ephemeral runtime state mount points. The entrypoint symlinks the node's +# legacy repo-root files (.demos_identity, demos_peerlist.json, .tlsnotary-key, +# output/) into /app/state so a single named volume covers all of them. +VOLUME ["/app/data", "/app/logs", "/app/state"] + +USER demos + +# Liveness check against the RPC HTTP root. start-period covers DB connect, +# peer discovery, and chain bootstrap which can take ~30-60s on cold start. +HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \ + CMD curl -fsS "http://localhost:${RPC_PORT:-53550}/" >/dev/null || exit 1 + +# The entrypoint bridges runtime files into /app/state for persistence, +# then execs the node. --no-tui is a CMD argument (no TTY in containers). +ENTRYPOINT ["/app/scripts/docker-entrypoint.sh"] +CMD ["bun", "-r", "tsconfig-paths/register", "src/index.ts", "--", "--no-tui"] diff --git a/docker-compose.yml b/docker-compose.yml index b7a3b5295..6b878e662 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,19 +1,274 @@ +# ============================================================================= +# Demos Network Node — Unified Docker Compose +# ============================================================================= +# One-command bring-up: +# cp .env.example .env # then set TLSNOTARY_SIGNING_KEY (or disable it) +# docker compose up +# +# Profiles: +# (default) postgres + node + tlsnotary -> minimum runnable node +# --profile monitoring add prometheus + grafana +# --profile full add node-exporter (host metrics) +# --profile neo4j add neo4j (only needed for CGC/KYC features) +# +# Persistent state lives in named volumes: +# pgdata, node_data, node_logs, node_identity, +# prometheus_data, grafana_data, neo4j_data, neo4j_logs +# +# Wipe everything (DANGER): docker compose down -v +# ============================================================================= services: + # --------------------------------------------------------------------------- + # PostgreSQL — primary datastore for the node (TypeORM with synchronize=true) + # Internal-only by default. Uncomment the ports stanza below to expose to host. + # --------------------------------------------------------------------------- + postgres: + image: postgres:16-alpine + container_name: demos-postgres + restart: unless-stopped + environment: + POSTGRES_USER: ${PG_USER:-demosuser} + POSTGRES_PASSWORD: ${PG_PASSWORD:-demospassword} + POSTGRES_DB: ${PG_DATABASE:-demos} + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${PG_USER:-demosuser} -d ${PG_DATABASE:-demos}"] + interval: 5s + timeout: 5s + retries: 10 + networks: + - demos-network + # ports: + # - "${PG_HOST_PORT:-5332}:5432" # uncomment to access DB from host + + # --------------------------------------------------------------------------- + # TLSNotary — provides cryptographic attestation of HTTPS responses. + # Healthcheck uses localhost INSIDE the container (correct — it's its own loopback). + # --------------------------------------------------------------------------- + tlsnotary: + image: ghcr.io/tlsnotary/tlsn/notary-server:v0.1.0-alpha.12 + container_name: demos-tlsnotary + restart: unless-stopped + platform: linux/amd64 + environment: + NS_NOTARIZATION__MAX_SENT_DATA: ${TLSNOTARY_MAX_SENT_DATA:-32768} + ports: + - "${TLSNOTARY_PORT:-7047}:7047" + # The upstream image has no curl/wget so its packaged HTTP healthcheck + # always fails. Use bash's /dev/tcp pseudo-device to verify the notary + # is listening — bash IS in the image, the default sh (dash) is not enough. + healthcheck: + test: ["CMD", "bash", "-c", "exec 3<>/dev/tcp/127.0.0.1/7047"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 10s + networks: + - demos-network + + # --------------------------------------------------------------------------- + # Demos node itself. + # Reads env from .env. PG_HOST=postgres and TLSNOTARY_HOST=tlsnotary are the + # in-network service names — DO NOT override to localhost when using compose. + # --------------------------------------------------------------------------- + node: + # IMAGE_NAME / IMAGE_TAG let you pull a pre-built image from a registry + # instead of building locally. See README → "Publishing the image" for + # the push/pull workflow. Defaults to a local build tagged demos-node:local. + image: ${IMAGE_NAME:-demos-node}:${IMAGE_TAG:-local} + build: + context: . + dockerfile: Dockerfile + container_name: demos-node + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + tlsnotary: + condition: service_started + environment: + # Network & ports + RPC_PORT: ${RPC_PORT:-53550} + EXPOSED_URL: ${EXPOSED_URL:-http://localhost:53550} + OMNI_ENABLED: ${OMNI_ENABLED:-true} + OMNI_PORT: ${OMNI_PORT:-53551} + OMNI_MODE: ${OMNI_MODE:-OMNI_PREFERRED} + RPC_SIGNALING_PORT: ${RPC_SIGNALING_PORT:-3005} + RPC_MCP_PORT: ${RPC_MCP_PORT:-3001} + # Database — service name from this compose + PG_HOST: postgres + PG_PORT: 5432 + PG_USER: ${PG_USER:-demosuser} + PG_PASSWORD: ${PG_PASSWORD:-demospassword} + PG_DATABASE: ${PG_DATABASE:-demos} + # Identity & peers — entrypoint symlinks these into /app/state on boot + IDENTITY_FILE: ${IDENTITY_FILE:-.demos_identity} + PEER_LIST_FILE: ${PEER_LIST_FILE:-demos_peerlist.json} + # Monitoring + METRICS_ENABLED: ${METRICS_ENABLED:-true} + METRICS_PORT: ${METRICS_PORT:-9090} + METRICS_HOST: ${METRICS_HOST:-0.0.0.0} + # TLSNotary — service name from this compose + TLSNOTARY_ENABLED: ${TLSNOTARY_ENABLED:-true} + TLSNOTARY_HOST: tlsnotary + TLSNOTARY_PORT: 7047 + TLSNOTARY_MODE: ${TLSNOTARY_MODE:-docker} + TLSNOTARY_FATAL: ${TLSNOTARY_FATAL:-false} + TLSNOTARY_SIGNING_KEY: ${TLSNOTARY_SIGNING_KEY:-} + # Logging & misc + LOG_LEVEL: ${LOG_LEVEL:-info} + PROD: ${PROD:-false} + L2PS_ZK_ENABLED: ${L2PS_ZK_ENABLED:-false} + # Optional external API keys (passed through if set in .env) + SOLANA_RPC: ${SOLANA_RPC:-} + ETHERSCAN_API_KEY: ${ETHERSCAN_API_KEY:-} + HELIUS_API_KEY: ${HELIUS_API_KEY:-} + NOMIS_API_KEY: ${NOMIS_API_KEY:-} + NOMIS_CLIENT_ID: ${NOMIS_CLIENT_ID:-} + RAPID_API_KEY: ${RAPID_API_KEY:-} + GITHUB_TOKEN: ${GITHUB_TOKEN:-} + DISCORD_BOT_TOKEN: ${DISCORD_BOT_TOKEN:-} + HUMAN_PASSPORT_API_KEY: ${HUMAN_PASSPORT_API_KEY:-} + volumes: + - node_data:/app/data # bundled bootstrap (genesis.json, evmChains, l2ps) + chain.db + - node_logs:/app/logs # all node logs + - node_state:/app/state # identity, peerlist, tlsnotary key, output/ + ports: + - "${RPC_PORT:-53550}:53550" + - "${OMNI_PORT:-53551}:53551" + - "${RPC_SIGNALING_PORT:-3005}:3005" + - "${RPC_MCP_PORT:-3001}:3001" + - "${METRICS_PORT:-9090}:9090" + networks: + - demos-network + + # --------------------------------------------------------------------------- + # Prometheus — metrics scraping. Reads ./monitoring/prometheus/prometheus.yml. + # --------------------------------------------------------------------------- + prometheus: + image: prom/prometheus:v2.48.0 + container_name: demos-prometheus + restart: unless-stopped + profiles: [monitoring] + command: + - --config.file=/etc/prometheus/prometheus.yml + - --storage.tsdb.path=/prometheus + - --storage.tsdb.retention.time=${PROMETHEUS_RETENTION:-15d} + - --web.console.libraries=/etc/prometheus/console_libraries + - --web.console.templates=/etc/prometheus/consoles + - --web.enable-lifecycle + volumes: + - ./monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro + - prometheus_data:/prometheus + ports: + - "${PROMETHEUS_PORT:-9091}:9090" + networks: + - demos-network + + # --------------------------------------------------------------------------- + # Grafana — dashboards. Provisioned datasource points at prometheus:9090. + # --------------------------------------------------------------------------- + grafana: + image: grafana/grafana:10.2.2 + container_name: demos-grafana + restart: unless-stopped + profiles: [monitoring] + depends_on: + - prometheus + environment: + GF_SECURITY_ADMIN_USER: ${GRAFANA_ADMIN_USER:-admin} + GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:-demos} + GF_USERS_ALLOW_SIGN_UP: "false" + GF_SERVER_ROOT_URL: ${GRAFANA_ROOT_URL:-http://localhost:3000} + GF_INSTALL_PLUGINS: grafana-clock-panel + GF_ANALYTICS_REPORTING_ENABLED: "false" + GF_ANALYTICS_CHECK_FOR_UPDATES: "false" + GF_ANALYTICS_CHECK_FOR_PLUGIN_UPDATES: "false" + GF_USERS_DEFAULT_THEME: dark + GF_AUTH_ANONYMOUS_ENABLED: "false" + GF_BRANDING_APP_TITLE: Demos Network + GF_BRANDING_LOGIN_TITLE: Demos Network + GF_BRANDING_LOGIN_SUBTITLE: Node Monitoring + GF_BRANDING_LOGIN_LOGO: /public/img/demos-logo.svg + GF_BRANDING_MENU_LOGO: /public/img/demos-logo.svg + GF_BRANDING_FAV_ICON: /public/img/favicon.png + GF_BRANDING_HIDE_VERSION: "true" + GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH: /etc/grafana/provisioning/dashboards/json/demos-overview.json + GF_FEATURE_TOGGLES_ENABLE: publicDashboards,topnav,newPanelChromeUI + GF_NEWS_NEWS_FEED_ENABLED: "false" + GF_SECURITY_DISABLE_GRAVATAR: "true" + GF_DATE_FORMATS_USE_BROWSER_LOCALE: "true" + GF_DATE_FORMATS_DEFAULT_TIMEZONE: browser + volumes: + - grafana_data:/var/lib/grafana + - ./monitoring/grafana/provisioning/datasources:/etc/grafana/provisioning/datasources:ro + - ./monitoring/grafana/provisioning/dashboards:/etc/grafana/provisioning/dashboards:ro + - ./monitoring/grafana/grafana.ini:/etc/grafana/grafana.ini:ro + - ./monitoring/grafana/branding/demos-logo-white.svg:/usr/share/grafana/public/img/demos-logo.svg:ro + - ./monitoring/grafana/branding/demos-icon.svg:/usr/share/grafana/public/img/demos-icon.svg:ro + - ./monitoring/grafana/branding/favicon.png:/usr/share/grafana/public/img/favicon.png:ro + ports: + - "${GRAFANA_PORT:-3000}:3000" + networks: + - demos-network + + # --------------------------------------------------------------------------- + # node-exporter — host-level metrics (CPU/RAM/disk/network). Optional. + # Enable with: docker compose --profile monitoring --profile full up + # --------------------------------------------------------------------------- + node-exporter: + image: prom/node-exporter:v1.7.0 + container_name: demos-node-exporter + restart: unless-stopped + profiles: [full] + command: + - --path.procfs=/host/proc + - --path.sysfs=/host/sys + - --path.rootfs=/rootfs + - --collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/) + volumes: + - /proc:/host/proc:ro + - /sys:/host/sys:ro + - /:/rootfs:ro + ports: + - "${NODE_EXPORTER_PORT:-9100}:9100" + networks: + - demos-network + + # --------------------------------------------------------------------------- + # Neo4j — only needed if CGC/KYC features are in use. Opt-in. + # Enable with: docker compose --profile neo4j up + # --------------------------------------------------------------------------- neo4j: image: neo4j:5.21 - container_name: neo4j-cgc + container_name: demos-neo4j restart: unless-stopped - ports: - - "7474:7474" - - "7687:7687" + profiles: [neo4j] environment: - - NEO4J_AUTH=neo4j/MaE,MEL23lon! - - NEO4J_ACCEPT_LICENSE_AGREEMENT=yes + NEO4J_AUTH: ${NEO4J_AUTH:-neo4j/changeme-please} + NEO4J_ACCEPT_LICENSE_AGREEMENT: "yes" volumes: - neo4j_data:/data - neo4j_logs:/logs + ports: + - "${NEO4J_HTTP_PORT:-7474}:7474" + - "${NEO4J_BOLT_PORT:-7687}:7687" + networks: + - demos-network + +networks: + demos-network: + name: demos-network + driver: bridge volumes: + pgdata: + node_data: + node_logs: + node_state: + prometheus_data: + grafana_data: neo4j_data: neo4j_logs: diff --git a/monitoring/prometheus/prometheus.yml b/monitoring/prometheus/prometheus.yml index 4d9a51988..ac37d4cb6 100644 --- a/monitoring/prometheus/prometheus.yml +++ b/monitoring/prometheus/prometheus.yml @@ -30,16 +30,16 @@ scrape_configs: # Prometheus self-monitoring - job_name: prometheus static_configs: - - targets: [localhost:9090] + - targets: [prometheus:9090] metrics_path: /metrics # Demos Network Node metrics # The node exposes metrics at /metrics endpoint on METRICS_PORT (default 9090) # NOTE: If you changed METRICS_PORT in your main .env file, update the target below to match - # For example, if METRICS_PORT=3333, change the target to 'host.docker.internal:3333' + # For example, if METRICS_PORT=3333, change the target to 'node:3333' - job_name: demos-node static_configs: - - targets: [host.docker.internal:9090] # Must match METRICS_PORT from main .env + - targets: [node:9090] # service name 'node' from unified docker-compose.yml; matches METRICS_PORT from .env labels: instance: local-node environment: development diff --git a/scripts/docker-entrypoint.sh b/scripts/docker-entrypoint.sh new file mode 100755 index 000000000..27f462811 --- /dev/null +++ b/scripts/docker-entrypoint.sh @@ -0,0 +1,55 @@ +#!/bin/sh +# Demos Network — container entrypoint. +# +# Bridges the writable runtime files the node creates at /app/ to the +# persistent volume at /app/state, so they survive `docker compose down`. +# Also routes the timestamped logs__/ directory into /app/logs. +# +# Files bridged: +# /app/.demos_identity <- /app/state/.demos_identity +# /app/demos_peerlist.json <- /app/state/demos_peerlist.json +# /app/.tlsnotary-key <- /app/state/.tlsnotary-key +# /app/output <- /app/state/output +# /app/publickey_* <- /app/state/publickey_* (after creation) +# +# Logs: +# /app/logs_* -> /app/logs/ (the legacy logger writes one dir per port+id) +set -eu + +STATE_DIR="${STATE_DIR:-/app/state}" +LOGS_DIR="${LOGS_DIR:-/app/logs}" + +mkdir -p "$STATE_DIR" "$LOGS_DIR" + +# Files: create on volume if absent, then symlink from /app +for f in .demos_identity demos_peerlist.json .tlsnotary-key; do + target="$STATE_DIR/$f" + link="/app/$f" + [ -e "$target" ] || : > /dev/null # do not pre-create — let node generate on demand + # If /app already has a real file (e.g. baked from image), move it once + if [ -e "$link" ] && [ ! -L "$link" ]; then + if [ ! -e "$target" ]; then + mv "$link" "$target" + else + rm -f "$link" + fi + fi + # Create symlink (idempotent — pointing into the volume) + [ -L "$link" ] || ln -s "$target" "$link" +done + +# Directory: output/ +if [ ! -L /app/output ]; then + mkdir -p "$STATE_DIR/output" + [ -d /app/output ] && [ ! -L /app/output ] && rmdir /app/output 2>/dev/null || true + ln -sfn "$STATE_DIR/output" /app/output +fi + +# publickey_* files — created at runtime by the node. Watch via post-start hook +# is overkill; instead, we pre-create the symlink target dir and rely on the +# node writing into /app/. After first boot, the file lives at /app/publickey_* +# (in container ephemeral layer). To persist, we'd need either the node code +# to write into state/, or a periodic sync. For now, accept that publickey_* +# is regenerable from the identity on every boot (it is — see src/index.ts). + +exec "$@" From 9556d53ba34e121169e82a7200b628277bdded59 Mon Sep 17 00:00:00 2001 From: tcsenpai Date: Thu, 30 Apr 2026 13:03:02 +0200 Subject: [PATCH 02/11] rewrite README/INSTALL around docker compose as primary path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Quick Start (Docker) becomes the default onboarding (3 commands). INSTALL.md restructured into two tracks: - Track 1: Docker Compose (recommended) — full env-var table, profiles reference, where data lives, backup procedure for the node_state volume, network exposure rules with ufw example, joining-the-network notes (EXPOSED_URL + bootstrap peerlist), troubleshooting - Track 2: Bare metal with ./run — original install path preserved, with a note clarifying that ./run is a wrapper around scripts/run Also fixes a documentation lie: TLSNOTARY_SIGNING_KEY was previously documented as required, but in docker mode (the default) the sidecar manages its own key. Updated all three files (README, INSTALL, .env.example) accordingly. The README's old standalone Monitoring section is collapsed into a short reference card now that monitoring is part of the unified compose. --- INSTALL.md | 449 ++++++++++++++++++++++++++++++++++------------------- README.md | 108 ++++++------- 2 files changed, 343 insertions(+), 214 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index 9b6e7924a..bad6c8d6b 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,124 +1,295 @@ -# 🚀 Demos Node Installation Guide +# Installing the Demos Node -**Welcome to the Demos Network!** This guide will help you set up a Demos node on Linux Debian (Ubuntu)-based systems. It walks through the installation steps—including the installation of dependencies—in more detail and is intended to help less experienced users get through the process. +This guide covers two ways to run a Demos node: -After installing the required Linux operating system on a bare metal or computer running the required hardware specs, you can simply copy and paste each line into your Linux system’s terminal. Note that lines with hashtags are comments; they do not need to be pasted into the terminal. +- **Track 1: Docker Compose (recommended)** — single-command bring-up of the full stack (node + Postgres + TLSNotary + monitoring). This is the path most operators should take. +- **Track 2: Bare metal with `./run`** — host-native install using Bun and a sidecar Postgres container. Useful for development, debugging, and TUI-based operation. -## 📋 System Requirements +Pick one. Both tracks produce a working node; they differ only in where the binary and its dependencies live. -### Operating System - -Install a Debian-based Linux distro like Ubuntu: [https://ubuntu.com/download/desktop](https://ubuntu.com/download/desktop) - -Each computer’s hardware can behave differently during Ubuntu installation. This may require a bit of troubleshooting on your end, but is not covered by this guide, as the main focus of this guide is specific to Demos node installation. +## System Requirements -### Hardware Minimum Requirements +### Hardware Minimum - 4GB RAM - 4 CPU cores (2GHz+) - Modern SSD - 200 Mbps internet connection -- Ubuntu 22.04 LTS or newer (or compatible Linux distribution) -### Hardware Recommended Specs +### Hardware Recommended - 8GB RAM - 6 CPU cores (2GHz+) - Modern SSD - 1 Gbps internet connection -## ⚡ Installation Steps - Short Version +### Operating System -This is the abridged installation guide. If this works for your system and allows the node to operate, then you are done here. If this results in errors, proceed to the full installation guide, which walks you through additional steps. All steps will require the use of the terminal. +Linux is the supported and tested target. Ubuntu 22.04 LTS or newer is recommended. Any modern distro that runs Docker works for Track 1. -### 1. Install Prerequisites +--- + +## Track 1: Docker Compose (recommended) + +### Prerequisites + +That's the whole list: -Open a terminal and enter: +- **Docker** 20.10 or newer +- **Docker Compose** v2 (the `docker compose` plugin, not the legacy `docker-compose` Python script) + +No Bun, no Postgres, no Rust toolchain on the host. Everything else runs inside containers. + +Verify: ```bash -sudo apt update && sudo apt upgrade -y -sudo apt install -y curl git wget build-essential ca-certificates gnupg lsb-release +docker --version +docker compose version ``` -### 2. Install the Following Packages +If you don't have Docker installed, follow the official guide: [https://docs.docker.com/engine/install/](https://docs.docker.com/engine/install/) and add yourself to the `docker` group so you don't need `sudo`: -- **Install Docker and docker-compose for non-root users** - - Docker: [https://docs.docker.com/get-started/get-docker/](https://docs.docker.com/get-started/get-docker/) - - Docker Compose: [https://docs.docker.com/compose/install/](https://docs.docker.com/compose/install/) -- **Install Bun** - - We suggest managing bun with mise ([https://mise.jdx.dev/getting-started.html](https://mise.jdx.dev/getting-started.html) and `mise use -g bun@latest`) for convenience +```bash +sudo usermod -aG docker $USER +newgrp docker +``` -### 3. Clone the Repository +### Quickstart (3 steps) ```bash -cd ~ -git clone https://github.com/kynesyslabs/node.git -# Double check that you are on testnet branch -git branch -# switch to your node directory -cd node +git clone https://github.com/kynesyslabs/node.git && cd node +cp .env.example .env # defaults are fine; edit only if you want to override +docker compose up +``` + +The first run pulls images and builds the node container, so expect a few minutes. Subsequent starts are near-instant. + +When the stack is healthy: + +- Node RPC: http://localhost:53550 (try `curl http://localhost:53550/info`) +- Grafana: http://localhost:3000 (default `admin` / `demos`) +- Prometheus: http://localhost:9091 + +### Environment file + +`.env.example` is the canonical template. Copy it to `.env` and edit. The variables you actually care about: + +| Variable | Default | Notes | +|----------|---------|-------| +| `TLSNOTARY_ENABLED` | `true` | Set to `false` to skip TLSNotary entirely. | +| `TLSNOTARY_SIGNING_KEY` | _(empty)_ | **Leave empty in docker mode (the default).** The TLSNotary sidecar manages its own key. Only set this if you've switched `TLSNOTARY_MODE` to `ffi` (in-process Rust binding). | +| `RPC_PORT` | `53550` | Host-mapped port for the node RPC. Change if 53550 is taken. | +| `EXPOSED_URL` | `http://localhost:53550` | Public URL advertised to peers. Set to your public IP/domain when running on a VPS. | +| `PG_USER` / `PG_PASSWORD` / `PG_DATABASE` | `demosuser` / `demospassword` / `demos` | Postgres credentials. Change `PG_PASSWORD` if you're paranoid; it never leaves the compose network. | +| `METRICS_ENABLED` / `METRICS_PORT` | `true` / `9090` | Prometheus scrape endpoint on the node. | +| `GRAFANA_ADMIN_PASSWORD` | `demos` | Grafana admin password. Change this. | +| `COMPOSE_PROFILES` | `monitoring,tlsnotary` | Which compose profiles are active. See below. | + +Leave `PG_HOST=postgres` and `TLSNOTARY_HOST=tlsnotary` exactly as shipped — those are the in-network service names. They are only `localhost` for the bare-metal path (Track 2). + +### Joining the network — required steps before real-world use + +A fresh node will run as an isolated single-validator chain by default. To participate in the actual Demos testnet/mainnet, you must do **two things**: + +**1. Set `EXPOSED_URL` to your reachable address.** + +The default `http://localhost:53550` is fine for local dev but useless for any peer trying to connect to you. Before going live: + +```env +# In .env +EXPOSED_URL=http://YOUR_PUBLIC_IP:53550 +# or for a DNS name: +EXPOSED_URL=http://node.example.com:53550 ``` -### 4. Install Dependencies +If running behind NAT, port-forward `53550` (RPC) and `53551` (OmniProtocol TCP) to this host. The firewall section below has the exact rules. + +**2. Provide a bootstrap peerlist.** + +The node persists its peerlist in the `node_state` volume at `demos_peerlist.json`. On a brand-new node this file is empty (only the node itself), so it cannot discover other peers. To bootstrap, write one or more known peer URLs into that volume before first start: ```bash -# Install all dependencies (requires Rust/Cargo for wstcp) -./scripts/install-deps.sh +# Create a bootstrap peerlist on your host +cat > /tmp/demos_peerlist.json < **Note:** The install script requires [Rust](https://rustup.rs/) to be installed. It will install the `wstcp` tool needed for TLSNotary WebSocket proxying. +Replace `bootstrap1.demos.network` etc. with current bootstrap nodes from the team. The node will merge any peers it learns from these into the file as it runs. + +### Network Exposure + +When running on a VPS or any machine you want peers to connect to, you must allow inbound connections on these ports: -### 5. Run Node and Generate Keys +| Port | Protocol | Direction | Purpose | +|------|----------|-----------|---------| +| 53550 | TCP | inbound | Node RPC (HTTP) | +| 53551 | TCP | inbound | OmniProtocol (peer-to-peer binary RPC) | +| 7047 | TCP | inbound | TLSNotary attestation server (only if you want others to attest against your notary) | + +These ports should NOT be exposed publicly: + +| Port | Why | +|------|-----| +| 5432 | Postgres — internal compose network only | +| 9090 | Node metrics — keep behind your firewall or VPN | +| 9091 | Prometheus — keep behind your firewall or VPN | +| 3000 | Grafana — only public if you want a public dashboard | + +`ufw` example for an Ubuntu VPS: ```bash -# Start both database and node for the first time, if successful, you will see your node’s private -# and public keys along with the database loaded and the node will join the consensus process -./run -# Stop the node for now so that you can edit the configuration files -Ctrl+C +sudo ufw allow 53550/tcp comment "Demos RPC" +sudo ufw allow 53551/tcp comment "Demos OmniProtocol" +sudo ufw allow 7047/tcp comment "Demos TLSNotary" +sudo ufw enable ``` -### 6. Configure the Node +If you're behind NAT (home server, dev box, etc.), forward 53550 and 53551 from your router to this host. TLSNotary forwarding is optional — only needed if you want peers to use *your* notary container. + +Don't forget to set `EXPOSED_URL` in `.env` to the address peers will reach you at (public IP, DNS name, or NAT-mapped hostname). + +### Profiles + +The compose file splits optional services into profiles so you only run what you need. + +| Profile | What it adds | +|---------|--------------| +| _(none)_ | postgres + tlsnotary + node — the minimum runnable stack | +| `monitoring` | prometheus + grafana | +| `full` | node-exporter (host CPU/RAM/disk metrics) — pair with `monitoring` | +| `neo4j` | neo4j (only needed for CGC/KYC features) | -Copy the example configuration files into working copies: +Concrete commands: ```bash -cp env.example .env -cp demos_peerlist.json.example demos_peerlist.json +# Default — everything except node-exporter and neo4j +# (monitoring is enabled by default via COMPOSE_PROFILES in .env.example) +docker compose up + +# Minimal — node + postgres + tlsnotary only, no Prometheus/Grafana +COMPOSE_PROFILES= docker compose up + +# Add host-level metrics (node-exporter) +COMPOSE_PROFILES=monitoring,full docker compose up + +# Add Neo4j (only if you actually need CGC/KYC) +COMPOSE_PROFILES=monitoring,neo4j docker compose up ``` -**Edit .env file:** The most important setting is `EXPOSED_URL`. Set it based on your setup: +### Where data lives -- Local testing: `http://localhost:53550` -- Remote machine: `http://YOUR_PUBLIC_IP:53550` -- Behind proxy: `https://demos.example.com` +State is stored in named Docker volumes, not bind mounts. They survive `docker compose down` but **not** `docker compose down -v`. -**Edit demos_peerlist.json:** Add known peers in the format: +| Volume | Holds | +|--------|-------| +| `pgdata` | PostgreSQL data directory (chain state, indexes) | +| `node_state` | Your `.demos_identity` (private key), `demos_peerlist.json`, `.tlsnotary-key`, `output/` | +| `node_data` | Bundled bootstrap data (genesis.json, evmChains, l2ps) plus chain runtime artifacts | +| `node_logs` | Node logs | +| `prometheus_data` | Prometheus TSDB | +| `grafana_data` | Grafana dashboards, users, settings | +| `neo4j_data` / `neo4j_logs` | Neo4j (only if `neo4j` profile is on) | -```json -{ - "publickey": "connectionstring" # Example: “publickey”:”http://localhost:53550” -} +Inspect a volume: + +```bash +docker volume inspect node_state ``` -For local testing, you can use your own public key (found in the “publickey” file in your node directory after the first run). +### Backup the node identity -### 7. Run Node +Your `.demos_identity` is the private key — losing it means losing your node's network identity. Back up the entire `node_state` volume: ```bash -# Start the node again if you would like to keep it running -./run +docker run --rm -v node_state:/src -v "$PWD":/dst alpine \ + tar czf /dst/node-state-backup.tar.gz -C /src . ``` +That writes `node-state-backup.tar.gz` into your current directory. To restore, do the reverse: stop the stack, recreate an empty `node_state` volume, and `tar xzf` the backup back into it. + +### View logs + +```bash +docker compose logs -f node # follow node logs +docker compose logs -f postgres # follow postgres logs +docker compose logs -f tlsnotary # follow tlsnotary logs +docker compose logs -f # follow everything +``` + +### Connect to the RPC + +```bash +curl http://localhost:53550/ # liveness check (root) +curl http://localhost:53550/info # node info (version, identity, etc.) +``` + +### Update the node + +```bash +git pull +docker compose up -d --build +``` + +The `--build` rebuilds the node image against the new source. Volumes (and therefore your identity, chain data, peerlist cache) are preserved. + +### Stop the node + +```bash +docker compose down # stop containers, KEEP volumes +docker compose down -v # stop AND DELETE all volumes (nuclear) +``` + +### Wipe everything + +```bash +docker compose down -v +``` + +This deletes your identity, chain data, Prometheus history, Grafana dashboards — everything. Only do this if you actually want to start from scratch. + +### Troubleshooting + +**TLSNotary feature not initializing** +> In docker mode (the default) no signing key is required — the sidecar manages its own. If you see initialization warnings, check that the `tlsnotary` container is reachable: `curl http://localhost:7047/info` should return JSON. If you switched to `TLSNOTARY_MODE=ffi`, you need a 64-char hex `TLSNOTARY_SIGNING_KEY` in `.env` (or leave it empty and the node auto-generates one in `.tlsnotary-key`). + +**EXPOSED_URL warning at startup** +> `⚠️ EXPOSED_URL is set to a loopback/unroutable address` means peers can't reach you. Fine for local dev. For real deployment set `EXPOSED_URL=http://YOUR_PUBLIC_IP:53550` in `.env`. + +**Port 53550 already in use** +> Set `RPC_PORT` (and optionally `EXPOSED_URL`) in `.env` to a free port, then `docker compose up -d` again. + +**Postgres connection refused / node fails to connect to DB** +> Postgres takes a few seconds to become healthy on first boot. The node has a `depends_on: condition: service_healthy` so this should be automatic — but if it persists, check `docker compose logs postgres` for crash loops or auth failures. + +**Out of disk** +> Volumes grow over time. Inspect with `docker system df -v`. To reclaim space without losing identity, only prune images: `docker image prune -af`. To reclaim everything: `docker compose down -v` (destructive). + +**`docker compose: command not found`** +> You have the legacy Python `docker-compose` instead of the v2 plugin. Install the plugin per [https://docs.docker.com/compose/install/](https://docs.docker.com/compose/install/). + +**Grafana password change doesn't take effect** +> Grafana persists the password in `grafana_data` after first boot. To change it, either edit it in the Grafana UI, or `docker compose down && docker volume rm grafana_data && docker compose up -d` (resets dashboards too). + --- -## 🔧 Full Installation Guide +## Track 2: Bare metal with `./run` -Start by installing a Debian Linux distro like Ubuntu: [https://ubuntu.com/download/desktop](https://ubuntu.com/download/desktop) +This is the original install path. The node binary runs natively on the host via Bun; only Postgres runs in a sidecar container managed by the `./run` script. Pick this track if you want host-native execution, are doing kernel-level debugging, or just prefer it. -### 1. Install Prerequisites +> Note: `./run` at the repo root is a thin wrapper that forwards to `scripts/run` (the actual implementation). Always invoke `./run` from the repo root — never call `scripts/run` directly. The wrapper exists so the legacy command path keeps working. -Open a terminal and enter: +### 1. Install Prerequisites ```bash sudo apt update && sudo apt upgrade -y @@ -127,6 +298,8 @@ sudo apt install -y curl git wget build-essential ca-certificates gnupg lsb-rele ### 2. Install Docker +The `./run` script still uses Docker for the Postgres sidecar. + ```bash # Remove old Docker versions sudo apt remove docker docker-engine docker.io containerd runc @@ -184,9 +357,7 @@ curl -fsSL https://bun.sh/install | bash bun -v ``` -### Demos Node Installation - -#### 1. Clone Repository +### 4. Clone Repository ```bash cd ~ @@ -197,7 +368,7 @@ cd node git branch ``` -#### 2. Install Dependencies +### 5. Install Dependencies ```bash # Install all dependencies (requires Rust/Cargo for wstcp) @@ -211,28 +382,24 @@ git branch > source ~/.cargo/env > ``` -## 🎯 Starting and Configuring the Node - -### 1. Start the Node - -You can start the node using the `run` script shown below. +### 6. First Run — Generate Keys ```bash ./run ``` -Running the node the first time will generate a private key for your node and store it in the `.demos_identity` file by default. The public key for your node is printed on the terminal when the node runs and is also saved in a `publickey_*` file in the same directory. +Running the node the first time will generate a private key for your node and store it in the `.demos_identity` file by default. The public key is printed on the terminal and saved in a `publickey_*` file in the same directory. + +Press `Ctrl+C` (or `Q` in the TUI) to stop the node so you can edit the configuration files. ### Run script usage ``` -🚀 Demos Network Node Runner +Demos Network Node Runner USAGE: ./run [OPTIONS] -Welcome to Demos Network! This script helps you run your blockchain node easily. - OPTIONS: -p Node port (default: 53550) -d PostgreSQL port (default: 5332) @@ -263,168 +430,129 @@ EXAMPLES: # -d: database port (default 5332) ``` -### 2. Check Node Status - -In a new terminal window: - -```bash -# Check if node is running -curl http://localhost:53550 - -# Check ports -sudo lsof -i :53550 # Node port -sudo lsof -i :5332 # Database port - -# Check Docker containers -docker ps - -# Stop the node for now so that you can edit the configuration files -Ctrl+C -``` - -### 3. Further configuration +### 7. Configure the Node -The `.env` and `demos_peerlist.json` files are used to configure the demos node. Copy the templates using the command below: +Copy the templates and edit them: ```bash -cp env.example .env +cp .env.example .env cp demos_peerlist.json.example demos_peerlist.json ``` -### 4. Edit Configuration Files - -```bash -nano .env -``` - -Set the following variable: +For the bare-metal path, override these in `.env`: -- **EXPOSED_URL:** Your node's public URL - - Local testing: `http://localhost:53550` - - Remote server: `http://YOUR_PUBLIC_IP:53550` - - Behind proxy: `https://your-domain.com` +- `PG_HOST=localhost` +- `PG_PORT=5332` (the host-mapped port the `./run` Postgres sidecar uses) +- `TLSNOTARY_HOST=localhost` -### 5. Node Identity - Public and Private Keys +Set `EXPOSED_URL` based on your setup: -After running the node for the first time your keypair will be generated. You should find the `publickey_*` and `.demos_identity` files inside the node directory. +- Local testing: `http://localhost:53550` +- Remote machine: `http://YOUR_PUBLIC_IP:53550` +- Behind proxy: `https://demos.example.com` -The public key file contains your public key, this can be shared and utilized in your node. The `.demos_identity` is your private key, **KEEP THIS PRIVATE**. Back up both public and private keys. +Set `TLSNOTARY_SIGNING_KEY`, or set `TLSNOTARY_ENABLED=false` to skip TLSNotary. -### 6. Joining a network +### 8. Joining a Network -To join a network, you can edit the `demos_peerlist.json` file to add known peers in the format: +Edit `demos_peerlist.json` to add known peers: ```jsonc { - "publickey": "connectionstring", // Example: "0xd0b2be2cb6d...": "http://otherpeer.localhost" + "publickey": "connectionstring" // Example: "0xd0b2be2cb6d...": "http://otherpeer.localhost" } ``` > [!IMPORTANT] > When joining a network, please make sure your node's exposed URL is accessible by the other nodes. If they can't access it, your node won't be able to participate in the consensus. -### 7. Start the Node Again +### 9. Start the Node ```bash -# Restart the node again with your new settings if you’d like to keep it running ./run ``` -## ✅ Verification - -### Check Node Status +### Verify In a new terminal: ```bash -# Check if node is running +# Liveness curl http://localhost:53550 +curl http://localhost:53550/info -# Check ports -sudo lsof -i :53550 # Node port -sudo lsof -i :5332 # Database port - -# Use different ports if defaults are busy -./run -p 53551 -d 5333 -# -p: node port (default 53550) -# -d: database port (default 5332) +# Ports +sudo lsof -i :53550 # Node port +sudo lsof -i :5332 # Database port -# Check Docker containers +# Containers (Postgres sidecar) docker ps ``` -### View Logs - -The node will output logs showing: - -- Database connection status -- RPC server initialization -- Your node's public key -- Peer connection attempts - ### Stopping the Node ```bash -# Press Ctrl+C in the terminal running the node +# Press Ctrl+C in the terminal running the node (or `Q` in the TUI) -# Stop database +# Stop the Postgres sidecar cd postgres_5332 ./stop.sh ``` -## 🛠️ Troubleshooting +### Bare-metal Troubleshooting -### Port Already in Use +**Port already in use** ```bash -# Check what's using the port sudo lsof -i :5332 - -# Use different port ./run -d 5333 ``` -### Docker Permission Issues +**Docker permission issues** ```bash sudo usermod -aG docker $USER newgrp docker ``` -### Database Connection Timeout +**Database connection timeout** ```bash # Restart Docker sudo systemctl restart docker -# Clean and restart +# Clean and restart the postgres sidecar cd postgres_5332 ./clean.sh ./start.sh ``` -### Missing Dependencies +**Missing dependencies** ```bash rm -rf node_modules bun.lockb bun install ``` -## 🔒 Security Notes +--- + +## Security Notes 1. **Backup your identity files:** - - `.demos_identity` (private key - KEEP SECRET) - - `public.key` (public identifier) + - `.demos_identity` (private key — KEEP SECRET) + - `publickey_*` (public identifier) + + Track 1 users: see the volume backup snippet above. Track 2 users: copy the files out of the node directory. -2. **Set proper permissions:** +2. **Set proper permissions on bare metal:** ```bash chmod 600 .demos_identity ``` -3. **Never share your private key** +3. **Never share your private key.** -## 🌐 Network Information +## Network Information > **Note:** These are the default ports. If you have modified any port settings in your `.env` file or run script flags, make sure to open those custom ports instead. @@ -446,14 +574,13 @@ bun install | 3000 | Grafana | Dashboard UI (monitoring stack) | | 5332 | PostgreSQL | Database (local only, do not expose) | -- Logs directory: `logs_53550_demos_identity/` - Configuration: `.env` and `demos_peerlist.json` -## ➡️ Next Steps +## Next Steps Once your node is running: -1. Note your public key from the console output -2. Share your connection string with other node operators to form a network -3. Monitor the logs for successful peer connections -4. Check the Demos Network documentation for updates +1. Note your public key from the console output (or from the `publickey_*` file). +2. Share your connection string with other node operators to form a network. +3. Monitor the logs (`docker compose logs -f node` for Track 1, terminal output or `logs/` for Track 2) for successful peer connections. +4. Check the Demos Network documentation for updates. diff --git a/README.md b/README.md index 8fa25daba..103f08dde 100644 --- a/README.md +++ b/README.md @@ -31,23 +31,64 @@ Demos is defined by the Yellowpaper publicly available in [its own repository](h ## Installation -For detailed installation instructions, please refer to [INSTALL.md](INSTALL.md). The installation guide covers: +Docker Compose is now the recommended way to run a Demos node. For full installation instructions — both the Docker path and the bare-metal `./run` path — see [INSTALL.md](INSTALL.md). The guide covers: -- System prerequisites and dependencies -- Docker and container setup +- Docker Compose quickstart (recommended) +- Bare-metal install with Bun + Postgres (alternative) - Node configuration and key generation - Network peer configuration - Troubleshooting common issues -## Quick Start +## Quick Start (Docker) -1. Install prerequisites (Docker, Bun runtime) -2. Clone this repository -3. Install dependencies with `bun install` -4. Configure your node settings -5. Run `./run` to start the node +```bash +git clone https://github.com/kynesyslabs/node.git && cd node +cp .env.example .env # defaults are fine; edit only if you want to override +docker compose up +``` + +Once the stack is healthy: RPC at http://localhost:53550 (try `curl http://localhost:53550/info`) and Grafana at http://localhost:3000 (default `admin` / `demos`). + +See [INSTALL.md](INSTALL.md) for profiles, env vars, volumes, upgrades, and troubleshooting. + +## Publishing the Image + +The compose file uses `${IMAGE_NAME}:${IMAGE_TAG}` (default `demos-node:local`) for the node service, so the same compose can either build locally or pull from a registry by switching `.env`. + +**Build and tag for a registry:** + +```bash +# Pick your registry coordinates +export IMAGE_NAME=ghcr.io/kynesyslabs/node # or docker.io//demos-node, etc. +export IMAGE_TAG=v0.9.8 # or git sha, or 'latest' + +docker build -t "$IMAGE_NAME:$IMAGE_TAG" . +docker push "$IMAGE_NAME:$IMAGE_TAG" +``` + +**Multi-arch (recommended for public registries):** + +```bash +docker buildx build \ + --platform linux/amd64,linux/arm64 \ + -t "$IMAGE_NAME:$IMAGE_TAG" \ + --push . +``` + +**Pull on a target host** — write `IMAGE_NAME` and `IMAGE_TAG` into that host's `.env`, then: + +```bash +docker compose pull node +docker compose up -d +``` + +For private registries, run `docker login ` first. + +## Advanced / Bare-Metal Run -For complete step-by-step instructions, see [INSTALL.md](INSTALL.md). +If you'd rather run the node directly on the host (no Docker for the node itself), the legacy `./run` shell script is still supported. It installs Bun, runs Postgres in a sidecar container, and starts the node natively — useful for development, debugging, and TUI-based operation. + +See [INSTALL.md → Track 2: Bare metal with `./run`](INSTALL.md) for the full walkthrough. ## Terminal User Interface (TUI) @@ -81,32 +122,11 @@ For debugging and development, you can disable the TUI and use traditional scrol This provides linear console output that can be easily piped, searched with grep, or redirected to files. -## Monitoring with Prometheus & Grafana - -The node includes a full monitoring stack with Prometheus metrics and pre-built Grafana dashboards. +## Monitoring -### Enabling Metrics +Prometheus + Grafana are part of the unified compose (default profile). Once `docker compose up` is healthy, Grafana is at http://localhost:3000 (`admin` / `demos`) with pre-provisioned dashboards. Configuration knobs (`GRAFANA_ADMIN_PASSWORD`, `PROMETHEUS_RETENTION`, etc.) live in `.env` — see [INSTALL.md → Track 1](INSTALL.md) for the full list. -Metrics are enabled by default. To configure, add to your `.env` file: - -```env -METRICS_ENABLED=true -METRICS_PORT=9090 -``` - -The node will expose metrics at `http://localhost:9090/metrics`. - -### Starting the Monitoring Stack - -```bash -cd monitoring -docker compose up -d -``` - -**Access Grafana**: http://localhost:3000 -**Default credentials**: admin / demos - -### Available Metrics +Available node-side metrics (scraped at `node:9090/metrics`): | Metric | Description | | ----------------------------------- | ----------------------- | @@ -117,25 +137,7 @@ docker compose up -d | `demos_system_memory_usage_percent` | Memory utilization | | `demos_service_docker_container_up` | Container health status | -### Configuration - -The node and monitoring stack are configurable via environment variables: - -**Node metrics (in `.env`):** -| Variable | Default | Description | -|----------|---------|-------------| -| `METRICS_ENABLED` | `true` | Enable/disable metrics endpoint | -| `METRICS_PORT` | `9090` | Node metrics endpoint port | - -**Monitoring stack (in `monitoring/.env`):** -| Variable | Default | Description | -|----------|---------|-------------| -| `PROMETHEUS_PORT` | `9091` | Prometheus server port | -| `GRAFANA_PORT` | `3000` | Grafana dashboard port | -| `GRAFANA_ADMIN_PASSWORD` | `demos` | Grafana admin password | -| `PROMETHEUS_RETENTION` | `15d` | Data retention period | - -For detailed monitoring documentation, see [monitoring/README.md](monitoring/README.md). +For dashboard internals and customization, see [monitoring/README.md](monitoring/README.md). ## Technology Stack From 0ed66dba4363e1e15fdb842a9ebd1c75c699ef8a Mon Sep 17 00:00:00 2001 From: tcsenpai Date: Thu, 30 Apr 2026 13:03:18 +0200 Subject: [PATCH 03/11] honor TLSNOTARY_HOST in docker-mode notary lookup The docker-mode startup hardcoded http://localhost:${port}/info when checking the notary container, which fails inside docker compose (localhost in the node container is not where the tlsnotary container is). Add a host field to TLSNotaryServiceConfig, read TLSNOTARY_HOST from env (default 'localhost' for bare-metal compatibility), and use it when building the /info URL. Also updates the failure-mode error log to point at the unified 'docker compose up -d tlsnotary' path in addition to the legacy standalone 'cd tlsnotary && docker compose up' path. --- src/features/tlsnotary/TLSNotaryService.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/features/tlsnotary/TLSNotaryService.ts b/src/features/tlsnotary/TLSNotaryService.ts index 27e533b8f..3326e1d7f 100644 --- a/src/features/tlsnotary/TLSNotaryService.ts +++ b/src/features/tlsnotary/TLSNotaryService.ts @@ -36,6 +36,8 @@ export type TLSNotaryMode = "ffi" | "docker" * Service configuration options */ export interface TLSNotaryServiceConfig { + /** Hostname of the notary in docker mode. Defaults to 'localhost'. */ + host?: string /** Port to run the notary WebSocket server on */ port: number /** 32-byte secp256k1 private key (hex string or Uint8Array) - only used in FFI mode */ @@ -206,6 +208,7 @@ export function getConfigFromEnv(): TLSNotaryServiceConfig | null { } return { + host: process.env.TLSNOTARY_HOST ?? "localhost", port: parseInt(process.env.TLSNOTARY_PORT ?? "7047", 10), signingKey, maxSentData: parseInt( @@ -439,14 +442,15 @@ export class TLSNotaryService { private async startDockerMode(): Promise { const debug = isTLSNotaryDebug() const fatal = isTLSNotaryFatal() + const host = this.config.host ?? "localhost" log.info( - `[TLSNotary] Docker mode: checking container on port ${this.config.port}...`, + `[TLSNotary] Docker mode: checking container at ${host}:${this.config.port}...`, ) try { // Try to fetch /info endpoint to verify container is running - const infoUrl = `http://localhost:${this.config.port}/info` + const infoUrl = `http://${host}:${this.config.port}/info` const response = await fetch(infoUrl, { signal: AbortSignal.timeout(5000), }) @@ -477,11 +481,14 @@ export class TLSNotaryService { const message = error instanceof Error ? error.message : String(error) log.error( - `[TLSNotary] Failed to connect to Docker notary on port ${this.config.port}: ${message}`, + `[TLSNotary] Failed to connect to Docker notary at ${host}:${this.config.port}: ${message}`, ) log.error("[TLSNotary] Make sure the Docker container is running:") log.error( - "[TLSNotary] cd tlsnotary && TLSNOTARY_PORT=${TLSNOTARY_PORT} docker compose up -d", + "[TLSNotary] docker compose up -d tlsnotary (root compose)", + ) + log.error( + "[TLSNotary] cd tlsnotary && docker compose up -d (legacy standalone)", ) if (fatal) { From 28de3a378612454908f4078688d9a630eb33e7f1 Mon Sep 17 00:00:00 2001 From: tcsenpai Date: Thu, 30 Apr 2026 13:03:27 +0200 Subject: [PATCH 04/11] warn on boot when EXPOSED_URL is a loopback address MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operators frequently leave EXPOSED_URL at the .env.example default (http://localhost:53550), which is fine for local dev but silently makes the node unreachable to peers in real deployments. Add a multi-line warning logged right after 'Our connection string is:' when the URL's host is one of: localhost, 127.0.0.1, 0.0.0.0, ::1, or host.docker.internal. The check is wrapped in try/catch so a malformed URL doesn't crash the node — it just skips the warning. --- src/index.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/index.ts b/src/index.ts index 64f0aeaa9..59ecd05b5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -329,6 +329,33 @@ async function preMainLoop() { const ourselves = "http://127.0.0.1:" + indexState.SERVER_PORT getSharedState.connectionString = ourselves log.info("Our connection string is: " + ourselves) + // REVIEW: Warn operators when EXPOSED_URL points at a loopback/unroutable + // host so they don't silently run an unreachable node on the public network. + try { + const exposedUrl = Config.getInstance().core.exposedUrl + const loopbackHosts = new Set([ + "localhost", + "127.0.0.1", + "0.0.0.0", + "::1", + "host.docker.internal", + ]) + const exposedHost = new URL(exposedUrl).hostname + if (loopbackHosts.has(exposedHost)) { + log.warning( + "\n============================================================\n" + + "⚠️ EXPOSED_URL is set to a loopback/unroutable address:\n" + + ` ${exposedUrl}\n` + + " Other peers cannot reach this node at this address.\n" + + " For real network participation, set EXPOSED_URL in .env\n" + + " to your public IP or DNS name (e.g. http://YOUR_IP:53550).\n" + + " See INSTALL.md → \"Joining the network\".\n" + + "============================================================", + ) + } + } catch { + // Malformed EXPOSED_URL — skip the warning rather than crashing the node. + } // And saves the public key file await fs.promises.writeFile( "publickey_" + getSharedState.signingAlgorithm + "_" + publicKeyHex, From b8af3f3acb0f3e9fa2de82aa407e59506fbdccbb Mon Sep 17 00:00:00 2001 From: tcsenpai Date: Thu, 30 Apr 2026 13:03:35 +0200 Subject: [PATCH 05/11] move prettier, tsx, npm-check-updates to devDependencies These three packages were in dependencies but are not imported anywhere in src/. They're operator/dev tools (formatter, TS runner shebang for utility scripts, dep-update CLI). Moving them to devDependencies keeps them available for development but lets the production Docker image skip them via 'bun install --production' in a future iteration. Combined with the node_modules prune step in the Dockerfile, this contributes to roughly a 460 MB image size reduction (2.51 GB -> 2.05 GB). --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index d52848fc3..27bf99cdf 100644 --- a/package.json +++ b/package.json @@ -78,9 +78,12 @@ "eslint": "^8.57.1", "jest": "^29.7.0", "knip": "^5.74.0", + "npm-check-updates": "^16.14.18", + "prettier": "3.8.0", "ts-jest": "^29.3.2", "ts-node": "^10.9.1", "ts-node-dev": "^2.0.0", + "tsx": "^3.12.8", "typescript": "^5.9.3" }, "dependencies": { @@ -128,12 +131,10 @@ "node-fetch": "2", "node-forge": "^1.3.3", "node-seal": "^5.1.3", - "npm-check-updates": "^16.14.18", "ntp-client": "^0.5.3", "object-sizeof": "^2.6.3", "pg": "^8.12.0", "poseidon-lite": "^0.3.0", - "prettier": "3.8.0", "prom-client": "^15.1.3", "reflect-metadata": "^0.1.13", "rijndael-js": "^2.0.0", @@ -147,7 +148,6 @@ "superdilithium": "^2.0.6", "terminal-kit": "^3.1.1", "tsconfig-paths": "^4.2.0", - "tsx": "^3.12.8", "tweetnacl": "^1.0.3", "typeorm": "^0.3.17", "web3": "^4.16.0", From 5e847f512aad3304daf37ddf8bd872ff03145833 Mon Sep 17 00:00:00 2001 From: tcsenpai Date: Thu, 30 Apr 2026 13:04:11 +0200 Subject: [PATCH 06/11] gitignore TEAM.md alongside AGENTS.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TEAM.md is the source-of-truth for the local /teammode behavioral overlay used with Claude Code. It pairs with AGENTS.md (already ignored) — both should stay local rather than become repo content. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7f28a0403..da83bacac 100644 --- a/.gitignore +++ b/.gitignore @@ -63,6 +63,7 @@ history/tmp/ # ---- AI Tooling (local-only) ---- CLAUDE.md AGENTS.md +TEAM.md mcp.json .serena/ .serena-backup/ From e0ef1869283cb7cd86846e45cfe2032641dc8729 Mon Sep 17 00:00:00 2001 From: tcsenpai Date: Thu, 30 Apr 2026 13:20:56 +0200 Subject: [PATCH 07/11] harden Dockerfile per security review (read-only app, allow-list COPY) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two security findings from automated review: 1. The runtime COPY at the runtime stage previously assigned write permissions on the entire /app tree to the demos user. A runtime exploit could rewrite the app's own source or node_modules. Now the application tree is owned by root and group-readable for demos only — the runtime user gets write access exclusively on /app itself (where the entrypoint creates symlinks) and the three volume mount points (/app/data, /app/logs, /app/state). 2. The build stage previously did 'COPY . .' which is permissive — any new top-level file added later silently rides into the image, and host-only files (run wrapper, captraf.sh, jest.config.ts, etc.) were already being copied even though .dockerignore filters most. Replaced with an explicit allow-list: src/ scripts/ data/ sdk/ libs/ tsconfig.json bunfig.toml ormconfig.json plus package.json + bun.lock from the earlier dep-resolution layer. Verified: build succeeds, all five services healthy, all endpoints 200, no EACCES errors at runtime, demos user cannot write to /app/src/. --- Dockerfile | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/Dockerfile b/Dockerfile index 39b824c41..c90b2e0d9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -43,8 +43,15 @@ RUN bun install --frozen-lockfile \ # when the lockfile considers them optional peers. RUN bun add bufferutil utf-8-validate -# Bring in the rest of the source tree. -COPY . . +# Bring in the runtime payload. Explicit allow-list (rather than `COPY . .`) +# so host-only files (run wrapper, captraf.sh, jest config, etc.) and any +# new top-level files added later don't silently land in the image. +COPY src/ ./src/ +COPY scripts/ ./scripts/ +COPY data/ ./data/ +COPY sdk/ ./sdk/ +COPY libs/ ./libs/ +COPY tsconfig.json bunfig.toml ormconfig.json ./ # Patch falcon-sign so uncaught WASM exceptions are logged before rethrow. # Mirrors scripts/run::patch_falcon_sign. Idempotent and tolerant of a @@ -148,16 +155,21 @@ RUN groupmod --new-name demos bun \ WORKDIR /app -# Copy the built tree from the builder stage with correct ownership in one go. -COPY --from=builder --chown=demos:demos /app /app - -# WORKDIR /app is owned by root by default. Hand it (and the volume mount -# points) to demos so the node can create .demos_identity, demos_peerlist.json, -# log files, and chain data on first boot. Empty named volumes that mount -# over these dirs inherit the directory ownership at first mount. -RUN chown demos:demos /app \ +# Copy the built tree from the builder stage. Defense-in-depth: the +# application's source and node_modules are owned by root with no +# write permission for the demos runtime user, so a code-execution +# exploit at runtime cannot rewrite the app itself. The demos user +# only gets write access on: +# - /app (entrypoint creates symlinks here) +# - /app/data, logs, state (volume mount points) +COPY --from=builder /app /app +RUN chown -R root:demos /app \ + && find /app -mindepth 1 -type d -exec chmod 0755 {} + \ + && find /app -type f -exec chmod 0644 {} + \ + && chmod 0755 /app/scripts/docker-entrypoint.sh \ && mkdir -p /app/data /app/logs /app/state \ - && chown demos:demos /app/data /app/logs /app/state + && chown demos:demos /app /app/data /app/logs /app/state \ + && chmod 0755 /app /app/data /app/logs /app/state # Sensible image-level defaults. Anything else (DATABASE_URL, EXPOSED_URL, # IDENTITY_FILE, PEER_LIST_FILE, etc.) must be supplied at runtime. From 791a9076d89f88809ec29ff0fab912343349053a Mon Sep 17 00:00:00 2001 From: tcsenpai Date: Thu, 30 Apr 2026 15:21:16 +0200 Subject: [PATCH 08/11] fix port mapping mismatch and pin volume names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port mapping: every published port previously had the host side configurable but the container side hardcoded, e.g. '${RPC_PORT:-53550}:53550'. Setting RPC_PORT=53560 in .env routed host port 53560 to container port 53550 where nothing listened. Both sides now use the variable. METRICS_PORT is special: prometheus.yml has a static node:9090 scrape target. Pinning the container-internal METRICS_PORT to 9090 keeps that valid; a new METRICS_HOST_PORT env var configures the host side independently. Volume names: every named volume in docker-compose.yml gets an explicit name: demos_ field. Without this, Compose prefixes volumes with the project name (the working directory, here 'node'), producing 'node_node_state' etc. — but the docs reference plain 'node_state'. Operators following the seeding command in INSTALL.md silently created an empty anonymous volume and seeded it instead of the real one. Docs updated to use the new demos_* names everywhere, including the bootstrap-peerlist seeding command and the backup procedure. Run-script usage block in INSTALL.md gets a 'text' language tag on its fenced code block to satisfy linters. --- .env.example | 7 +++++++ INSTALL.md | 36 +++++++++++++++++++----------------- docker-compose.yml | 33 +++++++++++++++++++++++++++------ 3 files changed, 53 insertions(+), 23 deletions(-) diff --git a/.env.example b/.env.example index 52e4a7554..9a406bb5e 100644 --- a/.env.example +++ b/.env.example @@ -71,8 +71,15 @@ PEER_LIST_FILE=demos_peerlist.json # Exposes Prometheus metrics at http://:/metrics METRICS_ENABLED=true +# Container-internal port. Pinned to 9090 in compose to match the static +# scrape target in monitoring/prometheus/prometheus.yml. Override only +# when running on bare metal (./run path). METRICS_PORT=9090 METRICS_HOST=0.0.0.0 +# Host-mapped port for the metrics endpoint (compose only). Set this if +# 9090 is already in use on the host — Prometheus inside the compose +# network is unaffected. +METRICS_HOST_PORT=9090 # === TLSNOTARY ============================================================== diff --git a/INSTALL.md b/INSTALL.md index bad6c8d6b..3ab3d6d4b 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -106,7 +106,7 @@ If running behind NAT, port-forward `53550` (RPC) and `53551` (OmniProtocol TCP) **2. Provide a bootstrap peerlist.** -The node persists its peerlist in the `node_state` volume at `demos_peerlist.json`. On a brand-new node this file is empty (only the node itself), so it cannot discover other peers. To bootstrap, write one or more known peer URLs into that volume before first start: +The node persists its peerlist in the `demos_node_state` volume at `demos_peerlist.json`. On a brand-new node this file is empty (only the node itself), so it cannot discover other peers. To bootstrap, write one or more known peer URLs into that volume before first start: ```bash # Create a bootstrap peerlist on your host @@ -115,9 +115,11 @@ cat > /tmp/demos_peerlist.json < You have the legacy Python `docker-compose` instead of the v2 plugin. Install the plugin per [https://docs.docker.com/compose/install/](https://docs.docker.com/compose/install/). **Grafana password change doesn't take effect** -> Grafana persists the password in `grafana_data` after first boot. To change it, either edit it in the Grafana UI, or `docker compose down && docker volume rm grafana_data && docker compose up -d` (resets dashboards too). +> Grafana persists the password in `demos_grafana_data` after first boot. To change it, either edit it in the Grafana UI, or `docker compose down && docker volume rm demos_grafana_data && docker compose up -d` (resets dashboards too). --- @@ -394,7 +396,7 @@ Press `Ctrl+C` (or `Q` in the TUI) to stop the node so you can edit the configur ### Run script usage -``` +```text Demos Network Node Runner USAGE: diff --git a/docker-compose.yml b/docker-compose.yml index 6b878e662..37c8f08ce 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -108,7 +108,10 @@ services: PEER_LIST_FILE: ${PEER_LIST_FILE:-demos_peerlist.json} # Monitoring METRICS_ENABLED: ${METRICS_ENABLED:-true} - METRICS_PORT: ${METRICS_PORT:-9090} + # METRICS_PORT is pinned to 9090 inside the container so the + # prometheus.yml scrape target (node:9090) stays valid. Use + # METRICS_HOST_PORT in the ports stanza to remap on the host. + METRICS_PORT: 9090 METRICS_HOST: ${METRICS_HOST:-0.0.0.0} # TLSNotary — service name from this compose TLSNOTARY_ENABLED: ${TLSNOTARY_ENABLED:-true} @@ -136,11 +139,17 @@ services: - node_logs:/app/logs # all node logs - node_state:/app/state # identity, peerlist, tlsnotary key, output/ ports: - - "${RPC_PORT:-53550}:53550" - - "${OMNI_PORT:-53551}:53551" - - "${RPC_SIGNALING_PORT:-3005}:3005" - - "${RPC_MCP_PORT:-3001}:3001" - - "${METRICS_PORT:-9090}:9090" + # Both sides of each mapping must use the same variable, otherwise + # overriding RPC_PORT (etc.) routes the host port to a container + # port where nothing is listening. + - "${RPC_PORT:-53550}:${RPC_PORT:-53550}" + - "${OMNI_PORT:-53551}:${OMNI_PORT:-53551}" + - "${RPC_SIGNALING_PORT:-3005}:${RPC_SIGNALING_PORT:-3005}" + - "${RPC_MCP_PORT:-3001}:${RPC_MCP_PORT:-3001}" + # METRICS_PORT — keep container port pinned to 9090 so the + # prometheus.yml scrape target (node:9090) stays valid. The + # host-side port is independently configurable via METRICS_HOST_PORT. + - "${METRICS_HOST_PORT:-9090}:9090" networks: - demos-network @@ -264,11 +273,23 @@ networks: driver: bridge volumes: + # Explicit `name:` overrides Compose's project-name prefix so the volumes + # have predictable identifiers that match what the docs reference. + # Without this, `node_state` would actually be `_node_state`, + # silently breaking seeding commands like `docker run -v node_state:/state`. pgdata: + name: demos_pgdata node_data: + name: demos_node_data node_logs: + name: demos_node_logs node_state: + name: demos_node_state prometheus_data: + name: demos_prometheus_data grafana_data: + name: demos_grafana_data neo4j_data: + name: demos_neo4j_data neo4j_logs: + name: demos_neo4j_logs From 1de5026933b575714a1f11fbdba5584ed3112168 Mon Sep 17 00:00:00 2001 From: tcsenpai Date: Thu, 30 Apr 2026 15:21:35 +0200 Subject: [PATCH 09/11] harden Dockerfile install step, bump bun base, fix size regression Three Dockerfile fixes: 1. 'bun install --frozen-lockfile && bun pm trust --all || true' parses as '(install && trust) || true', meaning a failed install would also be silently swallowed and ride into the image. Split into two RUN steps so '|| true' only tolerates trust failures. 2. Base image bumped from oven/bun:1.2-debian to oven/bun:1.3-debian. The host bun is 1.3 and writes bun.lock with a configVersion field that bun 1.2 doesn't understand. Container builds were hanging indefinitely on 'bun install --frozen-lockfile' because 1.2 couldn't reconcile the newer lockfile format. 3. The earlier 'chown -R + recursive chmod' pass for permission hardening duplicated the entire /app tree into a new layer (image grew to 4.57 GB). Moved ownership into COPY --chown so it shares the COPY layer; the only RUN step left is the small chmod for the entrypoint and the writable mount points. Image is back to 2.06 GB with the same security model: source tree owned root:demos read-only, only data/, logs/, state/, and /app itself writable by the demos user. Verified: build succeeds, all 5 services healthy, all endpoints 200, identity persists across docker compose down + up, demos user cannot write to /app/src/. --- Dockerfile | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/Dockerfile b/Dockerfile index c90b2e0d9..9c0b76764 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,7 @@ # Installs build toolchain, resolves dependencies, compiles native modules, # and patches falcon-sign to log uncaught exceptions before rethrowing. # ============================================================================= -FROM oven/bun:1.2-debian AS builder +FROM oven/bun:1.3-debian AS builder # Build-time deps for native modules (bufferutil, utf-8-validate, etc.). # python3-setuptools is required for node-gyp on newer Pythons. @@ -34,10 +34,12 @@ WORKDIR /app # Copy manifests first to maximise layer cache reuse on dep installs. COPY package.json bun.lock ./ -# Resolve all dependencies. `bun pm trust --all` runs lifecycle scripts for -# packages that explicitly opt-in (e.g. native module postinstalls). -RUN bun install --frozen-lockfile \ - && bun pm trust --all || true +# Resolve all dependencies. Split into two RUN steps so `|| true` only +# tolerates `bun pm trust` failures (some packages have no postinstall +# script to trust), not `bun install` failures — a failed install must +# fail the build, otherwise we'd build a silently broken image. +RUN bun install --frozen-lockfile +RUN bun pm trust --all || true # Native websocket accelerators - explicit install so they are not skipped # when the lockfile considers them optional peers. @@ -133,7 +135,7 @@ RUN if [ "$PRUNE_MODULES" = "true" ]; then \ # Stage 2: runtime # Minimal image with only what the node needs at run time. Runs as non-root. # ============================================================================= -FROM oven/bun:1.2-debian AS runtime +FROM oven/bun:1.3-debian AS runtime # OCI image metadata. LABEL org.opencontainers.image.source="https://github.com/kynesyslabs/node" \ @@ -162,11 +164,11 @@ WORKDIR /app # only gets write access on: # - /app (entrypoint creates symlinks here) # - /app/data, logs, state (volume mount points) -COPY --from=builder /app /app -RUN chown -R root:demos /app \ - && find /app -mindepth 1 -type d -exec chmod 0755 {} + \ - && find /app -type f -exec chmod 0644 {} + \ - && chmod 0755 /app/scripts/docker-entrypoint.sh \ +# +# COPY --chown bakes ownership into the same layer as the copy itself, +# avoiding a 2 GB chown layer that would otherwise duplicate the tree. +COPY --from=builder --chown=root:demos /app /app +RUN chmod 0755 /app/scripts/docker-entrypoint.sh \ && mkdir -p /app/data /app/logs /app/state \ && chown demos:demos /app /app/data /app/logs /app/state \ && chmod 0755 /app /app/data /app/logs /app/state From 1339cd5c97bd93e9897c5bbf51bdce23ec9117df Mon Sep 17 00:00:00 2001 From: tcsenpai Date: Thu, 30 Apr 2026 15:21:35 +0200 Subject: [PATCH 10/11] fix entrypoint output symlink and surface EXPOSED_URL parse errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Entrypoint: - /app/output handling used 'rmdir ... || true', which silently swallowed failures on a non-empty directory. ln -sfn does NOT replace a real directory — it would have created /app/output/output pointing at the volume, leaving the node writing to the ephemeral container layer. Switched to 'rm -rf' inside the [ -d ] guard so a non-empty directory is actually replaced. - Removed a no-op '[ -e "$target" ] || : > /dev/null' line that did nothing in either branch; the surrounding if-block already handles file-exists and file-absent cases correctly. src/index.ts: - The EXPOSED_URL loopback-warning code path had a bare 'catch {}' that swallowed URL parse errors silently. If an operator typed a malformed value into .env, they'd have no signal that the loopback check was being skipped. Now logs a clear warning with the offending value and the parse error; node still continues rather than crashing on a config quirk. --- scripts/docker-entrypoint.sh | 11 ++++++++--- src/index.ts | 10 ++++++++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/scripts/docker-entrypoint.sh b/scripts/docker-entrypoint.sh index 27f462811..20d500c9b 100755 --- a/scripts/docker-entrypoint.sh +++ b/scripts/docker-entrypoint.sh @@ -21,11 +21,11 @@ LOGS_DIR="${LOGS_DIR:-/app/logs}" mkdir -p "$STATE_DIR" "$LOGS_DIR" -# Files: create on volume if absent, then symlink from /app +# Files: create on volume if absent, then symlink from /app. +# The node generates these on demand on first boot — we don't pre-create. for f in .demos_identity demos_peerlist.json .tlsnotary-key; do target="$STATE_DIR/$f" link="/app/$f" - [ -e "$target" ] || : > /dev/null # do not pre-create — let node generate on demand # If /app already has a real file (e.g. baked from image), move it once if [ -e "$link" ] && [ ! -L "$link" ]; then if [ ! -e "$target" ]; then @@ -39,9 +39,14 @@ for f in .demos_identity demos_peerlist.json .tlsnotary-key; do done # Directory: output/ +# rmdir would only work on an empty dir, and 'ln -sfn TARGET /app/output' +# does NOT replace a real directory — it creates output/output inside it. +# So if /app/output exists as a non-symlink directory, blow it away first. if [ ! -L /app/output ]; then mkdir -p "$STATE_DIR/output" - [ -d /app/output ] && [ ! -L /app/output ] && rmdir /app/output 2>/dev/null || true + if [ -d /app/output ]; then + rm -rf /app/output + fi ln -sfn "$STATE_DIR/output" /app/output fi diff --git a/src/index.ts b/src/index.ts index 59ecd05b5..88acb7036 100644 --- a/src/index.ts +++ b/src/index.ts @@ -353,8 +353,14 @@ async function preMainLoop() { "============================================================", ) } - } catch { - // Malformed EXPOSED_URL — skip the warning rather than crashing the node. + } catch (err) { + // Malformed EXPOSED_URL — surface it so the operator can fix .env, + // but don't crash the node over a config quirk. + const message = err instanceof Error ? err.message : String(err) + log.warning( + `[CONFIG] EXPOSED_URL is not a valid URL — loopback check skipped. ` + + `Value: "${Config.getInstance().core.exposedUrl}". Error: ${message}`, + ) } // And saves the public key file await fs.promises.writeFile( From 40630456ccc20d21c9aa090c12ebfb5d8a064e0a Mon Sep 17 00:00:00 2001 From: tcsenpai Date: Thu, 30 Apr 2026 15:29:19 +0200 Subject: [PATCH 11/11] gate tlsnotary behind a compose profile and refresh stale docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewer flagged that COMPOSE_PROFILES=monitoring,tlsnotary was a no-op for the tlsnotary entry: the service had no 'profiles:' stanza so it always started, regardless of what the operator put in COMPOSE_PROFILES. The advertised disable knob (removing it from the list) silently did nothing. Compose changes: - tlsnotary service: profiles: [tlsnotary] - node depends_on tlsnotary now uses 'required: false' so the node still starts when the tlsnotary profile is off; pair with TLSNOTARY_ENABLED=false in .env to silence the node-side warnings - header comment in docker-compose.yml updated: profile semantics spelled out explicitly, volume names refreshed to demos_* .env.example: - COMPOSE_PROFILES comment expanded with the full menu, common pairings, and the TLSNOTARY_ENABLED pairing requirement - TLSNOTARY_ENABLED comment now flags the pairing too Doc sweep across INSTALL.md and README.md while we were here: - INSTALL.md profile table: '(none)' is now postgres + node only; added a 'tlsnotary' row with the TLSNOTARY_ENABLED pairing note; the example commands include 'tlsnotary' in COMPOSE_PROFILES where it was missing - INSTALL.md Track 2 (bare metal) section: corrected the TLSNotary signing-key explanation to be FFI-mode-specific instead of the old 'set this or set TLSNOTARY_ENABLED=false' lie - README.md Network Ports section: marked as bare-metal only, pointed docker users at INSTALL.md → Network Exposure; fixed inaccuracies (53551 is TCP not TCP/UDP; 55000-60000 applies to FFI mode only; postgres 5332 is bare-metal only) --- .env.example | 16 +++++++++++++++- INSTALL.md | 18 +++++++++++------- README.md | 18 +++++++++--------- docker-compose.yml | 32 +++++++++++++++++++++++--------- 4 files changed, 58 insertions(+), 26 deletions(-) diff --git a/.env.example b/.env.example index 9a406bb5e..354e59b47 100644 --- a/.env.example +++ b/.env.example @@ -85,6 +85,10 @@ METRICS_HOST_PORT=9090 # === TLSNOTARY ============================================================== # TLSNotary provides cryptographic attestation of HTTPS responses. +# Under docker compose this MUST track the 'tlsnotary' profile in +# COMPOSE_PROFILES below — set both to 'true'/include, or both to +# 'false'/exclude. Mixing them lets the node try to call a sidecar +# that's not running (it warns and continues, but it's noisy). TLSNOTARY_ENABLED=true # In docker compose: 'tlsnotary' (the service hostname). @@ -122,7 +126,17 @@ L2PS_ZK_ENABLED=false # These are consumed by docker-compose.yml, NOT the node binary itself. # Which compose profiles to bring up. Comma-separated. -# Common: 'monitoring,tlsnotary' — full stack. Empty = node + postgres only. +# +# Available profiles: +# tlsnotary — TLSNotary sidecar (HTTPS attestation) +# monitoring — Prometheus + Grafana +# full — Node Exporter (host CPU/RAM/disk metrics, pair with monitoring) +# neo4j — Neo4j (only for CGC/KYC features) +# +# Empty = the bare minimum (postgres + node). Common pairings: +# monitoring,tlsnotary (full stack — recommended default) +# monitoring (skip TLSNotary; ALSO set TLSNOTARY_ENABLED=false) +# monitoring,full (add host metrics) COMPOSE_PROFILES=monitoring,tlsnotary # Image coordinates for the node. Defaults to a local build. diff --git a/INSTALL.md b/INSTALL.md index 3ab3d6d4b..b16569a10 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -167,26 +167,30 @@ The compose file splits optional services into profiles so you only run what you | Profile | What it adds | |---------|--------------| -| _(none)_ | postgres + tlsnotary + node — the minimum runnable stack | +| _(none)_ | postgres + node — the bare minimum stack | +| `tlsnotary` | TLSNotary sidecar (HTTPS attestation). Pair with `TLSNOTARY_ENABLED=true` | | `monitoring` | prometheus + grafana | | `full` | node-exporter (host CPU/RAM/disk metrics) — pair with `monitoring` | | `neo4j` | neo4j (only needed for CGC/KYC features) | +`.env.example` ships with `COMPOSE_PROFILES=monitoring,tlsnotary` so a plain `docker compose up` brings up the recommended full stack. + Concrete commands: ```bash -# Default — everything except node-exporter and neo4j -# (monitoring is enabled by default via COMPOSE_PROFILES in .env.example) +# Default — postgres + node + tlsnotary + monitoring +# (uses COMPOSE_PROFILES from .env.example) docker compose up -# Minimal — node + postgres + tlsnotary only, no Prometheus/Grafana +# Minimal — only node + postgres, no sidecars +# Also set TLSNOTARY_ENABLED=false in .env to silence the node-side warnings COMPOSE_PROFILES= docker compose up # Add host-level metrics (node-exporter) -COMPOSE_PROFILES=monitoring,full docker compose up +COMPOSE_PROFILES=monitoring,tlsnotary,full docker compose up # Add Neo4j (only if you actually need CGC/KYC) -COMPOSE_PROFILES=monitoring,neo4j docker compose up +COMPOSE_PROFILES=monitoring,tlsnotary,neo4j docker compose up ``` ### Where data lives @@ -453,7 +457,7 @@ Set `EXPOSED_URL` based on your setup: - Remote machine: `http://YOUR_PUBLIC_IP:53550` - Behind proxy: `https://demos.example.com` -Set `TLSNOTARY_SIGNING_KEY`, or set `TLSNOTARY_ENABLED=false` to skip TLSNotary. +If you're using FFI mode (`TLSNOTARY_MODE=ffi`), set `TLSNOTARY_SIGNING_KEY` in `.env` to a 64-char hex secp256k1 key — or leave it empty and the node auto-generates one into `.tlsnotary-key` on first boot. To skip TLSNotary entirely, set `TLSNOTARY_ENABLED=false`. ### 8. Joining a Network diff --git a/README.md b/README.md index 103f08dde..29383dc31 100644 --- a/README.md +++ b/README.md @@ -157,20 +157,20 @@ After installation, configure your node by editing: ## Network Ports -The following ports must be open for the node to function properly. +For the docker-compose path see [INSTALL.md → Network Exposure](INSTALL.md#network-exposure) — that section has the canonical inbound/outbound rules and a `ufw` example for VPS deployments. -> **Note:** These are the default ports. If you have modified any port settings in your `.env` file or run script flags, make sure to open those custom ports instead. +For the bare-metal `./run` path: -### Required Ports +### Required Ports (bare metal) | Port | Protocol | Description | | ----------- | -------- | ------------------------------ | | 53550 | TCP | Node RPC API | -| 53551 | TCP/UDP | OmniProtocol P2P communication | -| 7047 | TCP | TLSNotary server | -| 55000-60000 | TCP/UDP | WebSocket proxy for TLSNotary | +| 53551 | TCP | OmniProtocol P2P communication | +| 7047 | TCP | TLSNotary server (FFI/docker) | +| 55000-60000 | TCP | WebSocket proxy for TLSNotary FFI mode | -### Optional Ports +### Optional Ports (bare metal) | Port | Protocol | Description | | ---- | -------- | ------------------------------------------------- | @@ -184,9 +184,9 @@ The following ports must be open for the node to function properly. ```bash # Required sudo ufw allow 53550/tcp # Node RPC -sudo ufw allow 53551 # OmniProtocol (TCP+UDP) +sudo ufw allow 53551/tcp # OmniProtocol sudo ufw allow 7047/tcp # TLSNotary -sudo ufw allow 55000:60000 # TLSNotary WS proxy (TCP+UDP) +sudo ufw allow 55000:60000/tcp # TLSNotary WS proxy (FFI mode) ``` ## Security diff --git a/docker-compose.yml b/docker-compose.yml index 37c8f08ce..b35565906 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,18 +2,24 @@ # Demos Network Node — Unified Docker Compose # ============================================================================= # One-command bring-up: -# cp .env.example .env # then set TLSNOTARY_SIGNING_KEY (or disable it) +# cp .env.example .env # docker compose up # -# Profiles: -# (default) postgres + node + tlsnotary -> minimum runnable node -# --profile monitoring add prometheus + grafana -# --profile full add node-exporter (host metrics) -# --profile neo4j add neo4j (only needed for CGC/KYC features) +# Profiles (every optional service is gated; .env.example sets COMPOSE_PROFILES +# = monitoring,tlsnotary by default so a plain `docker compose up` brings up +# the recommended full stack): # -# Persistent state lives in named volumes: -# pgdata, node_data, node_logs, node_identity, -# prometheus_data, grafana_data, neo4j_data, neo4j_logs +# No profile postgres + node -> minimum stack +# tlsnotary + tlsnotary sidecar -> notarization +# (pair with TLSNOTARY_ENABLED in .env) +# monitoring + prometheus + grafana -> dashboards +# full + node-exporter -> host metrics +# neo4j + neo4j -> CGC/KYC only +# +# Persistent state lives in named volumes (prefixed `demos_`): +# demos_pgdata, demos_node_data, demos_node_logs, demos_node_state, +# demos_prometheus_data, demos_grafana_data, demos_neo4j_data, +# demos_neo4j_logs # # Wipe everything (DANGER): docker compose down -v # ============================================================================= @@ -52,6 +58,10 @@ services: container_name: demos-tlsnotary restart: unless-stopped platform: linux/amd64 + # Profile-gated so COMPOSE_PROFILES=...,tlsnotary actually controls the + # sidecar. Drop 'tlsnotary' from COMPOSE_PROFILES to skip it; pair that + # with TLSNOTARY_ENABLED=false in .env so the node doesn't try to call it. + profiles: [tlsnotary] environment: NS_NOTARIZATION__MAX_SENT_DATA: ${TLSNOTARY_MAX_SENT_DATA:-32768} ports: @@ -86,8 +96,12 @@ services: depends_on: postgres: condition: service_healthy + # tlsnotary is profile-gated; if its profile is off, Compose drops + # this dependency silently and the node starts without it. Make sure + # to also set TLSNOTARY_ENABLED=false in .env in that case. tlsnotary: condition: service_started + required: false environment: # Network & ports RPC_PORT: ${RPC_PORT:-53550}