github CodesWhat/drydock v1.5.0-rc.29

pre-release4 hours ago

v1.5.0-rc.29

Full Changelog: v1.5.0-rc.28...v1.5.0-rc.29

[1.5.0-rc.29] — 2026-05-31

Added

  • Trigger environment variable taxonomy split — DD_ACTION_* and DD_NOTIFICATION_* prefixes. Action triggers (Docker, Docker Compose, Command) are now configured with DD_ACTION_* and dd.action.* labels; notification/messaging triggers (Slack, SMTP, Discord, Telegram, ntfy, Pushover, and all others) are configured with DD_NOTIFICATION_* and dd.notification.* labels. All three prefix families (DD_ACTION_*, DD_NOTIFICATION_*, DD_TRIGGER_*) are interchangeable at runtime — merge priority is DD_NOTIFICATION_* > DD_ACTION_* > DD_TRIGGER_*. A migration CLI (drydock config migrate --source trigger) rewrites DD_TRIGGER_*, dd.trigger.include, and dd.trigger.exclude to action-prefixed aliases automatically; use --dry-run to preview changes before applying.

  • 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.

  • 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 matching password|token|secret|key|hash are automatically redacted. Configurable time window (1–1440 minutes). (Phase 4.14)

  • Container log streaming APIWS /api/v1/containers/:id/logs/stream endpoint 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 APIGET /api/v1/containers/:id/logs endpoint with gzip compression support, stdout/stderr filtering, configurable tail size, and timestamp-based since filtering.

  • Debug dump APIGET /api/v1/debug/dump endpoint with configurable minutes query 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). New ContainerStatsAggregator polls each locally-monitored container once per tick (default 10 s) and computes a fleet-wide ContainerStatsSummary (total CPU%, total memory, top-N rows). Two new endpoints — GET /api/v1/stats/summary and GET /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=false workaround) where the widget showed zeros because the per-container cache was never warmed. The legacy GET /api/v1/containers/stats endpoint and the client-side summarizeContainerResourceUsage rollup have been removed.

  • Per-container update locks (commit 761fb834). New keyed LockManager primitive in app/updates/lock-primitives.ts replaces the module-level pLimit(1) that was serialising every container update across the entire process. Lock keys are derived per container (and per compose project for Dockercompose), 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 in app/store/update-operation.ts is now selective: status=queued operations stay queued for the recovery dispatcher to pick up, and phase=pulling rows are reset to queued (pull is idempotent). A new app/updates/recovery.ts module runs once after registry.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). New notificationOutbox LokiJS collection and app/notifications/outbox-worker.ts background 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/outbox REST surface lets operators list entries, retry from the DLQ, or discard.

  • Notification outbox UI (commit feature/v1.5-rc17). New Notification outbox page (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/cancel now accepts both queued and in-progress operations. Queued ops are marked failed immediately; in-progress ops are flagged via a cancelRequested field 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. Default 0 = unlimited. Positive integer N means 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-emits phase: 'health-gate' at a configurable interval (default 10 s). DD_UPDATE_HEALTH_GATE_HEARTBEAT_MS=0 disables heartbeats; values below 1000 ms or non-integers fail fast at startup.

  • Self-update now works when Drydock reaches the Docker daemon over a TCP host, not only through a bind-mounted /var/run/docker.sock (commit fc34ffb9). resolveHelperDockerConnection now 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 unavailable indicator when Drydock cannot update itself in the current deployment (commit cf777280). A new hard self-update-unavailable update-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_SECRET escape hatch for closed-LAN deployments. rc.20 tightened the agent-secret-over-HTTP check to a hard error. rc.21 introduces DD_AGENT_ALLOW_INSECURE_SECRET=true as 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 a security-scan-cycle-complete event. Triggers can configure SECURITYMODE=digest (or batch+digest) to receive one summary per cycle. Templates are customizable via SECURITYDIGESTTITLE / SECURITYDIGESTBODY. (#300)

  • Opt-in scheduled-scan notifications — New DD_SECURITY_SCAN_NOTIFICATIONS=true flag enables security-alert event emission from scheduled scans. Default is false; on-demand scans always emit.

  • Bulk security scan endpointPOST /api/v1/containers/scan-all scans 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 a Last-Event-ID header receive every event they missed; if the buffer has evicted the requested id the client receives a dd:resync-required event.

  • 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/:id endpoint — 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 new withRetry helper wraps every registry HTTP call: on 429 or 503 it honors the upstream Retry-After header, 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.source OCI label, dd.source.repo override 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 so once=true dedup survives process restarts.

  • Floating tag detection and UI indicator — New tagPrecision classifier (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+digest to 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 /metrics endpoint — Set DD_SERVER_METRICS_TOKEN to authenticate Prometheus scrapers via Authorization: Bearer <token>.

  • Disable default local watcher — Set DD_LOCAL_WATCHER=false to 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 via DD_SERVER_NAME. Custom templates can use container.notificationServerName and container.notificationAgentPrefix.

  • Infrastructure update modedd.update.mode=infrastructure label 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 by import.meta.glob in boot/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, and AppTabBar. (Discussion #199)

  • Podman API version negotiation — Docker watcher probes the daemon's /version endpoint over the Unix socket and pins Dockerode to the reported API version. Prevents EAI_AGAIN crashes caused by docker-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.html carries a short revalidation header.

Changed

  • Default watcher cron relaxed from hourly to every 6 hours (#342 follow-up). app/watchers/providers/docker/Docker.ts now defaults cron to 0 */6 * * * (every 6 hours) instead of 0 * * * * (hourly). Users who set DD_WATCHER_{name}_CRON explicitly are unaffected. Users who want near-real-time detection can still set DD_WATCHER_{name}_CRON=0 * * * *.

  • Action trigger default mode — Action triggers (docker, dockercompose, command) now default to AUTO=oninclude instead of AUTO=all, requiring an explicit dd.action.include label 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: findDockerSocketBind runs 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_SECRET auto-generated and persisted when unset. On first boot without DD_SESSION_SECRET set, drydock generates 64 random bytes and writes them to a secrets collection 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.runUpdateAvailableSimpleTrigger and runAcceptedUpdateBatch no longer await runAcceptedContainerUpdates, 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 shared DataTable component 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.yml and release-from-tag.yml into 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) (commit f0989301). 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.maybeEmitHighSeverityAlert now 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 refactorflushDigestBuffer / shouldHandleDigestContainerReport are 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-reconnect notification rule that fires when a remote agent reconnects after losing connection. Disabled by default.

  • app/updates/locks.ts renamed to app/updates/lock-primitives.ts (commit 4c506d21). 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-connected the dashboard now refetches only endpoints that can go stale between frames; dd:sse-resync-required still forces a full fan-out.

Deprecated

  • DD_TRIGGER_* environment variable prefix and dd.trigger.* container labels (deprecated v1.5.0, removal targeted v1.7.0). Use DD_ACTION_* / dd.action.* for update-action triggers (Docker, Docker Compose, Command) and DD_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. See DEPRECATIONS.md for the full schedule.

  • dd.action.include / dd.action.exclude (and legacy dd.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.

  • curl healthcheck overridecurl is 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).

Removed

  • Experimental eligibilityPills.{showSoft,deemphasizeSoft} preferences dropped before release. The de-emphasis is now baseline behavior with no toggle.

  • Experimental containers.showAutoUpdateDiagnostic preference + compact variant of UpdateEligibilityBadges dropped before release.

  • Two pre-existing unused imports flagged by biome's noUnusedImports (updates/request-update.ts default Trigger import; Docker.containers.processing-retrieval.test.ts mockGetFullReleaseNotesForContainer import).

  • Legacy GET /api/v1/containers/stats endpoint and summarizeContainerResourceUsage client-side rollup removed; superseded by the new fleet-aggregate stats subsystem (GET /api/v1/stats/summary).

Fixed

  • #391 — A failed Docker Compose update no longer destroys the running container. refreshComposeServiceWithDockerApi previously 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/health now returns 503 until 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 calls setAuthReadyFn before auth.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._doHandshake now skips pruneOldContainers whenever containers.length === 0 (and hasConnectedOnce is true) and emits a warning. Pruning is deferred to the next authoritative dd:watcher-snapshot, which is already gated on !containerEnumerationFailed && enrichmentErrors === 0 and 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 containerEnumerationFailed in Docker.watch() to suppress snapshots when getContainers() throws; (2) rc.25 extended suppression to per-container enrichment errors via a diagnostics.enrichmentErrors out-parameter; (3) rc.26 added a dedicated AgentStatsChanged event 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 operationId end-to-end: AgentTrigger.trigger / triggerBatch now accept and forward runtimeContext; AgentClient.runRemoteTrigger extracts per-container operationIds and includes them in the agent payload; the agent-side controller runTrigger accepts and threads the operationId into requestContainerUpdate; a new AgentClient.resolveAgentOperationId helper reuses the controller-side row when found. The controller-side queued row therefore transitions directly to in-progress and succeeded/failed from 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, stamping agent: this.name. The store's terminal-lifecycle emit therefore naturally carries the agent's container into emitContainerUpdateApplied / 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 useGlobalUpdateToast composable mounted once at App.vue is the single source of truth: listens for dd:sse-update-applied / dd:sse-update-failed / dd:sse-batch-update-completed, survives route navigation, dedupes by operationId over a 5-minute window, and waits for the matching dd:sse-container-added/updated/removed event before firing.

  • #291 — Dashboard update flow now fires the same toast sequence as the Containers view and shares the same useOperationDisplayHold composable, 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. hasRawUpdate in app/model/container.ts now performs the tag comparison only when both image.tag.value and result.tag are defined, matching the existing guard in getRawTagUpdate.

  • #342 — GitHub release-notes lookups now survive GitHub's secondary rate limit instead of giving up on the first burst. GithubProvider classifies a 403 as a secondary rate limit only when it carries a retry-after header or x-ratelimit-remaining: 0, retries those, and arms a short module-level cooldown driven by GitHub's own retry hint. The withRetry helper gains optional retryPredicate and retryDelayMs hooks.

  • #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. registryErrorTooltip now derives the registry hostname from the container's registryUrl and renders it through a new i18n string, e.g. ghcr.io — Request failed with status code 429.

  • #342 — Hybrid image:tag@sha256:digest refs no longer trigger a spurious "Cannot get a reliable tag" warning when Docker's RepoTags is empty. Docker.resolveImageName now detects hybrid refs and parses them directly via parse-docker-image-name; only true digest-only refs fall through to the existing resolveDigestOnlyImage path.

  • #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.

  • #355update-failed notifications no longer drop silently when the controller's container store races against post-failure prune. UpdateLifecycleExecutor now carries the failing container on the update-failed payload, and Trigger.handleContainerUpdateFailedEvent accepts payload.container as 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 — scanImageWithDedup uses a 15-minute error retry floor.

  • #357 / #355 — Trivy scan and SBOM no longer require /var/run/docker.sock inside the drydock container. The forced --image-src docker flag is removed; Trivy now uses its default source order and falls back to a registry pull when the local daemon isn't reachable. Set DD_SECURITY_TRIVY_IMAGE_SRC=remote to 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' && !isDigestPinned branch has been restored to the correct behavior: CopyableTag with 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 groupAssignedSizeMap ref records each group's API-assigned member count; the flatten condition now requires both buckets[key].length === 1 and groupAssignedSizeMap.value[key] === 1.

  • #374 — Security scans no longer hand Trivy a raw registry v2 API URL. The resolveContainerImageFullName fallback now strips the URL scheme and the /v2 path segment, matching Registry.getImageFullName, yielding a plain registry-1.docker.io/image:tag reference.

  • #385 — Telegram, Pushover, and other notification triggers no longer silently swallow update-applied and update-failed events after a compose recreate or on multi-agent deployments. The fix persists a snapshot of the Container on the operation entry at enqueue time (createAcceptedContainerUpdateRequest) and buildTerminalLifecycleEventBase now 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 / composeBatchMessage now honour runtimeContext.title / .body verbatim when set, so the security-digest path produces the configured securitydigesttitle / securitydigestbody output instead of update-available output.

  • #317 — A notification trigger configured with auto: false no longer also silently loses lifecycle notifications (update-applied, update-failed, security-alert, agent-connected, agent-disconnected). Auto-fire-on-detection handlers stay gated by auto; 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.url is stored as the v2 API base (e.g. https://ghcr.io/v2). resolveHelperImage now normalizes the reference to match Registry.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 useScanLifecycle composable maintaining a scansInFlight set keyed by container id (with a 120s safety timeout). A sibling dd-row-scanning class 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.)

  • #301GET /api/containers preloads 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_NAME to 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 (newTag truthy) surface through the filter.

  • #282batch+digest mode 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 own NotificationEventKind ('update-available-digest') so batch-channel and digest-channel dedup are independent.

  • #270 — Hide-pinned filter now uses computed tagPinned property 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.auto is not set. Pre-healthy timeout uses max(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 inspect Config.Image to recover the original tagged reference.

  • #229 / #228 — Spurious SMTP emails after updateclearDetectedUpdateState() now clears raw result/updateKind data instead of the derived updateAvailable boolean.

  • #223 — Dashboard layout customizations lost on page reload — Added gridLayout to PreferencesSchema; reorder now uses loadPersistedLayout.

  • #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_LIMIT that silently dropped entries beyond 6 in the Updates Available widget.

  • #202 — CalVer zero-padded month in strict family filter — Tags like 2026.02.0 were rejected when the current tag was 2025.11.1 because zero-padded single digits were treated as a family mismatch.

  • #200 — Dashboard widget mobile scroll — Added overscroll-contain to 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 Image field.

  • #180 — Duplicate containers after recreate — Three-layer deduplication filtering prevents alias containers from entering the store during Docker recreate cycles.

  • #156 — Container alias name canonicalizationgetContainerName() now strips Docker recreate alias prefixes (e.g. 8bf70beac570_termixtermix) before the name enters the store, so all triggers receive canonical names. MQTT Home Assistant sensor preserved during recreate (replacementExpected flag 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 #295DD_SESSION_SECRET no longer crashes startup when unset — The fallback is now a persisted secret: on first boot without DD_SESSION_SECRET set, drydock generates 64 random bytes and writes them to the store. Subsequent boots read the persisted value. (See also the Changed entry 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).

  • AgentClient timers are now cleared when an agent is removed, preventing orphaned timeouts (commit 03bf7211). A new idempotent stop() method cancels both stableConnectionTimer and reconnectTimer.

  • #368 — OIDC custom-dispatcher paths (cafile / DD_AUTH_OIDC_*_INSECURE=true) no longer fail with an opaque TypeError: fetch failed on Node 24. The fix imports fetch from undici and uses it whenever a custom dispatcher is required so both halves share the same dispatcher version.

  • OIDC warn logs now surface the full error.cause chain, making TLS and DNS failures actionable (commit 720d99a3).

  • 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 /v2 strip could silently corrupt references when the image name contained a /v2 path segment. The fix extracts a shared pure helper buildImageReference (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 503 instead of 500 when the security scanner is disabled.

  • Malformed dd.tag.transform regex patterns — The regex-transform label validator now throws at config time for malformed or oversized patterns.

  • dd.registry.lookup.image label no longer corrupts deploy identity (commit 594a07e8, fixes #336). normalizeContainer no longer overwrites image.name / image.registry.url; a new getImageForRegistryQuery helper applies the substitution only at each query boundary.

  • Password-manager autofill restored on login form (commit 3abe2fa6, fixes #335). Username and password inputs now carry name and id attributes.

  • security-scan-skipped audit row now fires when the gate is disabled globally (commit ae24e0a9).

  • Command action security warning updated to canonical DD_ACTION_COMMAND_* prefix (commit aa5fc98d).

  • Docker event history pruning amortized to reduce per-event splice cost (commit d6690cc8). The threshold is now 2×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 via cloneContainer before 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() and triggerBatch() 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 buildPopoverStyle helper 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 dialogConfirmDialog moved 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 an identityKey discriminator 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:href SVG attributes stripped by icon sanitizer (commit 0309bacb). href is now in the allowlist; the deprecated xlink:href form remains blocked.

  • Floating semver aliases excluded from greater-than check (commit 0b9eaaf3). isGreaterCandidateTag now requires strictly greater semver in one direction and not-greater in the reverse, so floating aliases like 3.3 and 3.3.0 drop out of the candidate set entirely.

Security

  • TCP Docker host is validated before the self-update controller passes it to Dockerode (commit 441b4358). validateTcpDockerHost rejects 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 to execFile.

  • Credential redaction expanded to x-registry-auth, *-token, and api-key fields (commit 4417ce25). A second regex pass now redacts x-registry-auth, any field matching *-token, and api-key / api_key values before the payload leaves the server.

  • Credential status pattern matching uses RE2 (commit df9b914a). BaseRegistry.getRejectedCredentialStatus now uses RE2JS.compile(…) to maintain the project-wide ReDoS immunity guarantee.

  • Registry instances using insecure=true now log a warning on every request (commit cd14e3a9).

  • DD_SESSION_SECRET is now required; auto-generated and persisted on first boot when unset. (See Changed entry 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 protobufjs 7.5.4 → 7.5.5 to close the prototype-chain arbitrary-code-execution advisory.

  • GHSA-r4q5-vmmm-2653 (medium) — Bumped follow-redirects to 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.remoteAddress in preference to request.ip, eliminating the X-Forwarded-For spoof-ability.

  • CORS origin required when enabled — Enabling CORS now requires DD_SERVER_CORS_ORIGIN to be set explicitly.

  • OIDC redirect target allowlist — Post-login redirects are now validated against the backend's endpoint allowlist.

  • Healthcheck HTTPS probe hardening/bin/healthcheck no longer uses popen() 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. Set DD_SSE_DEBUG_LOG_IP=true to temporarily log raw IPs.

  • HTTP trigger proxy URL scheme validation — HTTP trigger proxy URLs must now be http:// or https:// 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-parser upgraded 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 .snyk file for reviewed false-positive Snyk Code findings.

  • Supply-chain toolchain refresh — Bumped pinned Alpine edge/testing package versions for cosign and trivy in the Dockerfile.

  • Binary indices and drain concurrency cap for notification outbox (commit 9393253e). findReadyForDelivery fields switched to binary indices for O(log n) B-tree lookups. OutboxWorker gains a maxDrainConcurrency option (default 10) backed by a DrainSemaphore.

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).

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 canonical DD_NOTIFICATION_* + dd.notification.* (messaging providers) and DD_ACTION_* + dd.action.* (update executors). Touched 29 files in content/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.

Don't miss a new drydock release

NewReleases is sending notifications on new releases.