Skip to content

Commit dd977fd

Browse files
committed
release: v6.17.2 — HA Phase 4 (leader election, multi-replica now safe)
Redis SET NX PX with TTL 30s + heartbeat 10s. Fires onBecomeLeader / onBecomeReader callbacks on role transition. All 13 cron jobs leader-only via _m(name, fn) leader-aware wrapper. Docker event stream start/stop on role transition. Git polling same. SSH tunnels stay per-replica (readers need them for HTTP reads; documented acceptable cost, tracked for future v7.x). Graceful shutdown releases lock via Lua DEL-if-owned — next replica picks up in milliseconds, not 30s TTL wait. Internal-reset recovery: GET-and-compare after NX fail detects we still own the lock (after module state reset) and re-claims without spurious transition. Fixes a test edge case and a real-world event-loop hiccup scenario. Standalone completely unaffected: - cluster.isLeader() short-circuits to true, no Redis traffic - onBecomeLeader(fn) fires synchronously at registration - existing cron jobs start as before Tests: 871 → 879 (+8 leader-election tests): - lock acquire + contention (NX returns null while held) - onBecomeLeader + onBecomeReader fire on transitions - idempotent transitions don't fire twice - throwing callback doesn't prevent siblings - standalone synchronous onBecomeLeader - standalone never fires onBecomeReader HA v6.17.x complete: - v6.17.0: cluster abstraction + Redis rate limiter - v6.17.1: WS pub/sub cross-replica broadcasts - v6.17.2: leader election (this) → multi-replica SAFE v7.0.0 stable: failover runbook + sticky-LB docs + staging multi-replica soak before production-grade promotion. Lint 0/0.
1 parent 32cf981 commit dd977fd

13 files changed

Lines changed: 375 additions & 49 deletions

File tree

CHANGELOG.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,71 @@
22

33
All notable changes to Docker Dash are documented here.
44

5+
## [6.17.2] - 2026-04-22 — "HA Phase 4 — Leader election (multi-replica now safe)"
6+
7+
Cron jobs, Docker event stream, and git polling now run **on the leader replica only** in HA mode. Multi-replica HA deploy finally becomes safe: no more duplicate daily backups, no more concurrent `VACUUM` (DB corruption risk), no more N× GitHub API rate-limit hits from git polling.
8+
9+
### How it works
10+
11+
Redis `SET NX PX` with TTL 30s + heartbeat 10s:
12+
13+
- **Startup**: first call to `cluster.isLeader()` in HA mode lazily starts the election loop. Attempt `SET NX` to claim the `leader` key. Success → become leader. Failure → become reader. Standalone mode: always leader (return `true` without Redis traffic).
14+
- **Leader heartbeat**: every 10s, extend the lock with `SET XX PX` (refreshes TTL only if we still own it). If extension fails (TTL expired, someone else grabbed it), transition to reader and fire `onBecomeReader` callbacks.
15+
- **Reader poll**: every 10s, try `SET NX` — on leader death (or graceful `shutdown()`), a reader wins and transitions to leader.
16+
- **Graceful shutdown**: leader releases the lock proactively via a Lua script that only DELs if we still own it. Another replica picks it up within milliseconds instead of waiting out the 30s TTL.
17+
- **Internal-reset recovery**: if `_leaderState` is lost (e.g. module reset in tests) while Redis still holds our NODE_ID, `_electOnce` detects this via GET + comparison and re-claims leader without spurious role transition.
18+
19+
### Wiring — what runs on the leader only
20+
21+
**Cron jobs via `_m(name, fn)`** — now leader-aware. Any reader replica calling a `_m`-wrapped job returns immediately (silent skip, no metric increment). Opt-out via `_m(name, fn, { everywhere: true })` for idempotent jobs; none qualify today but the escape hatch exists.
22+
23+
All 13 cron jobs now leader-only:
24+
`stats-aggregate-1m` · `stats-aggregate-1h` · `alert-evaluate` · `session-mfa-cleanup` · `security-alert-windowed` · `purge-old-data` · `vacuum-db` · `certificate-scan` · `secret-rotation-scan` · `daily-backup` · `schedule-executor` · `s3-backup` · `sandbox-ttl-sweep`
25+
26+
**Docker event stream** — gated via `cluster.onBecomeLeader` / `onBecomeReader` in `src/ws/index.js`. On leader transition: `_startAllEventStreams()` subscribes to Docker for every active host. On reader transition: `_stopAllEventStreams()` destroys all streams. Readers still deliver events to their local clients via Redis pub/sub (shipped in v6.17.1).
27+
28+
**Git polling** — gated via the same callbacks in `src/jobs/index.js`. `gitPolling.startAll()` / `stopAll()` fire on role transition. Previously running per-replica would have N×-multiplied the GitHub API rate-limit hit.
29+
30+
### What still runs on every replica
31+
32+
- **SSH tunnels** (`src/services/ssh-tunnel.js`) — readers need them to serve HTTP reads (container list, stats, inspect). Not gated. Remote hosts see N SSH connections; acceptable for v6.17.2. Future v7.x may proxy read-path SSH through the leader.
33+
- **Stats service** (`statsService.start()`) — per-replica stats collection feeds local metrics endpoint. Aggregation (which writes to DB) is leader-only via the cron gate.
34+
35+
### Safety checks
36+
37+
- **Standalone completely unaffected.** `cluster.isLeader()` short-circuits to `true` without touching Redis. `onBecomeLeader(fn)` fires `fn` synchronously at registration — cron jobs start immediately.
38+
- **Rollback safe.** If you unset `DD_MODE` and restart, standalone path takes over. The `leader` key in Redis is orphaned (harmless) and expires via TTL.
39+
- **Throwing role-transition callbacks don't block siblings.** Each callback runs in its own try/catch.
40+
41+
### Tests — 8 new leader-election tests (879 total)
42+
43+
- `isLeader()` acquires the lock on first call (fresh Redis → become leader)
44+
- A second "replica" cannot acquire while held (NX returns null)
45+
- `onBecomeLeader` fires callbacks on role transition
46+
- `_forceRole` test helper — verifies callback sequence across multiple transitions
47+
- Idempotent transitions don't fire callbacks twice
48+
- A throwing callback doesn't prevent siblings from firing
49+
- Standalone mode: `onBecomeLeader` fires synchronously at registration
50+
- Standalone mode: `onBecomeReader` never fires
51+
52+
### Tests / Lint
53+
54+
- **879 passing + 4 skipped / 57 suites** (was 871 / 57; +8 Phase 4 tests)
55+
- Lint: 0 warnings / 0 errors
56+
57+
### Files touched
58+
59+
- `src/services/cluster.js` — +80 LOC: leader election loop, heartbeat, role transitions, callback registration, graceful lock release via Lua DEL-if-owned
60+
- `src/jobs/index.js``_m(name, fn, opts)` leader-aware, gitPolling start/stop wired to role callbacks
61+
- `src/ws/index.js` — Docker event stream start/stop on role transition
62+
- `src/__tests__/cluster.test.js` — +8 Phase 4 tests + standalone callback tests
63+
64+
### HA mode v6.17.x complete
65+
66+
v6.17.0 foundation + v6.17.1 pub/sub + v6.17.2 leader election = **multi-replica HA is safe**. v7.0.0 will bring the failover runbook, sticky-session LB docs, and a real multi-replica staging soak before promoting to "production-grade HA".
67+
68+
---
69+
570
## [6.17.1] - 2026-04-22 — "HA Phase 3 — WebSocket pub/sub via Redis"
671

772
Cross-replica WebSocket events now work. User connected to replica A **now receives** events emitted by replica B (alerts, container state changes, log lines) through Redis pub/sub. Before this, multi-replica HA deploys had silent event delivery gaps.

CONTRIBUTING.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@ Looking for where to start? These are great first contributions:
1111
- **Add a language translation** — copy `public/js/i18n/TEMPLATE.js`, translate values, add one `<script>` tag. Currently: 11 languages (EN, RO, DE, IT, FR, ES, PT, ZH, JA, KO, Klingon).
1212
- **Add an app template** — add an entry to `src/routes/templates.js` (JSON object with compose YAML). Currently: 33 templates.
1313
- **Improve i18n coverage** — some pages still have hardcoded English strings (grep for strings not using `i18n.t()`)
14-
- **Add tests**866 tests across 57 suites; more coverage is always welcome, especially integration tests
14+
- **Add tests**879 tests across 57 suites; more coverage is always welcome, especially integration tests
1515
- **Documentation** — improve README, add examples, write tutorials
1616
- **Accessibility** — add ARIA attributes, improve screen reader support, test keyboard navigation
1717

1818
### Project Stats (v5.3.0)
1919

2020
- **24 pages** in the frontend SPA (incl. Swarm, Compare with 8 tools)
2121
- **230+ API endpoints** (see `/api/docs` for full list)
22-
- **866 tests** (57 test suites, 100% passing)
22+
- **879 tests** (57 test suites, 100% passing)
2323
- **33 app templates** (+ custom user templates)
2424
- **37 database migrations** (001-037)
2525
- **11 languages** (EN, RO, DE, IT, FR, ES, PT, ZH, JA, KO, Klingon)

README.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
<a href="https://github.com/bogdanpricop/docker-dash/actions/workflows/ci.yml"><img src="https://github.com/bogdanpricop/docker-dash/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
99
<a href="https://github.com/bogdanpricop/docker-dash/releases/latest"><img src="https://img.shields.io/github/v/release/bogdanpricop/docker-dash?color=blue" alt="Release"></a>
1010
<a href="LICENSE"><img src="https://img.shields.io/github/license/bogdanpricop/docker-dash" alt="License"></a>
11-
<a href="https://github.com/bogdanpricop/docker-dash/actions/workflows/ci.yml"><img src="https://img.shields.io/badge/tests-871%20passing%20(100%25)-brightgreen" alt="Tests"></a>
12-
<img src="https://img.shields.io/badge/version-6.17.1-blue" alt="Version">
11+
<a href="https://github.com/bogdanpricop/docker-dash/actions/workflows/ci.yml"><img src="https://img.shields.io/badge/tests-879%20passing%20(100%25)-brightgreen" alt="Tests"></a>
12+
<img src="https://img.shields.io/badge/version-6.17.2-blue" alt="Version">
1313
<a href="SECURITY.md#security-audit-history"><img src="https://img.shields.io/badge/production%20readiness-9.7%2F10-brightgreen" alt="Production Readiness"></a>
1414
<a href="SECURITY.md"><img src="https://img.shields.io/badge/security-audited-brightgreen" alt="Security Audited"></a>
1515
<img src="https://img.shields.io/badge/Docker-~80MB-blue" alt="Image Size">
@@ -26,9 +26,14 @@
2626
</p>
2727
</p>
2828

29-
**Zero dependencies to deploy** — just Docker. No external database, no Redis, no build step. Current version: **v6.17.1**
29+
**Zero dependencies to deploy** — just Docker. No external database, no Redis, no build step. Current version: **v6.17.2**
3030

31-
**New in v6.17.x:** Optional HA mode via `DD_MODE=ha` + Redis. v6.17.0 shipped cluster abstraction + Redis rate limiter. **v6.17.1 adds cross-replica WS broadcasts via Redis pub/sub.** Cron leader election lands in v6.17.2 (real multi-replica safe). See [docs/features/ha-mode.md](docs/features/ha-mode.md).
31+
**HA mode feature-complete in v6.17.x** (standalone default unchanged):
32+
- **v6.17.0** — cluster abstraction + Redis-backed rate limiter
33+
- **v6.17.1** — cross-replica WS broadcasts via Redis pub/sub
34+
- **v6.17.2****cron + Docker event stream + git polling leader election** via Redis `SET NX PX`. Multi-replica HA is now safe: one replica holds the leader lock (30s TTL, 10s heartbeat); readers serve HTTP, ignore cron, have Docker events delivered via pub/sub.
35+
36+
v7.0.0 stable adds the failover runbook + staging multi-replica soak + sticky-session LB docs. See [docs/features/ha-mode.md](docs/features/ha-mode.md).
3237

3338
## Screenshots
3439

SECURITY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ If you discover a security vulnerability in Docker Dash, please report it respon
8888

8989
## Testing
9090

91-
- **866 tests** across 57 test suites (100% passing; 4 skipped are live-Cloudflare integration tests gated on a CI secret)
91+
- **879 tests** across 57 test suites (100% passing; 4 skipped are live-Cloudflare integration tests gated on a CI secret)
9292
- Unit tests: crypto round-trip, input validation, shell sanitization, git patterns
9393
- Integration tests: auth flow (login, session, logout, SSO), API endpoints (supertest), RBAC, security alerts
9494
- **CI pipeline** — GitHub Actions runs tests + syntax check + npm audit on every push

docker-compose.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ services:
44
context: .
55
dockerfile: Dockerfile
66
args:
7-
APP_VERSION: "${APP_VERSION:-6.17.1}"
8-
image: docker-dash:${APP_VERSION:-6.17.1}
7+
APP_VERSION: "${APP_VERSION:-6.17.2}"
8+
image: docker-dash:${APP_VERSION:-6.17.2}
99
container_name: docker-dash
1010
restart: unless-stopped
1111
env_file:
@@ -54,7 +54,7 @@ services:
5454
dd-egress-filter:
5555
build:
5656
context: ./docker/egress-filter
57-
image: docker-dash-egress-filter:${APP_VERSION:-6.17.1}
57+
image: docker-dash-egress-filter:${APP_VERSION:-6.17.2}
5858
container_name: dd-egress-filter
5959
restart: unless-stopped
6060
# Uses the default bridge so target containers on the default bridge can
@@ -83,9 +83,9 @@ services:
8383
# DD_MODE=ha
8484
# REDIS_URL=redis://redis:6379
8585
#
86-
# v6.17.1 ships the foundation (Redis-backed rate limiter + cluster abstraction).
86+
# v6.17.2 ships the foundation (Redis-backed rate limiter + cluster abstraction).
8787
# DO NOT run multi-replica in HA mode yet — WS pub/sub + cron leader election
88-
# land in v7.0.0-alpha.1 / v7.0.0-rc.1. Running v6.17.1 HA with 2+ replicas
88+
# land in v7.0.0-alpha.1 / v7.0.0-rc.1. Running v6.17.2 HA with 2+ replicas
8989
# causes duplicate cron execution (duplicate backups, concurrent VACUUM).
9090
#
9191
# Single-instance HA (1 replica + Redis) is useful for warming up operational

docs/features/ha-mode.md

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
# HA Mode — Optional Redis-backed High Availability
22

33
**Introduced:** v6.17.0 (foundation — rate limiter + cluster abstraction)
4-
**Full HA:** v7.0.0 (planned — adds WS pub/sub + cron leader election)
5-
**Status:** v6.17.0 ships the **preview foundation**. Multi-replica production deployment requires v7.0.0.
4+
**Feature-complete:** v6.17.2 (pub/sub + leader election)
5+
**Production-grade:** v7.0.0 stable (planned — failover runbook + staging multi-replica soak)
6+
**Status:** v6.17.2 ships **multi-replica safe HA**. Safe to run 2-3 replicas behind a sticky-session load balancer. Full production-grade promotion (with automated failover docs + soak test results) comes in v7.0.0.
67

78
---
89

@@ -24,17 +25,18 @@ For those environments, v6.17.0 introduces **opt-in HA mode**: a designated "wri
2425

2526
## What HA mode changes
2627

27-
| Subsystem | Standalone | HA mode (v6.17.0) | Full HA (v7.0.0) |
28-
|-----------|:----------:|:-----------------:|:----------------:|
29-
| Rate limiter | In-memory Map | **Redis INCR**| Redis INCR |
30-
| WebSocket broadcasts | In-process | In-process (each replica independent) | **Redis pub/sub** 🔜 |
31-
| Cron jobs | Run on the single process | **All replicas run them** ⚠️ | **Leader-only** 🔜 |
32-
| Docker event stream | Per-process | Per-replica (duplicate) | Leader-only 🔜 |
33-
| SSH tunnels | Per-process | Per-replica (duplicate) | Leader-only 🔜 |
34-
| Sessions | DB-backed, works across replicas | DB-backed, works across replicas | DB-backed |
35-
| DB | Local SQLite | Shared SQLite (single-writer recommended) | Shared SQLite (writer-enforced) |
36-
37-
**v6.17.0 ships only the Redis rate limiter + the cluster abstraction.** Running 2+ replicas with `DD_MODE=ha` today causes duplicate cron execution. **Don't.** Single-replica HA mode is useful for operational drill (sticky-session LB config, Prometheus scrape of Redis, Grafana dashboard wiring) before rolling out true multi-replica in v7.0.0.
28+
| Subsystem | Standalone | HA mode (v6.17.2) |
29+
|-----------|:----------:|:-----------------:|
30+
| Rate limiter | In-memory sliding window | **Redis INCR fixed window** |
31+
| WebSocket broadcasts | In-process | **Redis pub/sub on `ddash:pubsub` channel** (loop-safe via nodeId filter) |
32+
| Cron jobs | Single process runs them | **Leader-only** (Redis SET NX PX, 30s TTL + 10s heartbeat) |
33+
| Docker event stream | Per-process | **Leader-only** (start on become-leader, stop on become-reader) |
34+
| Git polling | Single process | **Leader-only** |
35+
| SSH tunnels | Per-process | **Per-replica** (readers need them to serve HTTP reads; documented acceptable cost) |
36+
| Sessions | DB-backed | DB-backed (works across replicas) |
37+
| DB | Local SQLite | Shared SQLite (single-writer — leader holds writes; readers proxy via internal API in v7.0) |
38+
39+
**v6.17.2 is multi-replica-safe.** Deploy 2-3 replicas behind a sticky-session LB. One replica holds the leader lock and runs all cron + Docker event stream + git polling. Readers serve HTTP, have WS events delivered via pub/sub. On leader death, a reader acquires the lock within ~30s (TTL). Graceful shutdown releases the lock immediately (Lua DEL-if-owned).
3840

3941
---
4042

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "docker-dash",
3-
"version": "6.17.1",
3+
"version": "6.17.2",
44
"description": "Full-featured Docker management dashboard",
55
"main": "src/server.js",
66
"scripts": {

public/js/pages/whatsnew.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,20 @@ const WhatsNewPage = {
99
// Add new releases at the TOP of this array.
1010
// Types: feature, fix, improvement, security, breaking
1111
_releases: [
12+
{
13+
version: '6.17.2',
14+
date: '2026-04-22',
15+
title: 'HA Phase 4 — Leader election (multi-replica HA is now safe)',
16+
changes: [
17+
{ type: 'feature', text: 'Cron jobs, Docker event stream, and git polling now run on the leader replica only via Redis SET NX PX (30s TTL + 10s heartbeat). All 13 cron jobs (daily-backup, vacuum-db, certificate-scan, secret-rotation-scan, stats aggregation, sandbox-ttl-sweep, s3-backup, schedule-executor, etc.) are leader-gated. No more duplicate backups, no more concurrent VACUUM on SQLite (DB corruption risk), no more N× GitHub API rate-limit hits.' },
18+
{ type: 'feature', text: 'Docker event stream per-replica was the gotcha of v6.17.1: multiple replicas subscribing to Docker + pub/sub broadcasting = N× event delivery to users. Now leader-only: on role transition leader starts event streams, reader stops them. Readers still get events via Redis pub/sub (shipped v6.17.1) and deliver to their local WS clients.' },
19+
{ type: 'feature', text: 'Graceful leader handover via Lua DEL-if-owned script on shutdown. Another replica picks up the lock within milliseconds instead of waiting 30s TTL. Internal-reset recovery: if state is lost but Redis still holds our NODE_ID, GET-and-compare re-claims without spurious transition.' },
20+
{ type: 'improvement', text: 'Standalone mode completely unaffected. isLeader() short-circuits to true without touching Redis. onBecomeLeader(fn) fires synchronously at registration. Existing cron jobs start immediately like before. Zero runtime overhead. Zero new env vars.' },
21+
{ type: 'improvement', text: 'SSH tunnels intentionally NOT leader-gated — readers need them to serve HTTP reads (container list, stats, inspect). Remote hosts see N SSH connections in multi-replica HA; acceptable for v6.17.2. Future v7.x may proxy read-path SSH through the leader.' },
22+
{ type: 'improvement', text: 'Tests: 871 → 879 (+8 leader-election tests via ioredis-mock). Covers lock acquire/contention, role transition callbacks, idempotent transitions, throwing-callback isolation, standalone synchronous onBecomeLeader. Lint 0/0.' },
23+
{ type: 'improvement', text: 'HA v6.17.x complete. Multi-replica HA deploy is SAFE from this release. v7.0.0 stable next — failover runbook, sticky-session LB docs, real staging multi-replica soak before production-grade HA promotion.' },
24+
],
25+
},
1226
{
1327
version: '6.17.1',
1428
date: '2026-04-22',

0 commit comments

Comments
 (0)