Skip to content

Feature/enable horizental scalability backend#5

Merged
omarmaaref merged 4 commits into
mainfrom
feature/enable_horizental_scalability_backend
Jul 19, 2026
Merged

Feature/enable horizental scalability backend#5
omarmaaref merged 4 commits into
mainfrom
feature/enable_horizental_scalability_backend

Conversation

@omarmaaref

Copy link
Copy Markdown
Owner

Summary

Changes

  • Backend:
  • Frontend:
  • Infra / CI:
  • Docs:

Testing

  • npm test in apps/backend passes
  • npx tsc --noEmit passes in both apps
  • Manually verified via the operator dashboard
  • Manually verified via the Swagger UI at /docs

Notes for reviewers

Adds an opt-in local horizontal-scale setup while leaving the plain
'npm run dev' flow untouched.

- apps/backend/Dockerfile — multi-stage Alpine build. Build stage runs
  npm ci + nest build; runtime stage installs --omit=dev deps, runs
  as the built-in 'node' user, and ships a HEALTHCHECK that curls
  /health. Final image ~150MB.
- apps/backend/.dockerignore — keeps node_modules, dist, .env, and
  Docker metadata out of the build context.
- docker-compose.yml — adds backend-1, backend-2, and nginx services
  gated behind the 'cluster' compose profile. YAML anchor deduplicates
  the two backend definitions. Both connect to the mongo service via
  'mongodb://mongodb:27017/bliq?directConnection=true' (directConnection
  skips topology discovery because rs.initiate advertises the primary
  as 'localhost:27017', which isn't resolvable from sibling containers).
  Neither backend publishes to the host — only nginx does, on 8090
  (avoiding a common Windows reservation on 8080).
- nginx/nginx.conf — round-robin load balancer with a shared upstream
  pool for HTTP and socket.io traffic. The socket.io location enables
  WebSocket upgrade via 'proxy_set_header Connection $connection_upgrade'
  driven by the map that promotes Upgrade requests to 'upgrade' and
  everything else to 'close'. Sticky sessions intentionally omitted:
  the frontend uses transports: ['websocket'] so socket.io never falls
  back to long-polling, and a live WebSocket is pinned to one upstream
  by TCP semantics regardless of the LB algorithm. Access log format
  'lb' includes upstream_addr so traffic distribution is visible in
  the logs.
- package.json — new scripts:
    cluster:up     docker compose --profile cluster up -d --wait --build
    cluster:down   docker compose --profile cluster down
    cluster:logs   follow logs across nginx + both backends
    cluster:reset  down -v then up (fresh volumes + rebuild)
Two related additions for the horizontally scaled setup.

- New HealthModule exposes GET /health returning { status: 'ok' }. Used
  by the Docker HEALTHCHECK on each backend container and available for
  any upstream load balancer that wants a readiness probe. Registered
  as the first feature module in AppModule so it's available before
  any Mongoose connection setup finishes.

- VehiclesGateway now stamps a stable instanceId on the vehicles.snapshot
  payload sent to each new client. The id is derived once at gateway
  construction time as (process.env.HOSTNAME ?? randomUUID().slice(0,8))
  — Docker sets HOSTNAME to the short container id, so in a cluster each
  replica advertises its own identity. The change event stream stays as
  the raw domain event; the client already knows the instance from the
  snapshot because a WebSocket is pinned to one upstream for its life.
  Connect/disconnect log lines gain the instanceId so container logs
  are self-identifying at a glance.
Complements the backend gateway change that now stamps an instanceId on
the initial vehicles.snapshot payload:

- types.ts adds VehiclesSnapshot ({ vehicles, instanceId }) matching the
  new WS envelope. VehicleChangedEvent stays as the raw domain event —
  the client only needs the instance once per connection.
- useFleetState tracks instanceId in state, populates it from the
  snapshot event, resets to null when the WebSocket disconnects or when
  the user toggles to polling mode. Returned as part of FleetState.
- Dashboard renders 'You are <name> · backend <id>' in the header
  whenever the socket is connected. Hidden in polling mode where there
  is no single backend answering (each poll may hit either replica).

Useful as a live indicator when the cluster profile is running two
replicas behind nginx — opening two browser sessions typically shows
two different instance ids, visibly proving the load balancer is
distributing WS handshakes.
…atency

Adds an opt-in local monitoring stack behind the 'monitoring' compose
profile. Grafana lands on :3001 with two datasources and one dashboard
provisioned from disk, so a reviewer clicks a URL and sees data — no
manual imports.

Backend
- @willsoto/nestjs-prometheus + prom-client added as deps.
- New MetricsModule (apis/metrics/) mounts GET /metrics for scraping,
  enables prom-client default metrics (Node runtime: event loop lag,
  heap, GC, file descriptors, ...), and registers a global
  HttpMetricsInterceptor that records every request's duration into an
  http_request_duration_seconds histogram labeled by method, route
  template (not raw URL — bounds cardinality), and status.
- Bucket layout (5ms .. 10s) chosen to give useful p50/p95/p99 for
  a Node HTTP workload.

Infra
- docker-compose gains 4 services under 'profiles: [monitoring]':
    prometheus  (:9090) — scrapes both backend replicas every 15s.
    loki        (:3100) — log store with filesystem backend.
    promtail    — tails Docker container logs and ships to Loki,
                  labeling by container/service name.
    grafana     (:3001) — anonymous viewer + admin/admin login.
- Each service is volume-backed for restart survival
  (prometheus_data, loki_data, grafana_data).
- monitoring/ folder holds all configs: prometheus scrape config,
  loki + promtail configs, grafana datasource + dashboard provisioning.
  Datasources have explicit uid: prometheus / uid: loki so the
  provisioned dashboard's panel references resolve reliably (Grafana
  auto-generates random UIDs otherwise).

Dashboard 'Bliq — Overview'
- HTTP latency p95 and p99 per route (histogram_quantile over
  the rate of the _bucket series)
- Request rate by status
- Node event loop lag p95
- Live backend log tail from Loki

npm scripts
- monitoring:up / monitoring:down / monitoring:reset — start / stop
  / wipe only the monitoring stack.
- observed:up / observed:down — start both 'cluster' and 'monitoring'
  profiles in one shot with --build. This is the 'give me everything'
  command.
@omarmaaref
omarmaaref merged commit e819a45 into main Jul 19, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants