v1.5.0-rc.30
Full Changelog: v1.5.0-rc.29...v1.5.0-rc.30
[1.5.0-rc.30] — 2026-06-05
Added
-
Trigger environment variable taxonomy split —
DD_ACTION_*andDD_NOTIFICATION_*prefixes. Action triggers (Docker, Docker Compose, Command) are now configured withDD_ACTION_*anddd.action.*labels; notification/messaging triggers (Slack, SMTP, Discord, Telegram, ntfy, Pushover, and all others) are configured withDD_NOTIFICATION_*anddd.notification.*labels. All three prefix families (DD_ACTION_*,DD_NOTIFICATION_*,DD_TRIGGER_*) are interchangeable at runtime — merge priority isDD_NOTIFICATION_*>DD_ACTION_*>DD_TRIGGER_*. A migration CLI (drydock config migrate --source trigger) rewritesDD_TRIGGER_*,dd.trigger.include, anddd.trigger.excludeto action-prefixed aliases automatically; use--dry-runto preview changes before applying. -
Per-agent Home Assistant MQTT topic segmentation (
DD_NOTIFICATION_MQTT_<name>_HASS_AGENTTOPICSEGMENT, defaultfalse). When enabled, Drydock inserts anagent/<name>segment into every Home Assistant MQTT topic — per-container state topics and watcher-level sensor topics — for containers owned by a remote agent, so two agents that both use the default watcher namelocalno longer publish to (and overwrite) the same topics. Enabling it also scopes the watcher-level sensor counts and the discovery-entity cleanup per agent, fixing the Home Assistant facet of #386. Controller-local container topics are unchanged. Because it changes the Home Assistant entity IDs for agent-owned containers, it is opt-in for the v1.5.x line and targeted to become the default in v1.7.0 — see Deprecated. -
Up-to-date and pinned badges in Kind column — Containers table now shows a green check-circle badge ("Up to date") for containers at their latest version, and a green pin badge ("Pinned") for containers with skipped updates, replacing the previous dash placeholder.
-
Show/hide toggle on the login password field (commit
e086c5bc). The sign-in password input now has an eye / eye-slash button to reveal or mask what was typed, with an accessible label andtype="button"so it never submits the form. -
Real-time container log viewer — WebSocket-based live log streaming from Docker containers directly in the UI. Features ANSI color rendering, automatic JSON log detection with syntax-highlighted pretty-printing, free-text and regex search with match navigation, stdout/stderr stream filtering, log level filtering for structured logs, copy to clipboard, and gzip-compressed download. Available in both the container detail panel and a dedicated full-page view at
/containers/:id/logs. (Phase 4.2) -
Diagnostic debug dump — One-click export of redacted system state from Configuration > Diagnostics. Collects runtime metadata, component state (watchers, registries, triggers, agents), Docker API diagnostics, MQTT Home Assistant sensors, recent Docker events, store stats, and
DD_*environment variables. Sensitive values matchingpassword|token|secret|key|hashare automatically redacted. Configurable time window (1–1440 minutes). (Phase 4.14) -
Container log streaming API —
WS /api/v1/containers/:id/logs/streamendpoint with Docker binary stream demultiplexing, session-based authentication on WebSocket upgrade, and fixed-window rate limiting (1,000 connections per 15 minutes). -
Container log download API —
GET /api/v1/containers/:id/logsendpoint with gzip compression support, stdout/stderr filtering, configurable tail size, and timestamp-basedsincefiltering. -
Debug dump API —
GET /api/v1/debug/dumpendpoint with configurableminutesquery parameter for time-windowed event collection. -
Dashboard customization — Customizable grid layout with drag-to-reorder, resize, and per-widget visibility toggles using
grid-layout-plus. Edit mode via pencil icon in breadcrumb header. Customize panel with checkboxes and S/M/L size badges. All widgets progressively collapse content based on container height. -
Resource usage dashboard widget — CPU and memory usage bars with top-N resource consumers, progressive detail at different widget sizes.
-
Fleet-aggregate stats subsystem (commits
feature/v1.5-rc17). NewContainerStatsAggregatorpolls each locally-monitored container once per tick (default 10 s) and computes a fleet-wideContainerStatsSummary(total CPU%, total memory, top-N rows). Two new endpoints —GET /api/v1/stats/summaryandGET /api/v1/stats/summary/stream— expose the current snapshot and a live SSE feed; the dashboard Resource Usage widget now consumes the SSE stream directly, fixing the regression (introduced in rc.13 by the?touch=falseworkaround) where the widget showed zeros because the per-container cache was never warmed. The legacyGET /api/v1/containers/statsendpoint and the client-sidesummarizeContainerResourceUsagerollup have been removed. -
Per-container update locks (commit
761fb834). New keyedLockManagerprimitive inapp/updates/lock-primitives.tsreplaces the module-levelpLimit(1)that was serialising every container update across the entire process. Lock keys are derived per container (and per compose project forDockercompose), so two unrelated containers can now pull and recreate concurrently while two services in the same compose project still serialise correctly. -
Restart recovery for queued and pulling updates (commit
00788b13). Startup reconciliation inapp/store/update-operation.tsis now selective:status=queuedoperations stay queued for the recovery dispatcher to pick up, andphase=pullingrows are reset toqueued(pull is idempotent). A newapp/updates/recovery.tsmodule runs once afterregistry.init(), re-resolves trigger and container for each queued operation, and dispatches them through the existing fire-and-forget pipeline. -
Notification outbox with retry and dead-letter queue (commits
a9561d93,7d2ef6eb,b215d295,ce26bece). NewnotificationOutboxLokiJS collection andapp/notifications/outbox-worker.tsbackground worker provide durable retry semantics for notification dispatch. On failure, the delivery intent is persisted to the outbox and the worker retries on a periodic drain with exponential backoff + jitter. After a configurable number of failed attempts (default 5) entries transition to the dead-letter queue; delivered and dead-letter entries are auto-purged past TTL (default 30 days). New/api/notifications/outboxREST surface lets operators list entries, retry from the DLQ, or discard. -
Notification outbox UI (commit
feature/v1.5-rc17). NewNotification outboxpage (route/notifications/outbox, nav under Settings) with status tabs (Dead-letter / Pending / Delivered), retry and discard actions. -
Cancel queued or in-flight updates (commits
4b79e3ac,79487115).POST /api/operations/:id/cancelnow accepts both queued and in-progress operations. Queued ops are marked failed immediately; in-progress ops are flagged via acancelRequestedfield and the lifecycle observes the flag at three safe checkpoints. -
Global concurrent-update cap (
DD_UPDATE_MAX_CONCURRENT). New counting semaphore provides a configurable global gate on how many update lifecycles run simultaneously. Default0= unlimited. Positive integerNmeans at most N updates run concurrently. Self-update operations bypass the global cap. -
Health-gate SSE heartbeat (
DD_UPDATE_HEALTH_GATE_HEARTBEAT_MS). While drydock waits for a new container to pass its health gate, a periodic heartbeat re-emitsphase: 'health-gate'at a configurable interval (default 10 s).DD_UPDATE_HEALTH_GATE_HEARTBEAT_MS=0disables heartbeats; values below 1000 ms or non-integers fail fast at startup. -
Post-start liveness grace window (
DD_UPDATE_POST_START_LIVENESS_GRACE_MS). After Dockerstart()returns, Drydock waits this many milliseconds and then re-inspects the new container. If the container has already exited, the lifecycle throws and the existing rollback machinery takes over — catching containers that exit immediately after an update (bad command, broken entrypoint, missing dependency) that would otherwise be recorded as a successful update. Default2000ms. Set to0to disable the check entirely. Values between 1 and 99 ms are rejected at startup; the minimum non-zero value is 100 ms. -
Recovery-boot concurrency cap (
DD_UPDATE_RECOVERY_BOOT_CONCURRENCY). When Drydock restarts after a crash it finds queued update operations left from the previous run and resumes them. This variable bounds how many are dispatched in parallel during that recovery sweep. Default4. Values of0are rejected at startup (minimum is 1). -
Self-update now works when Drydock reaches the Docker daemon over a TCP host, not only through a bind-mounted
/var/run/docker.sock(commitfc34ffb9).resolveHelperDockerConnectionnow inspects the watcher's Dockerode connection: a TCP host produces a TCP helper attached to Drydock's own Docker network. The bind-mounted-socket path is unchanged. -
The per-container Update button is locked with a
Self-update unavailableindicator when Drydock cannot update itself in the current deployment (commitcf777280). A new hardself-update-unavailableupdate-eligibility blocker is raised when self-update cannot run over either a bind-mounted socket or a TCP host. -
i18n coverage extended to the remaining hardcoded UI strings across 28 components (discussion #329). All 16 non-English locales now have full key parity with the English source. 17 locales ship in the picker: de, es, fr, it, nl, pl, pt-BR, tr, zh-CN, zh-TW, ar, ja, ko, ru, uk, vi, plus English.
-
DD_AGENT_ALLOW_INSECURE_SECRETescape hatch for closed-LAN deployments. rc.20 tightened the agent-secret-over-HTTP check to a hard error. rc.21 introducesDD_AGENT_ALLOW_INSECURE_SECRET=trueas an explicit controller-side opt-in for environments where the operator accepts that the agent secret travels in cleartext. Default behavior is unchanged. -
Security scan digest mode. Every scan cycle now carries a stable
cycleId(UUID v7) and emits asecurity-scan-cycle-completeevent. Triggers can configureSECURITYMODE=digest(orbatch+digest) to receive one summary per cycle. Templates are customizable viaSECURITYDIGESTTITLE/SECURITYDIGESTBODY. (#300) -
Opt-in scheduled-scan notifications — New
DD_SECURITY_SCAN_NOTIFICATIONS=trueflag enablessecurity-alertevent emission from scheduled scans. Default isfalse; on-demand scans always emit. -
Bulk security scan endpoint —
POST /api/v1/containers/scan-allscans all (or a filtered subset of) watched containers server-side, streams per-container progress over the existing scan SSE channel, and honors client-disconnect aborts. Rate-limited to 1 request / 60s per IP (authenticated-admin bypass). -
SSE Last-Event-ID replay (#289) — The server stamps every broadcast event with a monotonic
<bootId>:<counter>id and retains a 5-minute time-bounded ring buffer. Clients reconnecting with aLast-Event-IDheader receive every event they missed; if the buffer has evicted the requested id the client receives add:resync-requiredevent. -
Update-eligibility blockers on container rows — Backend surfaces 12 structured blocker reasons per container, rendered inline on the Containers list so users see why a row isn't updating without opening the detail drawer.
-
GET /update-operations/:idendpoint — Returns the current state of a specific update operation for reconciliation when the terminal SSE is missed. -
Inline update action in Security view (#299) — Image rows in the Security view now show an "Update" action button directly next to the vulnerability data when a newer image is available.
-
Watcher next-run metadata (#288) — Watcher API and Agents view now show when each watcher will next poll for updates, with an absolute-timestamp tooltip on hover.
-
Backend-driven update queue — Container updates are now queued server-side with per-trigger concurrency limits. UI shows Queued → Updating → Updated state progression with sequence labels (e.g. "Updating 1 of 3").
-
Registry 429 / 503 retry with Retry-After and per-host token bucket (commit
ffd1b57b, #342). A newwithRetryhelper wraps every registry HTTP call: on 429 or 503 it honors the upstreamRetry-Afterheader, then falls back to exponential backoff (1 s / 2 s / 4 s, capped at 60 s), up to 3 retries. A new per-host token bucket prevents the watcher from self-inflicting rate limits during a large cron cycle. -
Release notes inline popover (commit
09475fa6). The release-notes icon on container rows now opens an inline popover showing both the current and the available-version release notes side by side, with expand/collapse per panel. -
Container source project shortcut link (Discussion #295) — Containers now render a clickable "View project" link next to release notes when an
org.opencontainers.image.sourceOCI label,dd.source.repooverride label, or GHCR-derived source URL is available. -
Actionable deprecation banners (Discussion #214) — The 5 deprecation warning banners now show the concrete migration action inline and include a "View migration guide" link that deep-jumps to the relevant anchored section of the deprecations docs page.
-
Notification dropdown rework + themeable zebra stripes (Discussion #267) — Header carries the "Notifications" title plus a "Clear" text button, each row shows a per-entry dismiss affordance, and a split footer exposes "Mark all as read" + "Open audit log". Introduces
--dd-zebra-stripe, a new theme token. -
Notification history store — New LokiJS collection (
notifications_history) records a per-(trigger, container, event-kind) result hash soonce=truededup survives process restarts. -
Floating tag detection and UI indicator — New
tagPrecisionclassifier (specific|floating) detects mutable version aliases and auto-enables digest watching on non-Docker Hub registries. Container detail views show a caution badge when a floating tag is detected without digest watching enabled. (Discussion #178) -
Hide Pinned containers toggle — Checkbox in the container list filter bar hides containers pinned to specific versions. Persisted in user preferences. (Discussion #250)
-
Combined batch+digest notification mode — Triggers can now use
MODE=batch+digestto send both immediate batch emails and scheduled digest summaries. (#254) -
Multi-select event-type filter in audit log (commit
5e2d0c70, Discussion #332). The audit log's event-type filter is now a checkbox dropdown supporting any combination of event categories simultaneously. -
Bearer token auth for
/metricsendpoint — SetDD_SERVER_METRICS_TOKENto authenticate Prometheus scrapers viaAuthorization: Bearer <token>. -
Disable default local watcher — Set
DD_LOCAL_WATCHER=falseto prevent the built-in Docker watcher from starting, useful for controller-only nodes that manage remote agents exclusively. -
Multi-server notification identification (#283) — Notifications automatically include a
[server-name]prefix when agents are registered. Controller name configurable viaDD_SERVER_NAME. Custom templates can usecontainer.notificationServerNameandcontainer.notificationAgentPrefix. -
Infrastructure update mode —
dd.update.mode=infrastructurelabel for socket proxy containers enables helper-swap update path bypassing the socket proxy. -
i18n framework migration (refs #329). Bulk vue-i18n migration into per-namespace JSON catalogs under
ui/src/locales/en/(eight namespaces auto-loaded byimport.meta.globinboot/i18n.ts). Foundation for the Crowdin integration. 17 locales ship in the picker. -
Design system components — Added shared UI building blocks:
AppIconButton,AppBadge,StatusDot,DetailField, andAppTabBar. (Discussion #199) -
Podman API version negotiation — Docker watcher probes the daemon's
/versionendpoint over the Unix socket and pins Dockerode to the reported API version. PreventsEAI_AGAINcrashes caused bydocker-modem's redirect-following bug when Podman returns HTTP 301 for unversioned API paths. (#182) -
System log live streaming in UI — Added end-to-end WebSocket support for system logs (
/api/v1/log/stream) with new UI service/composable and live log view integration. -
System log viewer overhaul — Toolbar stays pinned at top, long lines wrap at viewport width, search matches component/level/channel fields, filter toggle shows only matching entries, sort toggle switches between oldest-first and newest-first. (#259, #260)
-
Rollback shortcut in container actions menu — Quick rollback option directly from the container row actions dropdown.
-
SPA + hashed-asset cache-control — Static UI assets with hashed filenames are served with immutable long-lived cache headers; the SPA
index.htmlcarries a short revalidation header.
Changed
-
Default watcher cron relaxed from hourly to every 6 hours (#342 follow-up).
app/watchers/providers/docker/Docker.tsnow defaultscronto0 */6 * * *(every 6 hours) instead of0 * * * *(hourly). Users who setDD_WATCHER_{name}_CRONexplicitly are unaffected. Users who want near-real-time detection can still setDD_WATCHER_{name}_CRON=0 * * * *. -
Action trigger default mode — Action triggers (
docker,dockercompose,command) now default toAUTO=onincludeinstead ofAUTO=all, requiring an explicitdd.action.includelabel before auto-updating containers. (#213) -
Self-update helper now prefers the bind-mounted Docker socket over a TCP watcher connection (commit
aa828d88). The resolution order is now inverted:findDockerSocketBindruns first, and if the target container carries a socket bind the helper uses that direct socket path regardless of the watcher's TCP configuration. TCP is the fallback for pure socket-less deployments. -
DD_SESSION_SECRETauto-generated and persisted when unset. On first boot withoutDD_SESSION_SECRETset, drydock generates 64 random bytes and writes them to asecretscollection inside/store/dd.json. Subsequent boots read the persisted value so sessions survive restarts. The env var still takes precedence when set. (rc.21 restored this after rc.20 made it a hard requirement without a migration path; existing deployments that set the variable see no change.) -
Watcher dispatch is fully fire-and-forget (commit
5cfa2286).Trigger.runUpdateAvailableSimpleTriggerandrunAcceptedUpdateBatchno longer awaitrunAcceptedContainerUpdates, so a slow update lifecycle no longer stalls the next watcher tick. -
"Update started" toasts renamed to "Update queued" (commit
79487115). Dispatch is fire-and-forget — the text now matches what actually happened. -
Shared DataTable column sizing overhaul (commit
596adcd2). All first-party table surfaces now route through the sharedDataTablecomponent with numeric sizing metadata, supporting pointer and keyboard column resizing, double-click autosize, and persistent manual/autosized widths per table. -
Crowdin export configuration aligned with app locale folders.
-
Tag-last release pipeline (fixes #306). Collapsed
release-cut.ymlandrelease-from-tag.ymlinto a single workflow where the git tag push is the last step, performed only after the Docker image has been built, pushed, signed with cosign, attested (SLSA), and the signed release tarball has been verified. Enforces the invariant: if the git tag exists, the image exists. -
Playwright E2E tests moved to a dedicated workflow file (
e2e-playwright.yml) (commitf0989301). OSSF Scorecard's CI-Tests check now scores independently from the main ci-verify suite. -
Translations refreshed from Crowdin (commit
202f3d83). Human translations synced from Crowdin for the rc.23 i18n extraction sweep, updating the 16 non-English locales across all UI namespaces. -
Security alert emit is non-blocking inside the update lifecycle (commit
6c5198dd).SecurityGate.maybeEmitHighSeverityAlertnow returns synchronously after firing the emit; the lifecycle no longer waits for sequential provider notifications. -
Expand all / Collapse all bulk toggle — Replaced the single chevron toggle in the Containers toolbar with an explicit "Expand all" / "Collapse all" button.
-
Soft eligibility blockers de-emphasized by default (Discussion #325). Soft blockers now render with neutral muted styling so hard blockers visually dominate the row, and active blockers sort hard-first.
-
Responsive dashboard layout persistence — Dashboard widget bounds and layout are now breakpoint-aware, persisting separate layouts per viewport tier.
-
Trigger digest flush DRY refactor —
flushDigestBuffer/shouldHandleDigestContainerReportare now parameterized on the event kind so update-digest and security-digest share a single implementation. -
Healthcheck execution path optimized — Default HEALTHCHECK probe replaced with a 65KB static C binary (
/bin/healthcheck). curl is retained for backward compatibility with user-defined HEALTHCHECK overrides during the deprecation window (scheduled for removal in v1.7.0, final warning release v1.6.0). -
Agent reconnect notification — New opt-in
agent-reconnectnotification rule that fires when a remote agent reconnects after losing connection. Disabled by default. -
app/updates/locks.tsrenamed toapp/updates/lock-primitives.ts(commit4c506d21). The module now contains general-purpose synchronisation primitives (Semaphore,LockManager) not tied to the updates subsystem. -
Exact-version package.json pinning — Flipped the four remaining caret specifiers to exact versions so every package.json matches the already-exact lockfile resolutions. Every dependency layer is now SHA-immutable.
-
Dashboard reconnect refresh is live-only. On
dd:sse-connectedthe dashboard now refetches only endpoints that can go stale between frames;dd:sse-resync-requiredstill forces a full fan-out.
Deprecated
-
DD_TRIGGER_*environment variable prefix anddd.trigger.*container labels (deprecated v1.5.0, removal targeted v1.7.0). UseDD_ACTION_*/dd.action.*for update-action triggers (Docker, Docker Compose, Command) andDD_NOTIFICATION_*/dd.notification.*for messaging/notification triggers (Slack, SMTP, Discord, Telegram, ntfy, Pushover, and all others). The legacy prefixes continue to work as aliases through v1.7.0. A migration CLI (drydock config migrate --source trigger) rewrites existing configs automatically. SeeDEPRECATIONS.mdfor the full schedule. -
dd.action.include/dd.action.exclude(and legacydd.trigger.include/dd.trigger.exclude) become hard manual-update blockers in v1.7.0. v1.5.x keeps them as soft blockers — the pill reads Trigger filtered / Trigger excluded but the manual Update button stays clickable (with a warn-and-confirm). v1.7.0 will lock the button and reject the API call when the labels filter out the matching trigger. See DEPRECATIONS.md for migration guidance. -
curlhealthcheck override —curlis retained in the image for user-defined HEALTHCHECK overrides during the v1.5.x deprecation window; removal is scheduled for v1.7.0 (v1.6.0 is the final warning release). -
Agent-less Home Assistant MQTT topic layout for multi-agent deployments (deprecated v1.5.0, default flips v1.7.0). When more than one node uses the default watcher name
local, the current agent-less topic layout makes same-named containers on different agents publish to and overwrite the same MQTT topics. SetDD_NOTIFICATION_MQTT_<name>_HASS_AGENTTOPICSEGMENT=trueto opt into the corrected per-agent layout now; it becomes the default in v1.7.0. Single-node deployments are unaffected. Enabling it changes the Home Assistant entity IDs for agent-owned containers — see DEPRECATIONS.md for migration guidance.
Removed
-
Experimental
eligibilityPills.{showSoft,deemphasizeSoft}preferences dropped before release. The de-emphasis is now baseline behavior with no toggle. -
Experimental
containers.showAutoUpdateDiagnosticpreference +compactvariant ofUpdateEligibilityBadgesdropped before release. -
Two pre-existing unused imports flagged by biome's
noUnusedImports(updates/request-update.tsdefaultTriggerimport;Docker.containers.processing-retrieval.test.tsmockGetFullReleaseNotesForContainerimport). -
Legacy
GET /api/v1/containers/statsendpoint andsummarizeContainerResourceUsageclient-side rollup removed; superseded by the new fleet-aggregate stats subsystem (GET /api/v1/stats/summary).
Fixed
-
Latent-bug audit of the 1.5 RC bug classes — ten same-class defects fixed before 1.5.0 final. A class-wide sweep of the recurring root-cause classes behind the 1.5 RC churn (container-identity-by-name, missing agent scope, terminal-event snapshots, operationId threading, notification dedup, stale-scan epochs, SSE reconnect, image-reference normalization, health readiness) surfaced the following still-unfixed instances. Each is active by default and ships with a regression test:
- Agent-hosted update operations now carry a container snapshot and can no longer be orphaned mid-prepare.
ContainerUpdateExecutorcreated fresh operation rows without acontainer, so terminalupdate-failed/rollback events emitted with no snapshot and the notification fell back to a store lookup that misses after a recreate — silently dropping the agent-path failure notification. The post-pull tail (clone-runtime-config + rename) was also unguarded, leaving the row stuckin-progressfor the full TTL on error. The snapshot is now persisted at enqueue and the tail marks the operation terminal on failure. Latent remnant of the #385/#386 terminal-lifecycle class. - The direct remote-trigger route (
POST /:type/:name/:agent) now threads the controller-minted operation id (#289 remnant). For update triggers it creates the controller-side queued row, honors a caller-suppliedoperationId(previously validated then silently discarded), forwards it to the agent as runtime context, and returns202 { operationId }instead of200 {};UpdateRequestErroris surfaced with its status. The agent no longer mints a divergent id the controller never tracks. - Digest- and batch-mode notifications no longer re-fire a spurious "update available" after a successful update (#408 digest/batch remnant). The original #408 guard (
recentlyAppliedContainerKeys) was applied only to the simple report path, so a staleupdateAvailable=truereport arriving before the watcher caught up could still re-buffer a just-updated container on the digest and batch paths. The digest path (shouldHandleDigestContainerReportplus thehandleContainerReportDigestlift) and the batch path (shouldHandleBatchContainerReportplus a newhandleContainerReportslift) now share the same suppress-then-lift lifecycle. The batch-path lift is mandatory rather than belt-and-braces: a puremode=batchtrigger registers neither the simple nor the digest handler, so without it the suppression key would never clear and that container's update-available notifications would be muted permanently after the first successful update. - Re-armed the post-update stale-scan guard (#265 regression). The rc.17 operation-store refactor removed the only call that stamped the manual-update epoch, leaving
preserveClearedUpdateStateas dead code; an in-flight cron scan could re-raise the cleared update badge.maybeFastResyncAfterUpdatenow stamps the epoch before the resync scan, suppressing earlier in-flight scans. - The Security view now refreshes its container list on SSE reconnect/resync. It previously refetched only vulnerabilities, leaving update-eligibility, blocker tooltips, and the inline Update action stale after a dropped connection (it could offer Update on a container that no longer exists).
- The self-update helper image reference is now built with
buildImageReference.resolveHelperImagehand-rolled the registry-URL normalization; whenregistry.urlended with a trailing slash (…/v2/) the concatenation produced a double-slash reference (ghcr.io//org/image:tag) that Docker rejects with HTTP 400. It now delegates to the canonical helper that strips the scheme and trailing/v2[/]before concatenation. - Scheduled security scans write their result onto the current container record, not a stale snapshot. The async Trivy scan back-wrote by spreading the container snapshot captured at batch-prep time, carrying stale update-state and — on the recreate path, where the container has a new id — creating a zombie store record for the old id. The write-back now re-reads the live record by id, skips if the container is gone, and merges only the
securityfield. - The agent
/healthendpoint now reflects watcher-registration failure. It was a hardcoded200, so an agent whose watchers all failed to register (unreachable Docker socket, bad TLS — swallowed withlog.warn) reported healthy forever. It now returns503when zero watchers are registered, mirroring the main API's readiness gate; the normal (≥1 watcher) path is unchanged. - The agent batch-completed event parser resolves controller-issued operation ids.
parseBatchUpdateCompletedPayloadwas the only event parser scoping itsoperationIdwithtoAgentScopedIdinstead ofresolveAgentOperationId; a controller-owned id reaching it would be double-scoped and stop matching the controller's row. It now resolves consistently with the other parsers (#289 class). - Container-recreate no longer fails with HTTP 403 on deployments using a hardened socket proxy that enforces a runtime allowlist (commit
af79e612). Drydock copied the daemon-reportedHostConfig.Runtime("runc") verbatim into thePOST /containers/createbody, and a socket proxy with an explicit runtime allowlist rejected it as not-allowlisted — even though the container was only ever using the daemon default. The fix fetches the daemon'sDefaultRuntimeviaGET /infoduring the clone-prepare step and omitsHostConfig.Runtimefrom the create body when it merely restates that default. Explicitly-selected non-default runtimes (nvidia,kata,sysbox-runc) are preserved, and when/infois unavailable the runtime is left untouched (prior behaviour). TheHostConfigis shallow-cloned before editing so the inspect spec kept for rollback is not mutated. - Stuck update operations now terminalise to a non-notifying
expiredstatus instead offailed(#410). The active-TTL sweep and the startup-orphan reconciliation marked timed-outqueued/in-progressrows asfailed, which firedmarkOperationTerminal'supdate-failedlifecycle event — a false "update failed" notification that hit two distinct cases: a genuinely orphaned operation (an agent-scoped controller trigger that errored after the queued row was created but before the agent ran it) and a slow-but-successful update whose agent confirmation simply outran the 30-minute TTL. Both now resolve to a new terminal status,expired, for whichemitTerminalLifecycleEventemits nothing at all, so neither case can cry wolf. The UI treatsexpiredas terminal — the "Updating" badge clears with no failure styling and no toast. A late agent report arriving after expiry is ignored (the row is already terminal), so such an operation reads asexpiredrather than flipping to a delayed success/failure. A second, deeper instance of the same false alarm is now closed: when a duplicate update operation executes against a container that an earlier pass — or Docker Compose, or an agent — already recreated, the rename/refresh hits a genuine Docker 404 (no such container/no longer exists) or a 409 conflict (a real error, not a TTL timeout), which previously terminalised asfailedand fired the same ghost "update failed" notification. The duplicate-op path now defuses these benign post-success errors at three choke points: the dead name-based dedup fallback ingetActiveUpdateOperationForContaineris repaired so a stale-container-id duplicate is blocked up front (409 instead of silently slipping through); and bothContainerUpdateExecutor'srename()catch and theDockerlifecycle outer catch consult a sharedduplicate-op-classificationhelper that downgradesfailed→expiredonly when asucceededterminal row already exists for the same container name inside a 15-minute window. A real failure with no recent success still terminalises asfailedand notifies. Terminal-lifecycle/orphan class, sibling to #385/#386. - The fleet-stats collector now backs off on stream reconnect instead of hot-looping. When a container's Docker stats stream closed immediately on open (an exited container, a daemon error, or a malformed stream),
restartCollectionre-opened it synchronously — a tight reconnect loop bounded only by a single microtask tick that spins CPU and floods logs, once per such container. Reconnects now use capped exponential backoff (1 s doubling to a 60 s ceiling), reset to the base delay once the stream delivers data; the scheduler refuses to stack timers, skips reconnecting once a container is unwatched, and clears the pending timer on the last release. Latent defect in the new-in-1.5 fleet-stats subsystem.
- Agent-hosted update operations now carry a container snapshot and can no longer be orphaned mid-prepare.
-
#386 — Agents permanently showing 0 running containers in the controller UI (the recurrence that survived the rc.20/25/26/28 fixes). Root cause was two compounding bugs outside the handshake/snapshot machinery the earlier fixes targeted. (1) The controller's own local Docker watcher was pruning every agent's containers. Its store query
getContainers({ watcher: this.name })was scoped by watcher name only, and remote agent containers are stored under the same default watcher name (local), so each controller watch cycle treated the agents' rows as stale (their IDs are not present on the local Docker), failed toinspect()them, and deleted them — roughly every 6h until the agent was restarted. This is also why the rc.28 handshake-0 guard appeared not to work: the rows were wiped seconds before the handshake ran, so there was no "last-known state" left to preserve.Docker.getContainersnow scopescontainersFromTheStorebynormalizeAgentValue(this.agent), so a watcher only ever prunes its own agent's containers. (2) A lostdd:watcher-snapshotwas never recovered until the next 6h cron. The agent emits its authoritative snapshot only at the end of each cron; if the controller's SSE stream was silently half-open or mid-reconnect at that instant the snapshot was written to a dead socket and lost, leaving the controller empty. The agent now caches the latest snapshot per watcher and replays it to each new SSE client immediately afterdd:ack, so the controller converges on the agent's true container set on any (re)connect. This also explains why agents with many containers were immune (constant per-container SSE traffic kept the connection warm) while low-traffic agents went idle and dropped. -
#386 follow-up — six more cross-agent contamination bugs of the same class, found by a class-wide audit. The prune/snapshot fix above closed the headline symptom; an audit of every store read/key/match that scoped by watcher name or container name without also scoping by agent (the controller's own
localcontainers and a remote agent'slocalcontainers share the default watcher name) found and fixed the following further sites. These are active by default:- Post-update fast-resync (
Docker.maybeFastResyncAfterUpdate) matched the resync target bywatcher_nameand could pick a different agent's same-named container, leaving a stale "update available" badge until the next cron — the candidate set is now agent-scoped. - Deferred update reconciliation (
scheduleDeferredReconciliation) resolved the container by name only and could no-op against another agent's same-named row, leaving the local operation stuck inrollback-deferreduntil restart — it now resolves by operation/container id with an agent-scoped name fallback. - Watcher-list stats (
GET /api/watchers,GET /api/watchers/:id) keyed the per-watcher stat buckets by bare watcher name, collapsing everylocalwatcher into a single inflated bucket — they are now keyed by the unique registry id (docker.localvsml.docker.local), so each watcher row reports its own counts. - Dashboard stats aggregator (
stats/aggregator.ts) mapped agent containers to the controller's local watcher, silently dropping them from the fleet summary (or misattributing CPU/memory on a name collision) — it now resolves the agent-aware watcher id, so agent containers are no longer queried against the wrong Docker socket. - Security-scan result cache (
store/container.insertContainer) keyed cached Trivy results bywatcher_nameonly, so a remote agent's same-named container could be stamped with the controller's scan verdict (false-clean / false-alarm) — the cache (only ever written for controller-local containers) is now skipped for agent containers. - Name-based webhook routing (
POST /watch/:name,POST /update/:name) resolved the container by scanning the entire store by name and fanned the action out to all watchers; it now dispatches only to the resolved owning watcher and returns409 Conflictwhen a name is ambiguous across agents/watchers (disambiguate with?agent=/?watcher=). Single-match (single-host) behavior is unchanged.
The Home Assistant facet of this class (watcher-level sensor counts summing across agents, and discovery-entity cleanup leaking ghost entities) is corrected by the new opt-in
DD_NOTIFICATION_MQTT_<name>_HASS_AGENTTOPICSEGMENTflag, because closing it requires a change to the MQTT topic structure — see Added and Deprecated. - Post-update fast-resync (
-
The Docker image builds again after Alpine
edge/testingdropped the pinnedtrivy=0.70.0-r1package.apk addfailed withbreaks: world[trivy=0.70.0-r1], which broke the multi-arch image build (and the Cucumber E2E job that builds it). trivy is now pinned to the package name only, matching the existingcurlexception in the same layer:edge/testingrotates and drops-rNbuilds quickly and per-arch mirrors desync during the window, so an exact-rNpin is inherently fragile for multi-arch builds. -
#408 — A successful update no longer fires a spurious "new version available" notification.
handleContainerUpdateAppliedEventclears theoncenotification history after an update is applied, which re-opens theoncegate before the watcher's next scan setsupdateAvailable=false. A lingering container report still carryingupdateAvailable=true(common during "Update All") then fired a duplicate "update available" message right after "updated successfully". A per-container suppression set now withholdsupdate-availablenotifications for a just-applied container until a watcher report confirmsupdateAvailable=false, after which the key is lifted so future real updates still notify. Additionally,update-appliedandupdate-failedlifecycle notifications now bypass the semver threshold filter, so a post-updateupdateKind.kindofunknown(e.g. digest-only updates) under a non-allthreshold no longer silently drops the success/failure notice. -
#391 — A failed Docker Compose update no longer destroys the running container.
refreshComposeServiceWithDockerApipreviously removed the old container before recreating it, so any failure of the create step (e.g. the newly-pulled image is not available for the host platform) left the service with zero containers and no recovery. The update path now (1) performs a pre-flight check that the pulled image is architecture-compatible with the host and aborts before removing the old container if it is not, and (2) wraps the stop/remove → recreate sequence so that, if recreate still fails, the original container is restored from its captured spec and the original error is re-thrown. The running container is never lost on a failed update. -
#402 —
/healthnow returns503until passport authentication strategies are fully registered, closing a startup race where the port accepted connections before auth was ready (intermittent 401s on startup).registerRoutes()in the auth subsystem now callssetAuthReadyFnbeforeauth.init(), wiring the readiness gate before strategy registration begins. Deployments that hit intermittent 401 errors during the first seconds after container start should see the issue resolved. -
#386 — A fresh-restart agent whose in-memory store has not yet been re-populated no longer wipes the controller's last-known container state on handshake.
AgentClient._doHandshakenow skipspruneOldContainerswhenevercontainers.length === 0(andhasConnectedOnceis true) and emits a warning. Pruning is deferred to the next authoritativedd:watcher-snapshot, which is already gated on!containerEnumerationFailed && enrichmentErrors === 0and is therefore unambiguous. -
#386 — Agents intermittently showing 0 running containers in the controller UI (multiple recurrences across rc.20–rc.26). The complete fix spans three layers: (1) rc.20 introduced
containerEnumerationFailedinDocker.watch()to suppress snapshots whengetContainers()throws; (2) rc.25 extended suppression to per-container enrichment errors via adiagnostics.enrichmentErrorsout-parameter; (3) rc.26 added a dedicatedAgentStatsChangedevent so the controller UI also refreshes on Docker-event-driven container changes (starts/stops between cron cycles), not only completed cron cycles. -
#289 — Agent-hosted container updates no longer leave an orphaned queued operation row on the controller that the 30-minute TTL sweep force-fails into a misleading "update failed" notification. The fix threads the controller's
operationIdend-to-end:AgentTrigger.trigger/triggerBatchnow accept and forwardruntimeContext;AgentClient.runRemoteTriggerextracts per-container operationIds and includes them in the agent payload; the agent-side controllerrunTriggeraccepts and threads theoperationIdintorequestContainerUpdate; a newAgentClient.resolveAgentOperationIdhelper reuses the controller-side row when found. The controller-side queued row therefore transitions directly toin-progressandsucceeded/failedfrom the agent's lifecycle events, eliminating the spurious "update failed" notification. -
#289 — Update-applied and update-failed notification triggers and UI success toasts no longer silently drop for containers running on a connected agent. The agent's container snapshot is now threaded through
buildAgentOperationBase,ensureAgentOperationForTerminal,markAgentOperationTerminal, and related helpers, stampingagent: this.name. The store's terminal-lifecycle emit therefore naturally carries the agent's container intoemitContainerUpdateApplied/emitContainerUpdateFailed, and both the notification trigger and the SSE toast fire end-to-end on the controller for agent-originated updates. -
#289 — Container rows no longer drop sort position during an in-flight update, and every terminal outcome (succeeded / failed / rolled-back) now fires a toast. The operation display hold captures a sort-field snapshot at hold start so the row stays pinned through the docker recreate window.
-
#290 — "Updated Successfully" toast no longer drops intermittently after a container update. A new
useGlobalUpdateToastcomposable mounted once atApp.vueis the single source of truth: listens fordd:sse-update-applied/dd:sse-update-failed/dd:sse-batch-update-completed, survives route navigation, dedupes byoperationIdover a 5-minute window, and waits for the matchingdd:sse-container-added/updated/removedevent before firing. -
#291 — Dashboard update flow now fires the same toast sequence as the Containers view and shares the same
useOperationDisplayHoldcomposable, fixing the last reporter symptoms (updating row no longer drops to the bottom mid-update; dropped terminal SSE no longer leaves the dashboard silent). -
#342 — A container is no longer shown as "update available" with a blank target version after a transient registry error.
hasRawUpdateinapp/model/container.tsnow performs the tag comparison only when bothimage.tag.valueandresult.tagare defined, matching the existing guard ingetRawTagUpdate. -
#342 — GitHub release-notes lookups now survive GitHub's secondary rate limit instead of giving up on the first burst.
GithubProviderclassifies a403as a secondary rate limit only when it carries aretry-afterheader orx-ratelimit-remaining: 0, retries those, and arms a short module-level cooldown driven by GitHub's own retry hint. ThewithRetryhelper gains optionalretryPredicateandretryDelayMshooks. -
#342 — Registry routing always uses the credentialed instance when one is configured (commit
069274fe). The router now gives explicit priority to credentialed instances. The silent anonymous fallback on 401/403 is also removed; auth rejection now throws an actionable error that surfaces as the "Check failed" badge in the UI. -
#342 — The registry-error tooltip on the Containers view now names the registry that failed.
registryErrorTooltipnow derives the registry hostname from the container'sregistryUrland renders it through a new i18n string, e.g.ghcr.io — Request failed with status code 429. -
#342 — Hybrid
image:tag@sha256:digestrefs no longer trigger a spurious "Cannot get a reliable tag" warning when Docker'sRepoTagsis empty.Docker.resolveImageNamenow detects hybrid refs and parses them directly viaparse-docker-image-name; only true digest-only refs fall through to the existingresolveDigestOnlyImagepath. -
#342 — Containers list "Version" column shows the correct data for all container types. Digest-pinned containers continue to show the
sha256:abc… → sha256:def…pair; floating-tag + digest-watch containers render the human-readable tag with the digest delta in the tooltip; hybrid digest containers show the tag with the digest pair inline. -
#355 —
update-failednotifications no longer drop silently when the controller's container store races against post-failure prune.UpdateLifecycleExecutornow carries the failing container on theupdate-failedpayload, andTrigger.handleContainerUpdateFailedEventacceptspayload.containeras the primary source with the store lookup as fallback. -
#357 — Transient Trivy failures no longer wipe previously-stored scan history. The scheduler now keeps the existing record when the new result is an error and the existing record is less than 7 days old. Error results are also no longer indefinitely re-spawning fresh Trivy invocations —
scanImageWithDedupuses a 15-minute error retry floor. -
#357 / #355 — Trivy scan and SBOM no longer require
/var/run/docker.sockinside the drydock container. The forced--image-src dockerflag is removed; Trivy now uses its default source order and falls back to a registry pull when the local daemon isn't reachable. SetDD_SECURITY_TRIVY_IMAGE_SRC=remoteto skip the local-daemon probe. -
#370 — Containers list "Version" column again shows the human-readable image tag for floating-tag + digest-watch containers (rc.20 inadvertently reverted the #356 fix). The
updateKind === 'digest' && !isDigestPinnedbranch has been restored to the correct behavior:CopyableTagwith the full digest delta in the cell tooltip. -
#371 — Containers "Group By Stack" view no longer dissolves a multi-container stack into "Ungrouped" while its last container is mid-update. A new
groupAssignedSizeMapref records each group's API-assigned member count; the flatten condition now requires bothbuckets[key].length === 1andgroupAssignedSizeMap.value[key] === 1. -
#374 — Security scans no longer hand Trivy a raw registry v2 API URL. The
resolveContainerImageFullNamefallback now strips the URL scheme and the/v2path segment, matchingRegistry.getImageFullName, yielding a plainregistry-1.docker.io/image:tagreference. -
#385 — Telegram, Pushover, and other notification triggers no longer silently swallow
update-appliedandupdate-failedevents after a compose recreate or on multi-agent deployments. The fix persists a snapshot of theContaineron the operation entry at enqueue time (createAcceptedContainerUpdateRequest) andbuildTerminalLifecycleEventBasenow forwards that snapshot on the terminal-lifecycle payload. The agent SSE wire was also extended to forward the container snapshot end-to-end. -
#328 — Triggering a security scan emitted a "container update" notification instead of the security digest.
renderBatchTitle/renderBatchBody/composeBatchMessagenow honourruntimeContext.title/.bodyverbatim when set, so the security-digest path produces the configuredsecuritydigesttitle/securitydigestbodyoutput instead of update-available output. -
#317 — A notification trigger configured with
auto: falseno longer also silently loses lifecycle notifications (update-applied,update-failed,security-alert,agent-connected,agent-disconnected). Auto-fire-on-detection handlers stay gated byauto; lifecycle handlers register unconditionally. -
#317 — Update button bypassed eligibility blockers, queuing requests the API would only reject one-by-one. The API now rejects manual updates on any hard blocker with the blocker's user-friendly message; the UI locks the Update button on hard blockers and shows a confirm-modal warning on soft blockers.
-
#315 — Self-update now works against private registries whose
registry.urlis stored as the v2 API base (e.g.https://ghcr.io/v2).resolveHelperImagenow normalizes the reference to matchRegistry.getImageFullName. -
#308 — Per-row scanning chip is now correctly anchored to the container being scanned and no longer floats in viewport-fixed gutter space. The scan lifecycle uses a
useScanLifecyclecomposable maintaining ascansInFlightset keyed by container id (with a 120s safety timeout). A siblingdd-row-scanningclass provides the containing block without dimming the row. -
#305 — Hide Pinned now hides every pinned container again, matching rc.8 behavior. (#293 had carved out an exception for pinned rows with a pending update.)
-
#301 —
GET /api/containerspreloads all active update operations in a single indexed scan, replacing the rc.8 per-container 3-scan fan-out. Synthetic per-attach time drops from ~5–25 ms to ~0.02–0.04 ms on large inventories. -
#296 — Controller identity detection now runs for host-based watchers (TCP to a local socket-proxy, the common Synology / Docker Compose pattern). Set
DD_SERVER_NAMEto override. -
#293 — Hide Pinned filter no longer hides pinned containers that have a pending update. Filter decluttering is preserved for static pinned containers; rows with a pending update (
newTagtruthy) surface through the filter. -
#282 —
batch+digestmode now sends both the immediate batch email and the scheduled morning digest for each detected update, matching the documented semantics. The fix splits the digest channel off as its ownNotificationEventKind('update-available-digest') so batch-channel and digest-channel dedup are independent. -
#270 — Hide-pinned filter now uses computed
tagPinnedproperty instead of stale stored field. Unconditional startup repair ensures tagPrecision data is always correct. -
#256 — Update operation state disambiguated by container ID instead of name, preventing cross-host bleed between same-name containers on different hosts.
-
#253 — Shorthand trigger references resolved in notification rule matching; notification buffering keys stabilized; debug logging added to every silent filter path.
-
#248 — API guard against duplicate container updates (409 conflict).
-
#245 — Container update fails with 500 error when no healthcheck — Health gate now skipped when
dd.rollback.autois not set. Pre-healthy timeout usesmax(120s, dd.rollback.window)instead of a fixed value. -
#238 — Container inspect Config.Image fallback — When Docker summary only exposes a
sha256:…image ID (no RepoTags), container discovery falls back to container inspectConfig.Imageto recover the original tagged reference. -
#229 / #228 — Spurious SMTP emails after update —
clearDetectedUpdateState()now clears rawresult/updateKinddata instead of the derivedupdateAvailableboolean. -
#223 — Dashboard layout customizations lost on page reload — Added
gridLayouttoPreferencesSchema; reorder now usesloadPersistedLayout. -
#222 — Dashboard customize panel responsive on mobile — Panel is opt-in on mobile (sliders icon to open), full-screen overlay with backdrop dismiss.
-
#217 — Dashboard Resource Usage widget minimum height raised from 3 grid units to 7 grid units so per-container CPU and Memory lists stay visible.
-
#213 — Dashboard Host Status widget no longer auto-scrolls to the last host when the host list changes. The scroll-snap classes, dynamic tail-spacer element, and measurement machinery have all been removed.
-
#208 — Dashboard updates widget 6-item cap removed — Removed hard-coded
RECENT_UPDATES_LIMITthat silently dropped entries beyond 6 in the Updates Available widget. -
#202 — CalVer zero-padded month in strict family filter — Tags like
2026.02.0were rejected when the current tag was2025.11.1because zero-padded single digits were treated as a family mismatch. -
#200 — Dashboard widget mobile scroll — Added
overscroll-containto all scrollable dashboard widgets. -
#192 — Digest-only image visibility — Watchers no longer silently drop containers with digest-only image references.
-
#186 — Registry failures in Updates Available widget — Containers with "check failed" status no longer appear in the dashboard "Updates Available" section.
-
#183 — Cascading -old container updates — "Update All" batch no longer triggers updates on containers renamed with
-old-{timestamp}suffix during a prior update. -
#182 — Podman pod infra containers skipped — Watchers now skip Podman pod infrastructure containers that have an empty
Imagefield. -
#180 — Duplicate containers after recreate — Three-layer deduplication filtering prevents alias containers from entering the store during Docker recreate cycles.
-
#156 — Container alias name canonicalization —
getContainerName()now strips Docker recreate alias prefixes (e.g.8bf70beac570_termix→termix) before the name enters the store, so all triggers receive canonical names. MQTT Home Assistant sensor preserved during recreate (replacementExpectedflag prevents premature empty retained discovery payload). -
Discussion #295 — Release-notes icon in the container table now always opens the same popover, even when only an external release URL is available. The popover now renders uniformly for both cases.
-
discussion #295 —
DD_SESSION_SECRETno longer crashes startup when unset — The fallback is now a persisted secret: on first boot withoutDD_SESSION_SECRETset, drydock generates 64 random bytes and writes them to the store. Subsequent boots read the persisted value. (See also theChangedentry above.) -
#362 — SSE reconnect exponential backoff no longer collapses to a flat 1 s loop when the agent is struggling. The backoff now only resets after the stream has stayed open for
SSE_STABLE_CONNECTION_MS(30 s). -
AgentClienttimers are now cleared when an agent is removed, preventing orphaned timeouts (commit03bf7211). A new idempotentstop()method cancels bothstableConnectionTimerandreconnectTimer. -
#368 — OIDC custom-dispatcher paths (cafile /
DD_AUTH_OIDC_*_INSECURE=true) no longer fail with an opaqueTypeError: fetch failedon Node 24. The fix importsfetchfromundiciand uses it whenever a custom dispatcher is required so both halves share the same dispatcher version. -
OIDC warn logs now surface the full
error.causechain, making TLS and DNS failures actionable (commit720d99a3). -
OIDC error logs now redact RFC-1918 IP addresses and absolute filesystem paths (commit
9b79de77). -
OIDC SSO broken after upgrade/restart — OIDC discovery made lazy so startup failure no longer drops the provider. Redirect, callback, and token paths retry on first use. (#246)
-
Image reference construction — unanchored
/v2strip could silently corrupt references when the image name contained a/v2path segment. The fix extracts a shared pure helperbuildImageReference(app/registries/image-reference.ts) that cleans the registry URL before concatenation using anchored regexes. -
ECR stale auth token cache write avoided on concurrent key change. The cache write is now keyed on the credentials snapshot captured at request start.
-
Icon proxy serves fallback image on upstream CDN timeout or 5xx. Non-existence failures now route through the existing fallback path and serve the placeholder image.
-
SBOM endpoint returns
503instead of500when the security scanner is disabled. -
Malformed
dd.tag.transformregex patterns — The regex-transform label validator now throws at config time for malformed or oversized patterns. -
dd.registry.lookup.imagelabel no longer corrupts deploy identity (commit594a07e8, fixes #336).normalizeContainerno longer overwritesimage.name/image.registry.url; a newgetImageForRegistryQueryhelper applies the substitution only at each query boundary. -
Password-manager autofill restored on login form (commit
3abe2fa6, fixes #335). Username and password inputs now carrynameandidattributes. -
security-scan-skippedaudit row now fires when the gate is disabled globally (commitae24e0a9). -
Command action security warning updated to canonical
DD_ACTION_COMMAND_*prefix (commitaa5fc98d). -
Docker event history pruning amortized to reduce per-event splice cost (commit
d6690cc8). The threshold is now2×maxEntries, so splices are amortized across many appends. -
Agent container list no longer shares mutable LokiJS references (commit
1f7d8034). The handler now clones each container viacloneContainerbefore stripping metadata. -
Docker multi-arch build no longer fails when Alpine repos drift between archs. The curl entry is now unpinned so apk installs whatever's current per-arch.
-
Telegram MarkdownV2 escaping in all trigger paths — Body text in
trigger()andtriggerBatch()was not escaped for MarkdownV2 reserved characters, causing Telegram API 400 errors and silent notification failures. (Discussion #211) -
#310 — Restored the
[server]/[agent]prefix on default notification body templates that rc.10 had stripped. -
#309 — Status column in the Containers list now shows its label alongside the icon at typical widths.
-
#296 — Notification server-name prefix no longer renders the container ID on Docker Compose setups.
-
#283 — Duplicate server name in notification prefix and suffix suppressed.
-
#271 — Log sort order persists across navigation.
-
#265 — Stale update detection suppressed from pre-clear watcher scans.
-
#323 — Popovers on the Containers list rendered off-screen for the last row + drifted on scroll — Added a
buildPopoverStylehelper that measures available space and flips the popover upward when needed; added a global scroll listener that closes both popovers. -
Watcher "Last Run" display — Watchers page now shows relative timestamps for last run. (#189)
-
#187 — Agent column picker positioning fixed.
-
#184 — Dashboard confirm dialog —
ConfirmDialogmoved to global app shell so update prompts from the dashboard appear immediately. -
Stack/group view no longer collapses to ungrouped mid-update when containers are recreated.
loadGroups()now indexes the map under id, name, AND displayName. -
#340 — Self-update no longer preserves stale Drydock version metadata. The self-update clone path now drops image-inherited environment variables and labels from the old image when the target image changed them.
-
#345 — Host names with numeric suffixes no longer lose the differentiating character in the Containers table.
-
Truncated release notes body now marked with trailing ellipsis (commit
3a9bd098). -
Accepted update dispatch failures now logged (commit
674a0ed8). -
Row update overlays anchored to first data cell width (commit
4bdb8d65). -
Update state lost on navigation/refetch — Backend list endpoint now enriches containers with in-progress update operation state.
-
Digest-only image visibility — Watchers no longer silently drop containers with digest-only image references. Digest watch now stays enabled when Docker summary exposes a
sha256:…image but container inspect recovers a tagged reference. -
Same-name container update holds isolated to the correct instance (commit
02433a02). Added anidentityKeydiscriminator so the hold follows the container's stable identity through id changes. -
Security view container chooser traps keyboard focus (commit
e98603c1). Added standard focus-trap: focus is moved to the first focusable element on open, Tab/Shift+Tab cycle within the popover, Escape returns focus to the previously-focused element. -
Legacy
xlink:hrefSVG attributes stripped by icon sanitizer (commit0309bacb).hrefis now in the allowlist; the deprecatedxlink:hrefform remains blocked. -
Floating semver aliases excluded from greater-than check (commit
0b9eaaf3).isGreaterCandidateTagnow requires strictly greater semver in one direction and not-greater in the reverse, so floating aliases like3.3and3.3.0drop out of the candidate set entirely.
Security
- Mau registry auth scope is now percent-encoded.
Mau.authenticateinterpolatedimage.nameinto the JWT authscopequery parameter without encoding, diverging from the parent Gitlab provider which encodes it; a URL-significant character in an image name could corrupt the query or inject extra parameters. The scope is now encoded identically to Gitlab.
The following entries are hardening from a 2026-06-01 multi-agent security review (no critical/high findings).
-
OIDC UserInfo response is bound to the id_token subject on login (commit
661c21b9). The authorization-code login path calledfetchUserInfowithskipSubjectCheck, omitting the OIDC Core 5.3.2 check that the UserInfosubmatches the tokensub. The validated id_token subject is now enforced; the bearer-token path (no id_token, no reliable expected subject) is unchanged. -
CSRF same-origin check no longer trusts forged
X-Forwarded-Host(commita132318e).getExpectedOriginreadX-Forwarded-Host/-Protounconditionally, so a client could forge them to satisfy the mutation same-origin check even withtrust proxydisabled. The forwarded host is now honored only when Expresstrust proxyis enabled, the protocol derives from the already-gatedreq.protocol, and theHostport is preserved for non-standard-port deployments. Upgrade note: this aligns enforcement with the long-documented requirement to setDD_SERVER_TRUSTPROXYbehind a TLS-terminating reverse proxy (reverse-proxy setup). A deployment that terminated TLS at a proxy but never setDD_SERVER_TRUSTPROXYpreviously worked only because the unconditional header trust masked the misconfiguration; it must now setDD_SERVER_TRUSTPROXY(hop count, e.g.1) or state-changing requests will return403. -
GitHub release-notes token withheld for untrusted source repos (commit
7186195c). Add.source.repocontainer label (settable by anyone who controls the container) could redirect the operator's release-notes token — including the GHCR PAT fallback — to an arbitrary GitHub repo. Source-repo resolution now carries provenance, and no token is attached when the repo originates from that per-deployment label; image-label, OCI-label, GHCR-path and Docker Hub sources remain trusted. A follow-up (commit0a014304) also segregates the release-notes cache by trust (#auth/#anon), so an untrusted not-found result can no longer suppress a later trusted, token-bearing fetch for the same repo. -
Playwright E2E image pinned by digest (commit
8234ef33).mcr.microsoft.com/playwright:v1.60.0-nobleis now pinned by itssha256digest so a mutated/republished tag cannot silently change the image the E2E job pulls and runs. -
Container-query filter values constrained to primitives (commit
a5ae7a89). Express'sqsparser turns?key[$regex]=…into a nested object, and the store's query-sanitizer only guarded prototype-pollution keys — so an authenticated container-list request could inject a LokiJS$regex(native RegExp → ReDoS, bypassing the project's re2js immunity) or$ne/$gtoperators. Exact-match filter values are now restricted to primitives at the store choke point, neutralizing operator injection for every caller. -
Security digest templates no longer evaluated as JavaScript (commit
e74fc56d).renderSecurityDigestTemplaterendered the operator-suppliedSECURITYDIGESTTITLE/SECURITYDIGESTBODYvianew Function('scan', …), executing them as a template literal — arbitrary code execution in the digest path. The renderer now routes through the same sandboxed${…}interpolation engine as every other trigger template; the per-severity lists and plural noun the default template relied on are pre-computed in code and exposed asscan.criticalList/scan.highList/scan.containerNoun, so default output is unchanged. The digest feature shipped only in the v1.5.0 RC train, so this is a within-cycle hardening; any RC user who set a custom digest template using JS expressions (arrow functions, array methods, arithmetic) should switch to the documented${scan.*}variables, as unsupported expressions now resolve to an empty string instead of running. -
Template method-call arguments split on top-level commas only (commit
c485f412).safeInterpolatesplit method arguments on every comma, so a comma inside a quoted string literal or nested parentheses (e.g.${x.replace(',', ';')}) was mis-parsed; argument splitting now respects quotes and parentheses, hardening the template parser the digest renderer depends on. -
Registry token-fetch requests now honor operator TLS settings (commit
dfbbd159). GAR, GitLab, Mau, DHI and public-ECR built their own token-fetch request and calledaxios()directly, bypassingwithTlsRequestOptions()— so the credential exchange ignored the configuredcafile/insecure/ client-cert and validated against the system trust store. The shared TLS helper is now applied to those token fetches (Ecrwas realigned to extendBaseRegistry). -
CSRF same-origin enforcement extended to authenticated
/authmutations (commitd1611f88). The/authrouter sat outside the middleware chain that runsrequireSameOriginForMutations, leaving bodylessPOST /auth/logoutandPOST /auth/rememberwithout same-origin protection (exploitable underDD_SERVER_COOKIE_SAMESITE=none). -
OIDC username falls back to the
subclaim beforeunknown(commit8e00dfd3). Identities without anemailclaim previously collapsed to a singleunknownusername sharing one session-eviction bucket. -
schemaVersion-1 manifest parsing guarded (commit
fee0bbcc). A malformed/missingv1Compatibilityfrom a registry threw an unhandled error that silently dropped the container from the watch cycle; it is now fully optional-chained and wrapped with a descriptive error. -
Diagnostic debug dump redacts plural
*_TOKENS_*env vars (commit238727f5). The redaction set matchedtokenbut nottokens, soDD_SERVER_WEBHOOK_TOKENS_*values printed in plaintext. -
Agent HTTP server is rate-limited before authentication (commit
8cddaeab). The shared-secret-gated agent endpoints had no throttle on repeated failed attempts; a 60s/300 limiter now sits ahead of the auth middleware (/healthexempt; the long-lived SSE stream counts once on open). -
Basic-auth string comparison no longer leaks length via timing (commit
6ff55463).timingSafeEqualStringearly-exited on length mismatch; both operands are now hashed to a fixed-length sha256 digest beforetimingSafeEqual, matchingverifyShaPassword. -
Quay pagination tokens percent-encoded (commit
a19e0102).next_page/lastvalues parsed from the registryLinkheader were appended to the request URL unencoded, allowing query-parameter injection from a malicious/buggy registry response. -
Startup warning when falling back to a store-persisted session secret (commit
9431c1d2). WhenDD_SESSION_SECRETis unset, a one-time warning now recommends setting it explicitly and keeping the store directory non-world-readable. -
Startup warning when
trust proxyis set to booleantrue(commit71eac008).DD_SERVER_TRUSTPROXY=truetrusts allX-Forwarded-Forhops, letting clients spoofreq.ipand evade per-IP login lockout; the warning steers operators to a hop count (=1). -
TCP Docker host is validated before the self-update controller passes it to Dockerode (commit
441b4358).validateTcpDockerHostrejects values that contain a URL scheme prefix, a userinfo segment, whitespace, or path separators, throwing a descriptive error before any network connection is attempted. -
Proxied SVG icons sanitized before caching (commit
54d93a3b). SVG payloads fetched from upstream icon CDNs are run through an allowlist-based sanitizer before being written to the icon cache. -
Command action trigger env values sanitized to strip shell metacharacters (commit
1113d8ca). Container-derived values injected into the command subprocess environment are now stripped of shell metacharacters ($,`,;,(,)) before the environment map is passed toexecFile. -
Credential redaction expanded to
x-registry-auth,*-token, andapi-keyfields (commit4417ce25). A second regex pass now redactsx-registry-auth, any field matching*-token, andapi-key/api_keyvalues before the payload leaves the server. -
Credential status pattern matching uses RE2 (commit
df9b914a).BaseRegistry.getRejectedCredentialStatusnow usesRE2JS.compile(…)to maintain the project-wide ReDoS immunity guarantee. -
Registry instances using
insecure=truenow log a warning on every request (commitcd14e3a9). -
DD_SESSION_SECRETis now required; auto-generated and persisted on first boot when unset. (SeeChangedentry above for the full history of rc.20 vs rc.21.) -
Agent connections over plain HTTP with a configured secret are now rejected at startup (commit
7c6f6c20). Use HTTPS or a TLS-terminating proxy for agent connections that require a shared secret. -
GHCR token fallback treats whitespace-only tokens as missing (commit
711d583c). All token checks now call.trim().length > 0. -
GHSA-xq3m-2v4x-88gg (critical) — Bumped
protobufjs7.5.4 → 7.5.5 to close the prototype-chain arbitrary-code-execution advisory. -
GHSA-r4q5-vmmm-2653 (medium) — Bumped
follow-redirectsto 1.16.0 to close the custom-auth-header cross-domain leak advisory. -
Hook command grammar validator — User-supplied pre-update / post-update hook commands are now validated against a restricted shell-safe grammar at config time.
-
OIDC authorization endpoint strict match — The OIDC flow now requires an exact match against the discovered authorization endpoint.
-
OIDC token redaction in error logs — Error log lines from the OIDC pipeline redact bearer / id / refresh tokens.
-
Rate-limit key derivation — Unauthenticated rate-limit buckets now key on
socket.remoteAddressin preference torequest.ip, eliminating theX-Forwarded-Forspoof-ability. -
CORS origin required when enabled — Enabling CORS now requires
DD_SERVER_CORS_ORIGINto be set explicitly. -
OIDC redirect target allowlist — Post-login redirects are now validated against the backend's endpoint allowlist.
-
Healthcheck HTTPS probe hardening —
/bin/healthcheckno longer usespopen()with shell command interpolation. The probe now locates the openssl binary explicitly, fork/execs it with pipes, and uses poll-driven I/O with SIGPIPE handling. -
SSE log IP hashing with opt-in raw mode — SSE connect/disconnect lines log a salted hash of the source IP (
h:xxxxxxxx) by default. SetDD_SSE_DEBUG_LOG_IP=trueto temporarily log raw IPs. -
HTTP trigger proxy URL scheme validation — HTTP trigger proxy URLs must now be
http://orhttps://schemes. -
Vulnerability CSV export escape hardening — Every CSV field is now quoted unconditionally, and tab/CR leading characters are escaped alongside
=+-@. -
Vite CVE patches — Updated vite to 8.0.7 (ui) and 7.3.2 (demo) to fix CVE-2026-39363, CVE-2026-39364, CVE-2026-39365.
-
Axios CVE-2025-62718 — Updated axios 1.13.6 → 1.15.0.
-
fast-xml-parser override 5.5.8 → 5.7.1 — Addresses GHSA-gh4j-gqv2-49f6 / CVE-2026-41650.
-
uuid 13.0.0 → 14.0.0 — Addresses GHSA-w5hq-g745-h8pq.
-
fast-xml-parserupgraded to 5.5.8 — Addresses CVE-2026-33349 (numeric entity expansion bypass). -
Log injection prevention — Removed version string interpolation from startup and migration log messages.
-
Reflected XSS in Podman redirect guard — 404 handler no longer reflects request URL in response body.
-
WebSocket origin and lockout hardening — Added stricter WebSocket origin validation and safer lockout file-permission handling.
-
Agent log entry sanitization — Agent log proxy endpoint now uses an allowlist-based normalizer that only forwards known fields.
-
Security bouncer enforcement on container updates — Update and Update All actions now enforce the security bouncer gate.
-
Vulnerability URL and CSV sanitization — Vulnerability URLs validated before rendering; CSV export fields sanitized against formula injection.
-
Snyk policy file — Added a repo-level
.snykfile for reviewed false-positive Snyk Code findings. -
Supply-chain toolchain refresh — Bumped pinned Alpine edge/testing package versions for
cosignandtrivyin the Dockerfile. -
Binary indices and drain concurrency cap for notification outbox (commit
9393253e).findReadyForDeliveryfields switched to binary indices for O(log n) B-tree lookups.OutboxWorkergains amaxDrainConcurrencyoption (default 10) backed by aDrainSemaphore.
Dependencies
-
Vite 7.3 upgraded to 8.0 — Migrated to Vite 8.0 with Rolldown bundler.
-
Patch/minor dependency bumps — Updated all patch/minor dependencies and upgraded knip to v6.
-
Vulnerable transitive dependency patches — nodemailer 8.0.3→8.0.4, picomatch→4.0.4, brace-expansion→5.0.5, smol-toml→1.6.1, yaml→2.8.3, next 16.2.1→16.2.2 (CVE-2025-59472), lodash 4.17.23→4.18.1 (CVE-2026-2950, CVE-2026-4800).
-
Pinned
piniaandvue-i18nto exact versions (commitfd0b02a5). Both were the only^-ranged UI dependencies; pinned to the locked3.0.4/11.4.2to match the exact-pinning discipline used everywhere else. -
re2js 1.2.3 → 2.8.3 (major). Upgraded the ReDoS-safe RE2 regex engine behind
safeRegExp()(tag include/exclude/transform) across the1.x → 2.xboundary. The only 2.0.0 breaking change — native-ECMAScriptreplaceAll/replaceFirstreplacement semantics — is not exercised by drydock; the compiledcompile/matcher/find/groupsurface is unchanged and Node ≥24 satisfies re2js 2.x's Node ≥18 floor. All 298 tag/regex tests pass unchanged. -
Runtime/security dependency bumps — helmet 8.1.0→8.2.0, undici 8.2.0→8.3.0, ws 8.20.1→8.21.0, axios 1.16.0→1.16.1 (dependency + override), express-rate-limit 8.5.1→8.5.2, nodemailer 8.0.7→8.0.10, semver 7.8.0→7.8.1.
Documentation
-
v1.5.0 deprecation sweep. Migrated every documentation example and test fixture off the v1.5.0-deprecated
DD_TRIGGER_*/dd.trigger.*prefixes onto canonicalDD_NOTIFICATION_*+dd.notification.*(messaging providers) andDD_ACTION_*+dd.action.*(update executors). Touched 29 files incontent/docs/current/**, the in-repo README roadmap, CONTRIBUTING, and all QA/CI/demo compose fixtures. -
Guide/API endpoint synchronization — Updated current docs and guides to consistently use canonical
/api/v1/*paths, expanded container list API docs, and added dashboard customization guide. -
#342 follow-up — Registry env-var naming convention now explained in the registries index. A new "Naming registry instances" callout explains that the
{REGISTRY_NAME}placeholder is a user-chosen label that namespaces multiple instances of the same registry type. -
#342 follow-up — Watcher cron callout explains rate-limit interaction with hourly polling.