Changelog

Changelog

All notable changes to this project will be documented in this file.

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

Fork point: upstream post-8.1.1 (2025-11-27) Upstream baseline: WUD 8.1.1 + 65 merged PRs on main (Vue 3 migration, Alpine base image, Rocket.Chat trigger, threshold system, semver improvements, request→axios migration, and more)

Unreleased

[1.5.1-rc.2] — 2026-06-28

Changed

  • The entire UI is now translatable. The last hardcoded English strings (dashboard widgets, security view, detail panels, host-status labels, the log viewer's invalid-regex notice, and SSE update-failed fallbacks) were extracted into the vue-i18n catalogs, so every surface now resolves through the translation system. Combined with the newly opened community translation project on Crowdin, contributors can translate any part of the interface.

Fixed

  • The security view now shows release notes for the running image even when no update is pending. The detail panel, table, and card surfaces previously gated the release-notes link behind "an update is available," so a container with no pending update showed nothing even though its current release notes were known. The running-tag notes now appear whenever they exist, and a "View project" link (the source repository) was added alongside them, matching the containers view. (Discussion #295)

  • The dashboard "Recent Updates" widget now uses the shared release-notes and project-link components. It previously rendered a bare release-notes anchor with no project link and no structured current/available notes. It now renders the same icon links as every other surface, fed from the container's sourceRepo, releaseNotes, and currentReleaseNotes. (Discussion #295)

  • Auto-apply update triggers now honor the maintenance window on every detection path. A container update detected through certain code paths could be auto-applied outside the configured maintenance window because the window check was missing on those paths. The gate is now enforced uniformly, so updates only auto-apply inside the window regardless of how the update was detected. (#321)

Security

  • Suppressed a ZAP DAST false positive (rule 10049, Storable and Cacheable Content). The baseline scan flagged cacheable static responses that are not sensitive; the rule is now downgraded in .zap/rules.tsv and the JSON-to-SARIF converter handles the suppression so the security workflow stays green without masking real findings. (#374)

[1.5.1-rc.1] — 2026-06-26

Added

  • Remote agents now report their log level and watcher schedule. The agent handshake (dd:ack) includes logLevel and pollInterval (the watcher's cron), so the GET /api/v1/agents response and the Agents view populate these fields for connected agents instead of leaving them blank.

Changed

  • Coverage reporting moved from Codecov to Qlty Cloud. Part of the org-wide consolidation onto Qlty (one vendor for code quality and coverage). CI now publishes the normalized app/ui lcov reports to Qlty Cloud via GitHub OIDC — no stored coverage token — replacing the Codecov upload and codecov.yml. The vitest 100% coverage thresholds in the app/ and ui/ test suites remain the enforced gate; the README coverage badge now points at Qlty.

  • Maturity gate counts from the registry publish date when trustworthy. For Docker Hub and GHCR (including lscr.io), the gate now measures elapsed time from the real image push date (last_updated / updated_at) instead of from when drydock first detected the update. An image that has been public longer than maturityMinAgeDays clears the gate immediately on the first scan that finds it. All other registries expose only the OCI image build date, which is not a reliable push signal, so drydock falls back to its own first-detection timestamp (updateDetectedAt) for those. A trusted publish date is skipped if it fails to parse or is in the future (clock-skew protection).

Fixed

  • Maturity gate (maturityMode: 'mature') never triggered; the first-detection timestamp was never computed. The function that stamps updateDetectedAt returned undefined immediately whenever updateAvailable was false, which is always the case while maturity suppression is active, so the timestamp was never computed and the maturity clock never started. Containers blocked by the maturity gate remained permanently "maturing" and never became update-available.

  • Maturity clock reset on every container recreation. When a container was stopped and recreated (Portainer stack redeploy, docker compose down && up), its new Docker container ID caused drydock to treat it as a fresh container, resetting updateDetectedAt to zero. Containers that are frequently redeployed could never accumulate enough age to clear the gate. The clock is now keyed on a stable container identity and survives recreation as long as the same update remains pending, including the common case where a slow image pull means the replacement container isn't yet visible when the old one is pruned.

  • In-memory container cache key collision. The cache key joined watcher name and container name with _, so my_prod + nginx and my + prod_nginx produced the same key. A wrong cache hit could apply a stale security scan result or maturity timestamp from one container to a different container, most visibly after a recreation event. The separator is now ::.

  • A changed update candidate now restarts the maturity soak. When a new image digest or tag is published while an earlier update is still inside the maturity window, the gate restarts the clock for the new candidate instead of letting it inherit the previous candidate's elapsed time. A freshly pushed image always soaks for the full maturityMinAgeDays rather than being treated as already mature. (Local watches previously kept the original detection time here; remote-agent containers already behaved this way.)

  • Docker and Docker Compose actions can pull private GCR and Google Artifact Registry images again. getAuthPull() for the GCR and GAR providers returned the raw service-account email as the username and the private key as the password, which docker login rejects, so any action trigger targeting a private gcr.io or *-docker.pkg.dev image failed to authenticate and could not apply the update. It now returns the _json_key username with the service-account JSON as the password, the format Google's registry auth expects (and the same one the token-exchange path already used).

  • Quay tag pagination no longer breaks on standard Link headers. The next_page cursor was matched with a greedy pattern that swallowed the trailing >; rel="next" from an RFC 5988 Link header and corrupted the cursor, so repositories with more than one page of tags could silently stop paginating. The parser now reads next_page and last cursors correctly from both bare-URL and RFC 5988 header forms, and still URL-encodes them to block scope injection. The inherited TrueForge provider gets the same fix.

Security

  • Custom registry TLS settings now apply to every registry request. DD_REGISTRY_*_CAFILE, _INSECURE, and _CLIENTCERT were honored only on the credential handshake for the GAR, GitLab, Mau, DHI, ACR, and ECR providers; the follow-up tag-list, manifest, and blob calls fell back to the system CA bundle. The custom TLS agent is now propagated to those calls, including the anonymous (no-credential) code paths in GAR, GCR, and Quay, so a private CA or an insecure setting is enforced end to end rather than only while fetching the token.

  • Hook command environment values are sanitized against shell injection. Lifecycle hook commands run through /bin/sh -c, and registry-controlled values (image name, tag, update digest) flowed into the hook environment unsanitized, so a crafted tag could inject shell commands into a hook script that expanded those variables unquoted. The values are now scrubbed of shell metacharacters, matching the sanitization the command action already applies.

  • DD_SESSION_SECRET__FILE is now honored. The session secret was read straight from process.env, bypassing the __FILE secret-file resolution every other secret uses, so the documented file form was silently ignored and the instance fell back to a generated secret on every restart. It now reads the resolved value, so a secret supplied via DD_SESSION_SECRET__FILE is used (and, as with every other secret, the file form wins when both the file and the bare variable are set).

  • Secret files are checked for unsafe permissions and trailing newlines. When a DD_*__FILE secret is readable by group or others, drydock logs a non-fatal warning recommending chmod 600 (skipped on Windows, where the mode bits are not meaningful). File-sourced secret values are also trimmed of a trailing newline so an editor- or echo-added \n can't corrupt a credential, matching the common Docker *_FILE convention.

  • Debug dump and container environment no longer leak credentials. The debug dump's redaction missed SMTP passwords (*_PASS keys) and webhook URLs with embedded secrets, both of which appeared in the dumped environment for any authenticated user. They are now redacted, and the same *_PASS gap is closed for the container runtime environment shown via /api/containers. Registry usernames and service hostnames stay visible by design, since they aren't secrets and aid debugging.

Upgrade Notes

  • One-time notification burst on first scan after upgrade (Docker Hub / GHCR containers only). Containers on Docker Hub or GHCR whose pending update is already older than maturityMinAgeDays will clear the maturity gate immediately on the first poll after upgrading. Notification triggers in always mode will fire once for each such container. Action triggers (docker, docker-compose, command) will also fire, so containers previously held by the gate may be updated automatically on that first poll. Review your active action-trigger configuration before upgrading if you want to control the timing. This is expected behavior: those images were already mature; the gate was simply unaware of it.

1.5.0 — 2026-06-22

Added

  • Trigger taxonomy split — DD_ACTION_* and DD_NOTIFICATION_* prefixes. Action triggers (Docker, Docker Compose, Command) now use DD_ACTION_* / dd.action.*; messaging triggers (Slack, SMTP, Discord, Telegram, ntfy, Pushover, and all others) use DD_NOTIFICATION_* / dd.notification.*. All three families remain interchangeable at runtime through v1.7.0. A migration CLI (drydock config migrate --source trigger) rewrites existing configs automatically. The legacy DD_TRIGGER_* / dd.trigger.* aliases are deprecated (removal targeted v1.7.0).

  • Experimental Portwing edge-agent mode. Agents behind NAT or firewalls can dial out to the controller over a persistent wss:// WebSocket instead of requiring an inbound connection. Enable with DD_EXPERIMENTAL_PORTWING=true. Uses Ed25519 public-key challenge-response auth; operator key management is exposed at /api/v1/portwing/keys.

  • Real-time container log viewer. WebSocket-based live log streaming from Docker containers in the UI — ANSI color rendering, automatic JSON pretty-printing, free-text/regex search, stdout/stderr and log-level filtering, copy to clipboard, and gzip download. Available in the container detail panel and at /containers/:id/logs.

  • Notification outbox with retry and dead-letter queue. Failed notification deliveries are persisted and retried with exponential backoff + jitter. After 5 attempts (configurable), entries move to a dead-letter queue. New /api/notifications/outbox REST surface and a dedicated UI page let operators list, retry, or discard entries.

  • Durable update queue with restart recovery. Container updates are queued server-side with per-trigger concurrency limits. Queued and mid-pull operations survive controller restarts and are recovered automatically. New cancel endpoint (POST /api/operations/:id/cancel) accepts both queued and in-flight operations. A global concurrent-update cap (DD_UPDATE_MAX_CONCURRENT) is available.

  • Customizable dashboard. Drag-to-reorder, resize, and per-widget visibility toggles. New Resource Usage widget shows fleet-wide CPU and memory with top-N consumers, fed by a live fleet-stats SSE stream (GET /api/v1/stats/summary/stream).

  • Diagnostic debug dump. One-click export of redacted system state from Configuration > Diagnostics — runtime metadata, component state, Docker diagnostics, recent events, and DD_* env vars, with sensitive values auto-redacted. Available at GET /api/v1/debug/dump.

  • SSE Last-Event-ID replay. Every broadcast event carries a monotonic <bootId>:<counter> id; clients reconnecting with Last-Event-ID receive missed events from a 5-minute ring buffer. Clients that fall behind the buffer receive a dd:resync-required event.

  • Update-eligibility blockers on container rows. Sixteen structured blocker reasons are surfaced inline on the Containers list, so users see why a container isn't updating without opening the detail drawer. Hard blockers lock the Update button; soft blockers show a warn-and-confirm modal.

  • Per-agent Home Assistant MQTT topic segmentation (DD_NOTIFICATION_MQTT_<name>_HASS_AGENTTOPICSEGMENT). Prevents two agents sharing the default watcher name from overwriting each other's topics. Opt-in for v1.5.x; targets default-on in v1.7.0.

  • Full i18n coverage across the UI. All hardcoded strings migrated to per-namespace JSON catalogs with 17 locale options (de, es, fr, it, nl, pl, pt-BR, tr, zh-CN, zh-TW, ar, ja, ko, ru, uk, vi, plus English). Human translations synced from Crowdin.

  • Colored startup banner. Renders the whale logo as a truecolor half-block art print on interactive terminals; auto-suppressed when not a TTY or when NO_COLOR is set.

Changed

  • Action trigger default mode changed to AUTO=oninclude. Action triggers no longer auto-update every container by default; an explicit dd.action.include label is required. Notification triggers are unaffected.

  • Default watcher cron relaxed from hourly to every 6 hours. Reduces registry pressure for the common case; override with DD_WATCHER_{name}_CRON.

  • Consolidated security scanning on Grype; dropped Snyk. Grype now scans both the built container image and all six npm lockfiles. Snyk's GitHub SCM integration, .snyk policy file, security-snyk-weekly.yml, and related scripts are removed. Free gates (CodeQL, dependency-review, OpenSSF Scorecard, zizmor) continue to run on every PR.

  • Healthcheck binary replaces curl. Default HEALTHCHECK now uses a 65 KB static C binary (/bin/healthcheck). curl is retained for user-defined overrides through v1.6.0 (removal in v1.7.0).

  • DD_SESSION_SECRET auto-generated and persisted when unset. On first boot without the variable set, drydock generates 64 random bytes and persists them in the store so sessions survive restarts. The env var still takes precedence.

  • Tag-last release pipeline. The git tag is pushed only after the Docker image is built, pushed, signed (cosign), and attested (SLSA). If the tag exists, the image exists.

Fixed

  • Containers pinned to a fully-specified semver tag no longer climb to newer versions by default. Tags classified as specific precision now track digest changes only. Opt in to semver climbing with dd.tag.include or dd.tag.family=loose.

  • Multi-agent deployments no longer produce spurious 409 conflicts, cross-agent container contamination, or persistent "0 running containers" in the controller UI. Root causes: the controller's local watcher was pruning remote-agent rows; agent watcher snapshots were lost when the SSE stream was half-open mid-reconnect. Fixed by scoping store reads and key derivation to { agent, watcher } at every call site, and having agents replay the latest snapshot to each new SSE client immediately after dd:ack.

  • Self-update overlay holds until the swap is complete. Three compounding bugs (premature overlay dismissal, unreachable finalize callback, helper container appearing in the watcher list) are fixed. A new unauthenticated /api/v1/self-update/{operationId}/status endpoint lets the UI poll during the restart window.

  • Spurious "update failed" and "update available" notifications eliminated. False notifications arising from concurrent duplicate requests (409 race), post-update stale watcher scans, timed-out operation TTLs, and digest/batch path suppress-lifecycle gaps are all closed.

  • Live log viewer and WebSocket upgrades now work behind TLS-terminating reverse proxies. WebSocket upgrades and the log-stream origin check now honor X-Forwarded-Host/X-Forwarded-Proto when DD_SERVER_TRUSTPROXY is set.

  • Docker Compose update no longer destroys the running container on failure. A pre-flight architecture-compatibility check runs before removing the old container; if the recreate still fails, the original container is restored from its captured spec.

  • Registry 429/503 handling with Retry-After and per-host token bucket. Every registry HTTP call retries up to 3 times with exponential backoff honoring upstream Retry-After; a per-host token bucket prevents self-inflicted rate limiting during large cron cycles.

  • Standard Bearer token exchange now works for Chainguard, Codeberg/Forgejo, and other v2 registries that issue a WWW-Authenticate challenge. callRegistry implements the spec-compliant challenge-response flow with realm-host validation.

Security

  • Command trigger no longer inherits the full process environment. Child processes receive a fixed allowlist of system variables plus drydock-provided container vars. Additional variables can be whitelisted with DD_ACTION_COMMAND_{name}_ENV.

  • HTTP trigger blocks cloud metadata endpoints. Requests resolving to 169.254.0.0/16, fe80::/10, and related link-local ranges (including IPv4-mapped and IPv4-compatible spellings) are rejected before sending. Opt-out available via DD_NOTIFICATION_HTTP_{name}_ALLOWMETADATA=true.

  • CSRF same-origin enforcement hardened. Forwarded-host headers are now trusted only when Express trust proxy is enabled; /auth mutations (logout, remember) are now covered by the same-origin check; and DD_SERVER_TRUSTPROXY=true (all hops) now emits a startup warning recommending a hop count.

  • Security digest templates no longer evaluated as JavaScript. SECURITYDIGESTTITLE/SECURITYDIGESTBODY previously passed through new Function() — arbitrary code execution. The renderer now uses the same sandboxed ${…} interpolation engine as all other trigger templates.

  • Bearer token-endpoint requests now set maxRedirects: 0 to prevent credential exfiltration if a token endpoint returns a redirect. Applied to BaseRegistry and all providers that build their own credentialed token-fetch requests.

  • Container image CVE surface cleared. Bumped node:24-alpine base (24.14.0 → 24.16.0), cosign (2.6.3 → 3.0.6), musl, curl, and git, clearing all HIGH/CRITICAL findings in drydock-controlled packages. Residual HIGHs inside vendored Go binaries (cosign, trivy) are scoped out of the fail-on-HIGH gate via .grype.yaml.

  • LokiJS query-filter values constrained to primitives. ?key[$regex]=…-style operator injection (ReDoS via native RegExp, $ne/$gt operator injection) is neutralized at the store choke point for every caller.

  • Multiple CVE patches: undici bumped to 8.5.0 (8 advisories including CVE-2026-9675), nodemailer bumped to 9.0.1 (GHSA-p6gq-j5cr-w38f, CVSS 7.1), protobufjs to 7.6.3 (GHSA-xq3m-2v4x-88gg, critical), vite to 8.0.16, axios to 1.16.1 (CVE-2025-62718), and additional transitive patches across all six lockfiles.

1.5.0-rc.38 — 2026-06-19

Added

  • Colored startup banner. When drydock starts on an interactive terminal it now renders the whale logo as a compact truecolor half-block banner followed by a drydock v<version> · <mode> identity line. The art is baked from the master logo (drydock.png) at build time by scripts/gen-banner.mjs, so startup decodes no image. The banner is written to stderr and suppressed automatically when stdout/stderr is not a TTY or NO_COLOR is set, so logs and piped output stay clean.

Changed

  • Consolidated dependency/CVE scanning on Grype; dropped Snyk. Snyk's GitHub SCM integration scans the full dependency requirement graph across every package.json/package-lock.json in the repo rather than the resolved, shipped dependency set, so it over-reports advisories in transitive packages the lockfile never actually resolves to — noise on top of a redundant paid integration. Grype replaces it on both axes: it scans the built container image (the image's package catalog is the dependency set actually shipped) and the six npm lockfiles (root, app, ui, e2e, apps/demo, apps/web), matching the lockfile-resolved versions instead of the manifest graph, so it does not emit the requirement-graph false positives. The free gates already in CI cover the rest — CodeQL (SAST), dependency-review (new-dependency CVEs on PRs), OpenSSF Scorecard, and zizmor — so nothing else was needed (Trivy intentionally not added; drydock is TypeScript/Node, so the Go call-graph scanner govulncheck used on sibling repos does not apply here). The new security-grype.yml runs the dependency scan on pull requests (path-filtered to dependency/Dockerfile/workflow changes) plus a weekly cron and manual dispatch, builds and scans the container image on scheduled/manual runs, and uploads distinct-category SARIF to the GitHub Security tab. Removed the .snyk policy file, the security-snyk-weekly.yml workflow, the setup-snyk composite action, and the scripts/snyk-* gate/quota scripts.
  • Refreshed the drydock whale logo across the app, website, demo, and docs. A new master render replaces the brand mark everywhere — the in-app logo and favicons, the website/demo favicons, PWA icons, and OpenGraph cards, and the README/docs logos (including the dark-mode variant). All brand assets are now regenerated from a single master (drydock.png) via scripts/regenerate-brand-assets.sh. Filenames are unchanged, so the Home Assistant entity_picture URL contract is preserved.

Security

  • Documentation site (apps/web) js-yaml pinned to 4.2.0 (GHSA-h67p-54hq-rp68). fumadocs-mdx pulled js-yaml 4.1.1 transitively; an override forces the patched 4.2.0. Build-time dependency of the website only — not part of the shipped drydock image.

  • E2E load-test harness @opentelemetry/core pinned to 2.8.0 (CVE-2026-54285). artillery pulled @opentelemetry/core 2.7.1 transitively, vulnerable to unbounded memory allocation in W3C Baggage propagation; an override forces the patched 2.8.0. Test-only dependency — not part of the shipped drydock image.

  • Patched the container image's HIGH/CRITICAL CVE surface and scoped the Grype image gate. The first grype-image scan on main flagged a pre-existing CVE backlog that nothing had been scanning (Snyk Container never ran — no token was configured). Bumped the node:24-alpine base (node 24.14.0 → 24.16.0 clearing CVE-2026-21710, musl 1.2.5 → 1.2.6, curl 8.19.0 → 8.20.0, git 2.52.0 → 2.54.0) and cosign 2.6.3 → 3.0.6, which clears every HIGH/CRITICAL in the Node runtime and Alpine OS packages. The only residual HIGH/CRITICAL findings live inside the vendored Go module graphs compiled into the bundled cosign and trivy CLI binaries (drydock shells out to them for signature verification and container scanning) — those clear only when Alpine rebuilds the packages, so a documented .grype.yaml scopes the fail-on-HIGH image gate to the dependencies drydock controls (Node, OS packages, the app npm graph) and excludes the two tool-binary locations. cosign 3.0.6 keeps the verify --output json/--certificate-identity/--certificate-oidc-issuer/--key flags drydock's signature path uses.

  • Patched a batch of newly-disclosed undici CVEs across the runtime and tooling workspaces. osv-scanner flagged eight undici advisories disclosed in 2026 — CVE-2026-6733, CVE-2026-6734, CVE-2026-9675, CVE-2026-9678, CVE-2026-9679, CVE-2026-9697, CVE-2026-11525, and CVE-2026-12151. The shipped backend (app) carries undici as a direct dependency and was on 8.3.0, vulnerable to all eight — bumped to 8.5.0, the only release clearing the full set (CVE-2026-9675 is fixed solely in 8.5.0), and pinned in overrides as well. The dashboard build (ui) and the e2e load-test harness pulled undici 7.25.0/7.26.0 transitively; an overrides entry forces 7.28.0 (the patched 7.x line) in each — build- and test-only, not part of the shipped image.

  • Patched nodemailer to 9.0.1 (GHSA-p6gq-j5cr-w38f, CVSS 7.1). A message-level raw option bypassed nodemailer's disableFileAccess/disableUrlAccess guards, allowing arbitrary file read and full-response SSRF in the delivered message. drydock's SMTP trigger only calls createTransport/sendMail with plain from/to/subject/text fields and never passes raw, so the sink isn't reachable here — but the advisory affects every release through 9.0.0 with the fix landing only in 9.0.1, so the direct dependency in app is bumped from 8.0.10. The 8→9 major jump doesn't touch the stable createTransport/sendMail core drydock relies on.

1.5.0-rc.37 — 2026-06-15

Security

  • Patched a batch of newly-disclosed transitive CVEs across every workspace. osv-scanner flagged advisories disclosed 2026-06-15 in build- and test-time dependencies: vite (CVE-2026-53571, CVE-2026-53632), @babel/core (CVE-2026-49356), form-data (CVE-2026-12143), protobufjs (CVE-2026-54269), and ws (CVE-2026-48779). Each is pinned to a fixed version via an override (or a direct bump where the dependency is direct). js-yaml@3.14.2, reachable only through artillery's test-only load-test harness, is triaged as unreachable: its sole fix removes the safeLoad() API artillery still calls, and it parses only trusted in-repo configs.

Changed

  • Registry rate-limiter burst raised from 5 to 10 for ghcr.io and Docker Hub. The conservative burst allowance was tripping the limiter during legitimate request spikes (enumerating tags across many containers at once); the sustained rate (2 req/s) is unchanged.

  • Hardened the E2E/CI suite against transient flakes. Crash-prone real-application e2e fixtures (Home Assistant, Radarr) now run a keep-alive entrypoint so the watcher consistently discovers the full container set instead of intermittently seeing one short; the test-bootstrap readiness count is now exact and strict; and the Playwright container-detail helpers wait on real conditions rather than fixed timeouts. No shipped runtime behavior changes from this item.

1.5.0-rc.36 — 2026-06-15

Added

  • Experimental Portwing edge-agent mode — agents behind NAT or firewalls can now dial OUT to drydock over a persistent wss:// WebSocket instead of waiting for an inbound controller connection (PR #429, M5). The feature is experimental and opt-in: set DD_EXPERIMENTAL_PORTWING=true to enable it. When disabled, the endpoint is not mounted and the feature has zero runtime footprint. Once enabled, agents connect to WS /api/v1/portwing/ws using the portwing/1.0 subprotocol. Authentication is Ed25519 public-key challenge-response with timestamp + nonce replay protection (±60 s clock-skew window, 16 MB maximum frame size). Operator key management is exposed through a REST registry at /api/v1/portwing/keys (list, register, revoke). Because the feature is experimental the protocol and API surface may change in a future release without a deprecation notice.

  • Remote-agent runtime info now carries logLevel and pollInterval in the acknowledgement payload (PR #430, M4). Drydock threads these fields through buildRuntimeInfoFromAck and surfaces them in the Agents view alongside the existing runtime metadata.

Fixed

  • Containers pinned to a fully-specified semver tag (e.g. image:v1.13.3, 3+ numeric segments) no longer climb to newer versions by default (#321). Tags classified as specific precision are now treated as digest-only by default — getTagCandidates returns an empty tag list so updates track digest changes only, not semver version bumps. Opt in to semver climbing by setting dd.tag.include (restricts climbing to matching tags) or dd.tag.family=loose (unrestricted climbing as before). Floating tags (latest, 16-alpine, etc.) and 1–2-segment partial versions are unaffected.

  • Maintenance window now gates when auto-updates are applied, not just when update checks run (#321). Previously watchFromCron respected the window, but maybeFastResyncAfterUpdate (the post-update fast resync) called watchContainer unconditionally — allowing a triggered resync to detect a new image and dispatch an update outside the window. The fast resync now mirrors the same maintenance-window guard used by watchFromCron. Additionally, computeUpdateEligibility gains a maintenanceWindowOpen context field: when false, a soft maintenance-window-closed blocker is recorded in the eligibility result. Manual UI/API-triggered updates pass undefined and remain ungated.

  • Self-update overlay no longer flickers — the UI holds the "Applying Update" screen until the swap is actually complete. Three compounding defects made the self-update experience look broken: (1) the UI's connectivity probe treated any successful /auth/user response as "update finished", but the old server keeps answering during the image pull — so the overlay dismissed almost immediately, then the page died again when the old container actually stopped; (2) the in-progress self-update operation could never be recorded as completed: the finalize callback secret was regenerated per-process (the helper's POST always failed with 403 after the restart) and startup reconciliation expired the operation before the helper could finalize it; and (3) the transient drydock-self-update-<timestamp> helper container carried no watch-exclusion label, so it flashed into the container list with watch-by-default enabled. The finalize secret is now per-operation, with its SHA-256 hash persisted on the operation row so the restarted process can validate the helper's callback; startup reconciliation grants fresh in-progress self-update operations a 10-minute grace window (with expiry as the bounded fallback if the helper dies); a new unauthenticated GET /api/v1/self-update/{operationId}/status endpoint reports the operation state while the session is unavailable mid-restart; the UI polls that endpoint during a self-update and only reloads once the operation reaches a terminal state (succeeded, rolled-back, failed, or expired); and the helper container is labeled dd.watch=false so it never appears in the watcher's container list. The UI also closes its SSE connection when self-update mode begins (after the ack is delivered) — the browser's built-in EventSource retry would otherwise reconnect to the new server, clear the overlay before the swap was committed, and leave the SPA running stale pre-update assets. Dry-run mode no longer shows the overlay at all: the UI notification is skipped and the no-op operation is marked terminal immediately instead of lingering in-progress.

1.5.0-rc.35 — 2026-06-10

Fixed

  • False "update failed" notification when concurrent update requests race (#421). When a duplicate update request was rejected with HTTP 409 ("update already in progress") while the winning update was still in flight, the controller classified the conflict as a genuine failure — firing an "update failed" notification immediately followed by "updated successfully". The duplicate classifier now recognizes three benign signals instead of one: a recently-succeeded operation (as before), a 409 response whose body explicitly carries the active-update lock message ("Container update already queued/in progress" — authoritative even before the winner's state has propagated from a remote agent over SSE), and another active (queued or in-progress) operation for the same container and agent+watcher identity. The same reclassification now also covers the Docker-native rename path in ContainerUpdateExecutor, which previously marked the duplicate failed before the outer classifier could run.

  • Update operations could hang in-progress forever when deferred rollback reconciliation failed. The deferred reconciliation callback only logged a warning on error, leaving the operation permanently active and blocking all future updates for that container until restart. The operation is now terminalized as failed (self-update and already-terminal operations excluded, as elsewhere).

  • Spurious "update available" notifications around just-updated containers (#408 hardening). Three escape hatches in the post-update suppression mechanism are closed: suppression now keys on both the container ID and the watcher-scoped name, so the recreated container's new Docker ID can no longer dodge the check; the batch retry buffer consults suppression before re-queuing; and on startup the suppression set is re-seeded from update operations that succeeded within the last hour, so a controller restart between an update and the watcher's confirming scan no longer re-fires a stale notification. Suppression entries now also expire after one hour and are cleared on trigger deregistration, so containers deleted outright (or agents that never reconnect) can no longer leak entries for the life of the process.

  • Live log viewer returned 403 behind TLS-terminating reverse proxies even with DD_SERVER_TRUSTPROXY set. WebSocket upgrades bypass Express, so the log-stream origin check never honored trust-proxy. It now compares the browser origin against X-Forwarded-Host/X-Forwarded-Proto (first hop) when trust proxy is enabled — and remains byte-for-byte strict when it is not.

  • Containers of permanently removed agents lingered in the store forever. Startup now prunes container rows whose agent no longer matches any registered agent component. Rows of registered-but-currently-disconnected agents are untouched.

  • "Updated successfully" toast fired while the new container was still starting (#290 follow-up). The toast settled on the old container's removal event during a recreate; it now waits for the replacement container's arrival (replacementExpected removals are skipped, and the new container's ID rides the dd:update-applied payload as newContainerId), closing the status gap between the last update activity and the success notification. The toast dedup TTL also gained a 20% margin over the server's SSE replay buffer to prevent a boundary duplicate on reconnect.

  • Watcher enrichment failures that threw non-Error values leaked malformed entries into the container snapshot. Thrown non-Error values are now wrapped, counted as enrichment errors, and excluded.

  • GCR registry reported anonymous configurations as authenticated. Gcr.getAuthPull() now returns undefined without credentials, matching the other providers. GHCR 404 detection now checks the axios response status instead of matching error-message strings.

Security

  • Command trigger no longer inherits the full process environment. User-authored command scripts previously received every DD_* secret (registry tokens, notification tokens, agent secrets) via process.env. The child environment is now built from a fixed allowlist (PATH, HOME, SHELL, USER, LANG, LC_ALL, TZ, TMPDIR, TMP, TEMP) plus the drydock-provided container variables. Scripts that legitimately need more can name additional variables with the new DD_ACTION_COMMAND_{name}_ENV option (comma-separated).

  • Hook commands can be restricted to an allowlist of binaries (DD_HOOKS_ALLOWED_COMMANDS). With hooks enabled, dd.hook.pre/dd.hook.post labels could invoke any binary on the image. The new comma-separated allowlist matches the hook command's first token (basename, or exact path for entries containing /); when unset, behavior is unchanged and a one-time warning recommends configuring it.

  • HTTP trigger blocks cloud metadata endpoints. Requests resolving to link-local ranges (169.254.0.0/16 including 169.254.169.254, fe80::/10, fd00:ec2::254) are rejected before sending — including IPv4-mapped and IPv4-compatible IPv6 spellings of those ranges (::ffff:169.254.169.254, ::ffff:a9fe:a9fe, ::169.254.169.254), which would otherwise slip past the literal-IP check. Private-network and localhost targets remain fully supported — they are the normal self-hosted case. The rare legitimate link-local target can opt out via DD_NOTIFICATION_HTTP_{name}_ALLOWMETADATA=true.

Performance

  • Tag transform patterns are no longer recompiled in the sort hot path. Compiled RE2 transform patterns are cached per formula and tag candidates are transformed once before sorting instead of twice per comparison — previously ~3,000 compilations per 300-tag container per watch cycle.

  • Container normalization no longer deep-clones the entire container per registry call. The watcher now copies only the image/registry fields it mutates instead of structuredClone of labels and environment for every container every cycle.

  • Update-operation retention pruning uses indexed status queries instead of materializing the whole collection every 100th mutation, and the default rejected-credential pattern in BaseRegistry is compiled once at module load. An unused updatedAt collection index no longer taxes every mutation.

Changed

  • Trigger providers must implement trigger()/triggerBatch(). The base implementations now throw instead of silently doing nothing, so a provider that forgets to override fails loudly. All 22 bundled providers already comply; this only affects out-of-tree forks.

1.5.0-rc.34 — 2026-06-07

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.

  • Per-agent Home Assistant MQTT topic segmentation (DD_NOTIFICATION_MQTT_<name>_HASS_AGENTTOPICSEGMENT, default false). When enabled, Drydock inserts an agent/<name> segment into every Home Assistant MQTT topic — per-container state topics, watcher-level count/update sensors, and watcher running-status sensors — for containers owned by a remote agent, so two agents that both use the default watcher name local no 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 and type="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 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 watched local and agent-owned containers 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.

  • Post-start liveness grace window (DD_UPDATE_POST_START_LIVENESS_GRACE_MS). After Docker start() 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. Default 2000 ms. Set to 0 to 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. Default 4. Values of 0 are 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 (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

  • Legacy aggregate container stats route now fails explicitly. GET /api/v1/containers/stats is still removed from the public stats API, but the router now catches the legacy path before /:id and returns 410 Gone with migration targets (GET /api/v1/stats/summary for aggregate stats, GET /api/v1/containers/:id/stats for per-container stats) instead of treating stats as a container id.

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

  • 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, including watcher running-status topics. Set DD_NOTIFICATION_MQTT_<name>_HASS_AGENTTOPICSEGMENT=true to 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.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

  • #418 — Operators behind a TLS-terminating reverse proxy now get an actionable startup log when manual updates fail with 403 CSRF validation failed. The rc.30 same-origin hardening made DD_SERVER_TRUSTPROXY mandatory behind a proxy that terminates TLS (without it, drydock sees req.protocol as http while the browser sends an https Origin, so every state-changing request is rejected) — but the only symptom was an opaque 403 with no pointer to the fix. When trust proxy is disabled and a request arrives carrying X-Forwarded-Proto: https, drydock now logs a one-time warning naming the exact cause and the DD_SERVER_TRUSTPROXY=1 fix (linking the reverse-proxy FAQ). The CSRF guard itself is unchanged — genuine cross-site forgery is still rejected.

  • #415 — Registries that require a Bearer token exchange for tag lookups (Chainguard cgr.dev, Codeberg/Forgejo codeberg.org, and other standard Docker v2 registries) no longer fail with 401 Unauthorized. callRegistry now implements the spec-compliant flow: on a 401 carrying a WWW-Authenticate: Bearer realm=…,service=…,scope=… challenge it fetches a token from the realm (anonymously for public images, or with the configured credentials) and retries the request once. The realm host is validated against the registry's own host via the existing validateAuthUrlHost guard, and the handler falls back to the original error if the exchange can't be completed, so it is purely additive for providers that already authenticate. Fixes tag enumeration for public Codeberg images and credentialed Chainguard images.

  • #413 — Containers deployed as a Docker Swarm stack (e.g. Portainer agents via docker stack deploy) now group in the container stack view. The grouping key derivation in app/api/group.ts recognized com.docker.compose.project but not com.docker.stack.namespace, the label Swarm stacks carry instead of the Compose project label — so Swarm-deployed services fell through to the ungrouped bucket while Compose services grouped correctly. com.docker.stack.namespace is now the lowest-priority auto-detected group key, after dd.group > wud.group > com.docker.compose.project.

  • #411 — Multi-agent deployments no longer produce a false 409 Conflict when identically-named containers are being updated on different agents. The duplicate-update check revived in rc.30 fell back to a global name-only lookup (getActiveOperationByContainerName) with no agent+watcher scope. Containers on different hosts are physically separate — the lookup is now scoped by { agent, watcher } via an optional ContainerIdentityFilter parameter added to both getActiveOperationByContainerName and getInProgressOperationByContainerName in app/store/update-operation.ts. Legacy operation rows that predate the container-snapshot field (#385) are accepted unconditionally as a backward-compatible fallback. The same scoping is applied to the ContainerUpdateExecutor reconciliation path, the list-handler eligibility context, and the SSE container-enrichment path so cross-agent contamination is closed at all five call sites.

  • 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. ContainerUpdateExecutor created fresh operation rows without a container, so terminal update-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 stuck in-progress for 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-supplied operationId (previously validated then silently discarded), forwards it to the agent as runtime context, and returns 202 { operationId } instead of 200 {}; UpdateRequestError is 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 stale updateAvailable=true report arriving before the watcher caught up could still re-buffer a just-updated container on the digest and batch paths. The digest path (shouldHandleDigestContainerReport plus the handleContainerReportDigest lift) and the batch path (shouldHandleBatchContainerReport plus a new handleContainerReports lift) now share the same suppress-then-lift lifecycle. The batch-path lift is mandatory rather than belt-and-braces: a pure mode=batch trigger 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 preserveClearedUpdateState as dead code; an in-flight cron scan could re-raise the cleared update badge. maybeFastResyncAfterUpdate now 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. resolveHelperImage hand-rolled the registry-URL normalization; when registry.url ended 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 security field.
    • The agent /health endpoint now reflects watcher-registration failure. It was a hardcoded 200, so an agent whose watchers all failed to register (unreachable Docker socket, bad TLS — swallowed with log.warn) reported healthy forever. It now returns 503 when 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. parseBatchUpdateCompletedPayload was the only event parser scoping its operationId with toAgentScopedId instead of resolveAgentOperationId; 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-reported HostConfig.Runtime ("runc") verbatim into the POST /containers/create body, 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's DefaultRuntime via GET /info during the clone-prepare step and omits HostConfig.Runtime from the create body when it merely restates that default. Explicitly-selected non-default runtimes (nvidia, kata, sysbox-runc) are preserved, and when /info is unavailable the runtime is left untouched (prior behaviour). The HostConfig is shallow-cloned before editing so the inspect spec kept for rollback is not mutated.
    • Stuck update operations now terminalise to a non-notifying expired status instead of failed (#410). The active-TTL sweep and the startup-orphan reconciliation marked timed-out queued/in-progress rows as failed, which fired markOperationTerminal's update-failed lifecycle 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 which emitTerminalLifecycleEvent emits nothing at all, so neither case can cry wolf. The UI treats expired as 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 as expired rather 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 as failed and 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 in getActiveUpdateOperationForContainer is repaired so a stale-container-id duplicate is blocked up front (409 instead of silently slipping through); and both ContainerUpdateExecutor's rename() catch and the Docker lifecycle outer catch consult a shared duplicate-op-classification helper that downgrades failedexpired only when a succeeded terminal row already exists for the same container name inside a 15-minute window. A real failure with no recent success still terminalises as failed and 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), restartCollection re-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.
  • #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 to inspect() 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.getContainers now scopes containersFromTheStore by normalizeAgentValue(this.agent), so a watcher only ever prunes its own agent's containers. (2) A lost dd:watcher-snapshot was 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 after dd: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 local containers and a remote agent's local containers 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 by watcher_name and 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 in rollback-deferred until 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 every local watcher into a single inflated bucket — they are now keyed by the unique registry id (docker.local vs ml.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 by watcher_name only, 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 returns 409 Conflict when 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_AGENTTOPICSEGMENT flag, because closing it requires a change to the MQTT topic structure — see Added and Deprecated.

  • The Docker image builds again after Alpine edge/testing dropped the pinned trivy=0.70.0-r1 package. apk add failed with breaks: 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 existing curl exception in the same layer: edge/testing rotates and drops -rN builds quickly and per-arch mirrors desync during the window, so an exact -rN pin is inherently fragile for multi-arch builds.

  • #408 — A successful update no longer fires a spurious "new version available" notification. handleContainerUpdateAppliedEvent clears the once notification history after an update is applied, which re-opens the once gate before the watcher's next scan sets updateAvailable=false. A lingering container report still carrying updateAvailable=true (common during "Update All") then fired a duplicate "update available" message right after "updated successfully". A per-container suppression set now withholds update-available notifications for a just-applied container until a watcher report confirms updateAvailable=false, after which the key is lifted so future real updates still notify. Additionally, update-applied and update-failed lifecycle notifications now bypass the semver threshold filter, so a post-update updateKind.kind of unknown (e.g. digest-only updates) under a non-all threshold no longer silently drops the success/failure notice.

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

  • Mau registry auth scope is now percent-encoded. Mau.authenticate interpolated image.name into the JWT auth scope query 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 called fetchUserInfo with skipSubjectCheck, omitting the OIDC Core 5.3.2 check that the UserInfo sub matches the token sub. 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 (commit a132318e). getExpectedOrigin read X-Forwarded-Host/-Proto unconditionally, so a client could forge them to satisfy the mutation same-origin check even with trust proxy disabled. The forwarded host is now honored only when Express trust proxy is enabled, the protocol derives from the already-gated req.protocol, and the Host port is preserved for non-standard-port deployments. Upgrade note: this aligns enforcement with the long-documented requirement to set DD_SERVER_TRUSTPROXY behind a TLS-terminating reverse proxy (reverse-proxy setup). A deployment that terminated TLS at a proxy but never set DD_SERVER_TRUSTPROXY previously worked only because the unconditional header trust masked the misconfiguration; it must now set DD_SERVER_TRUSTPROXY (hop count, e.g. 1) or state-changing requests will return 403.

  • GitHub release-notes token withheld for untrusted source repos (commit 7186195c). A dd.source.repo container 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 or from a persisted container.sourceRepo fallback without provenance; image-label, OCI-label, GHCR-path, and live Docker Hub metadata lookups remain trusted. A follow-up (commit 0a014304) 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-noble is now pinned by its sha256 digest 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's qs parser 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/$gt operators. 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). renderSecurityDigestTemplate rendered the operator-supplied SECURITYDIGESTTITLE / SECURITYDIGESTBODY via new 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 as scan.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). safeInterpolate split 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 called axios() directly, bypassing withTlsRequestOptions() — so the credential exchange ignored the configured cafile / insecure / client-cert and validated against the system trust store. The shared TLS helper is now applied to those token fetches (Ecr was realigned to extend BaseRegistry).

  • Bearer token-endpoint requests now set maxRedirects: 0 to prevent credential exfiltration via HTTP redirect. A host-validated token endpoint returning a 3xx could otherwise redirect the operator's Basic credentials to an attacker-controlled host; setting maxRedirects: 0 causes axios/follow-redirects to fail closed on any redirect response, and the existing failClosedAuth catch path turns that into a clean auth failure without forwarding credentials. Applied to BaseRegistry.authenticateBearerFromAuthUrl and to the four providers (GAR, GitLab, Mau, DHI) that build their own credentialed token-fetch requests outside the shared helper. The malformed-realm fallback debug log is also sanitized to avoid logging attacker-controlled realm values verbatim (committed separately as 786c2317).

  • CSRF same-origin enforcement extended to authenticated /auth mutations (commit d1611f88). The /auth router sat outside the middleware chain that runs requireSameOriginForMutations, leaving bodyless POST /auth/logout and POST /auth/remember without same-origin protection (exploitable under DD_SERVER_COOKIE_SAMESITE=none).

  • OIDC username falls back to the sub claim before unknown (commit 8e00dfd3). Identities without an email claim previously collapsed to a single unknown username sharing one session-eviction bucket.

  • schemaVersion-1 manifest parsing guarded (commit fee0bbcc). A malformed/missing v1Compatibility from 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 (commit 238727f5). The redaction set matched token but not tokens, so DD_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 (/health exempt; the long-lived SSE stream counts once on open).

  • Basic-auth string comparison no longer leaks length via timing (commit 6ff55463). timingSafeEqualString early-exited on length mismatch; both operands are now hashed to a fixed-length sha256 digest before timingSafeEqual, matching verifyShaPassword.

  • Quay pagination tokens percent-encoded (commit a19e0102). next_page/last values parsed from the registry Link header 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). When DD_SESSION_SECRET is unset, a one-time warning now recommends setting it explicitly and keeping the store directory non-world-readable.

  • Startup warning when trust proxy is set to boolean true (commit 71eac008). DD_SERVER_TRUSTPROXY=true trusts all X-Forwarded-For hops, letting clients spoof req.ip and 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). 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).

  • Pinned pinia and vue-i18n to exact versions (commit fd0b02a5). Both were the only ^-ranged UI dependencies; pinned to the locked 3.0.4 / 11.4.2 to 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 the 1.x → 2.x boundary. The only 2.0.0 breaking change — native-ECMAScript replaceAll/replaceFirst replacement semantics — is not exercised by drydock; the compiled compile/matcher/find/group surface 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 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.

1.5.0-rc.29 — 2026-05-30

Fixed

  • Docker Compose update failure safety (#391, #407). Prevented a failed Compose update from destroying the running container.

  • ZAP SARIF path handling (#404, #405). Relativized HTTP artifact URIs through SARIF originalUriBaseIds and preserved the leading slash so root-path findings validate in GitHub Code Scanning.

  • DAST authentication alignment (#403). Updated the baseline DAST credentials to match the QA compose password hash.

  • Authentication readiness gate (#402). Closed a strategy-registration timing gap so /health reports readiness only after authentication strategies are registered.

1.4.5 — 2026-03-17

Added

  • Dashboard Update buttons — Per-row update buttons and "Update all" button in the Updates Available dashboard widget. (#173)
  • Getting Started guide — New step-by-step onboarding guide covering watchers, tag filters, registries, notifications, auto-updates, safety features, and multi-host setup. (#153)

Fixed

  • Container recreate alias filtering — Hardened Docker watcher timestamp parsing, added event handler early return for transient aliases, canonical MQTT topic naming, and stale topic cleanup for recreated containers. (#156)
  • About modal version display — Version is now fetched dynamically from the API instead of being hardcoded, ensuring the modal always reflects the running server version. (#167)
  • Version resolution fallbackDD_VERSION=unknown is now skipped so the version is correctly read from package.json at startup.
  • Theme circle transition origin — The theme toggle circle animation now originates from the click point instead of the viewport center.
  • Trigger code bugs — Gotify URL and Apprise URL now correctly enforce .required() validation; Kafka clientId casing normalized with clientId kept as a deprecated compatibility alias until v1.6.0.

Security

  • OIDC debug log redaction — Sensitive OIDC parameters (client_id, code_challenge, state, etc.) are now redacted in debug logs. (#168)
  • Agent API error sanitization — Error logs and responses in the agent API are sanitized to prevent leaking internal details.
  • Registry config value redaction — Trigger group configuration now logs keys only, not values, to prevent secret leakage.

Changed

  • API versioning — All UI fetch calls migrated from /api/ to /api/v1/ paths.
  • OIDC empty bearer token log level — Downgraded empty bearer token log from warn to debug. (#169)

Documentation

  • Docs audit (78 files) — Fixed 18 doc accuracy issues, 3 code bugs, 22+ broken links, and restructured 8 pages (90 callouts → 36). Triggers overview reduced from 17 callouts to 3 with threshold reference table. Template variables expanded from 12 to 30 with example values. Docker Compose trigger now linked from triggers overview. (#153, #172)

Dependencies

  • Securityfast-xml-parser 5.3.8 → 5.5.6 (CVE: numeric entity expansion bypass), next 16.1.6 → 16.1.7 (HTTP smuggling, CSRF bypass, DoS)
  • CIstep-security/harden-runner v2.15.1 → v2.16.0, github/codeql-action v4.32.6 → v4.33.0
  • App@aws-sdk/client-ecr, @slack/web-api, express-rate-limit, fast-check, knip, nodemailer, undici, @types/node
  • UI@iconify-json/lucide, knip, jsdom 28 → 29
  • Websitefumadocs-core, fumadocs-mdx, fumadocs-ui, lucide-react, lefthook, @vercel/analytics v1 → v2, @vercel/speed-insights v1 → v2
  • Demo@iconify-json/lucide, @vitejs/plugin-vue, msw 2.10 → 2.12
  • E2E@dotenvx/dotenvx, fast-xml-parser, minimatch

Chore

  • QA infrastructure — Added Portainer, slow-shutdown container, and watchevents service for end-to-end container recreate testing. (#156)
  • Coverage and types cleanup — Coverage read retry, OpenAPI re-export, and auth types cleanup.
  • Demo version bump — Bumped demo package version to 1.4.5.
  • Alias filtering test coverage — Added timestamp edge-case tests for Docker watcher alias filtering. (#156)
  • msw worker regenerated — Updated mockServiceWorker.js for msw 2.12 (origin-based → clientId-based validation).

1.4.4 — 2026-03-16

Added

  • Click-to-copy on version tags — CopyableTag component with clipboard feedback on all version displays (dashboard, container list, detail panels). (#164)
  • Dark mode icon inversion — Simple Icons (si: prefix) auto-invert in dark mode via Tailwind dark variant.
  • Tailwind v4 class-based dark mode@custom-variant dark directive for proper .dark class detection.

Changed

  • Dashboard Updates Available version column centered.
  • Sidebar search button border removed⌘K badge improved dark mode contrast.
  • URL rebrand — All URLs updated from drydock.codeswhat.com to getdrydock.com.

Fixed

  • Dashboard host-status widget showing 0 for non-agent remote watchers — Dashboard host-status widget incorrectly showed zero container counts for non-agent remote watchers by using watcher.id instead of watcher.name as the lookup key. (#155)
  • Container recreate alias duplicates — Unconditional 30s transient window skip, single inspect per event, event path guard prevent Docker's transient <id-prefix>_<name> aliases from producing duplicate container entries in triggers. (#156)
  • Tooltip viewport overflow — Replaced CSS pseudo-element tooltips with body-appended popup using position:fixed and auto-flip. (#165)
  • Theme switcher broken — Restored document binding for startViewTransition API.

1.4.3 — 2026-03-16

Fixed

  • DNS resolution failures on Alpine (EAI_AGAIN) — Node.js 24 defaults to verbatim DNS ordering, which on Alpine's musl libc can cause getaddrinfo EAI_AGAIN errors when IPv6 records are returned first on dual-stack networks. Drydock now defaults to IPv4-first DNS ordering at startup, configurable via DD_DNS_MODE (ipv4first | ipv6first | verbatim, default: ipv4first). (#161)
  • Multi-platform Docker build validation — CI now validates multi-platform Docker builds to catch architecture-specific failures early.

Security

  • Zizmor findings blocking in CI and lefthook — GitHub Actions security findings from zizmor are now blocking in both CI and local pre-push hooks.
  • Secrets scoped to dedicated GitHub environments — CI secrets are now scoped to dedicated GitHub environments instead of repository-level access.

Documentation

  • Docker socket security guide — Expanded watcher docs with comprehensive security section: comparison table of all access methods, socket proxy setup (recommended), remote Docker over TLS with cert generation walkthrough, rootless Docker guide, full Docker API endpoint reference showing exactly which endpoints Drydock uses for read-only monitoring vs write operations (updates).

Dependencies

  • CI actions — Bumped upload-artifact to v7 and replaced nick-fields/retry.

1.4.2 — 2026-03-15

Added

  • Update-operations paginationGET /api/containers/:id/update-operations now supports limit and offset query parameters with _links navigation, matching the existing container list pagination pattern.
  • Periodic audit log pruning — Audit store now runs a background timer (hourly, unref'd) to prune stale entries even with low insert volume, in addition to the existing insert-count-based pruning.
  • Container release notes enrichment — Watch cycles now enrich update candidates with GitHub release metadata (result.releaseNotes and result.releaseLink), and full notes are available via GET /api/containers/:id/release-notes.
  • Container list sort and filter query paramsGET /api/containers now supports sort (name, status, age, created, with optional - prefix for descending), plus status, kind, watcher, and maturity filters.
  • Update age and maturity API signals — Container payloads now track updateAge and support maturity states (hot, mature, established) for filtering and policy workflows.
  • Suggested semver tag hints — Containers tracked on non-semver tags such as latest now expose result.suggestedTag to surface the best semver target.
  • oninclude trigger auto mode — Trigger auto now supports oninclude, which only auto-runs for containers explicitly matched by include labels. (#160)
  • Container runtime observability APIs — Added /api/containers/stats, /api/containers/:id/stats, and /api/containers/:id/stats/stream for resource telemetry, plus richer per-container runtime snapshots.
  • Real-time container log streaming — Added WebSocket streaming at /api/v1/containers/:id/logs/stream with stdout/stderr, tail, since, and follow controls.
  • Container logs and runtime stats panels — Container detail views now include dedicated live Logs and Runtime Stats tabs (including full-page logs route support).
  • Signed registry webhook receiver — Added POST /api/webhooks/registry with HMAC signature verification and provider-specific payload parsing for registry push events.
  • Auth lockout Prometheus observability — Added Prometheus metrics for login success/failure and lockout activity.

Changed

  • Extract maturity-policy module — Consolidated scattered day-to-millisecond conversions and maturity policy constants into app/model/maturity-policy.ts, shared by backend, UI, and demo app.
  • Extract security-overview module — Moved security vulnerability aggregation and pagination logic from crud.ts into dedicated app/api/container/security-overview.ts for improved readability and testability.
  • Refactor Docker Compose trigger — Extracted YAML parsing/editing into ComposeFileParser and post-start hook execution into PostStartExecutor, reducing the monolithic Dockercompose.ts by ~400 lines.
  • Decompose useContainerActions — Split the 1200-line composable into focused modules: useContainerBackups, useContainerPolicy, useContainerPreview, and useContainerTriggers.
  • Registry error handling — Replaced catch (e: any) with catch (e: unknown) and getErrorMessage(e) in component registration and trigger/watcher startup.
  • E2E test resilience — Container row count assertions now use toBeGreaterThan(0) instead of hardcoded counts, preventing false failures when the QA environment has a different number of containers.
  • Registry digest cache dedup per poll cycle — Digest cache lookups now deduplicate repeated requests within a single poll cycle, reducing redundant registry calls and improving metric accuracy.
  • Extract runtime config evaluation context type — Consolidated scattered runtime field evaluation parameters into a typed ClonedRuntimeFieldEvaluationContext interface for trigger providers.
  • Argon2 hash parsing type safety — Extracted Argon2Parameters interface, parameter key type guard, and PHC parameter parsing into a reusable function for improved type safety.
  • Extract agent client initialization methods — Extracted URL parsing, HTTPS detection, protocol validation, and TLS configuration from monolithic constructor into focused private methods.
  • Extract shared self-hosted registry config schema — Deduplicated the registry configuration schema (url, login, password, auth, cafile, insecure, clientcert, clientkey) into a reusable helper shared by Custom and SelfHostedBasic registry providers.

Documentation

  • Podman docs expansion — Added Podman setup/compatibility guidance plus SELinux, podman-compose, and production notes in watcher and FAQ docs.
  • Docs site URL rebrand — Replaced drydock.codeswhat.com links with getdrydock.com across docs pages, sitemap/robots metadata, and website copy.

Fixed

  • CSRF validation behind reverse proxies — Same-origin mutation checks now honor X-Forwarded-Proto and X-Forwarded-Host when present before falling back to direct request protocol/host, preventing false 403 CSRF validation failed responses in TLS-terminating proxy setups. (#146)
  • Hosts page missing env-var-configured watchers — The Hosts page hardcoded a single "Local" entry and only added agent-based hosts. Watchers configured via DD_WATCHER_* environment variables (e.g. remote Docker hosts) were never displayed, even though their containers appeared correctly on the Containers page. The page now fetches all watchers from the API and displays each local watcher with its actual name and connection address. (#151)
  • Docker events reconnect with malformed errors — Reconnect failure logging no longer crashes when the error object has no message property.
  • Security overview container count — The security vulnerability overview now uses the lightweight store count instead of loading all container objects just to count them.
  • Compose path deduplication — Replaced indexOf-based dedup with Set for compose file path detection in container security view.
  • Build provenance attestation — CI attestation step now runs with if: always() so provenance is attested even when prior optional steps are skipped.
  • Container recreate alias duplicates in triggers — When containers were recreated externally (via Portainer or docker compose up), Docker's transient <id-prefix>_<name> aliases were treated as new containers, producing duplicate entries in MQTT Home Assistant discovery sensors and Telegram notifications. Added alias deduplication filtering and force-removal of stale container IDs on recreate detection. (#156)
  • Stale store data after external container recreation — Watch-at-start scan was suppressed when the store already had container data, leaving stale records from the previous run after external container recreation. Removed the suppression so every startup runs a full scan with alias filtering. (#157)
  • Watcher container counts on Hosts page — Per-watcher container counts on the Hosts page used watcher.id (e.g. docker.esk83) as the lookup key instead of watcher.name (e.g. esk83), causing counts to display as zero for env-var-configured watchers. (#155)
  • Docker images tagged :main with no version — CI release workflow triggered on both main branch pushes and version tags, producing Docker images tagged :main that showed DD_VERSION=main instead of a real version number. Release workflow now only triggers on version tags (v*). (#154)
  • Maturity badge sizing and tooltip clipping — Fixed maturity badge height mismatch with other text badges and removed overflow-hidden from DataListAccordion that clipped tooltips in list view.

Security

  • Agent secret over plaintext HTTP warning — Agent clients now log a warning when a shared secret is configured over unencrypted HTTP, advising HTTPS configuration.
  • Auth audit log injection — Login identity values are now sanitized with sanitizeLogParam() before inclusion in audit log details, preventing log injection via crafted usernames.
  • SSE self-update ack hardening — Added validation for empty clientId/clientToken, non-ack broadcast mode, and client-not-bound-to-operation rejection.
  • FAQ: removed insecure seccomp advice — Removed the "Core dumped on Raspberry PI" FAQ entry that recommended --security-opt seccomp=unconfined, which completely disables the kernel's syscall sandbox. The underlying libseccomp2 bug was fixed in all supported OS versions since 2021.

Dependencies

  • biome — Bumped to 2.4.7 with import ordering fixes for new lint rules.
  • vitest — Bumped to 4.1.0 in app workspace with fast-check/vitest 0.3.0 and knip 5.86.0.
  • UI packages — Bumped vue, vitest, storybook, and icon packages.
  • CI actions — Bumped zizmor-action to v0.5.2 and cosign-installer to v4.1.0.

1.4.1 — 2026-03-14

Added

  • Headless mode (DD_SERVER_UI_ENABLED) — Run drydock as an API-only service by setting DD_SERVER_UI_ENABLED=false. The REST API, SSE, and healthcheck endpoints remain fully functional while the UI is not served. Useful for controller nodes that only manage agents.
  • Maturity-based update policy — Per-container update maturity policy configurable from the container row's Update policy menu (or PATCH /api/v1/containers/:id/update-policy). Modes: all (default — allow any update) and mature (block updates detected less than maturityMinAgeDays ago, default 7). UI shows NEW/MATURE badges with flame/clock icons on containers with available updates. (#120)
  • ?groupByStack=true URL parameter — Bookmarkable URL parameter to enable stack grouping on the containers page. Also accepts ?groupByStack=1. (#145)

Changed

  • Connection-lost overlay animation — The connection-lost and reconnecting overlays now use a bounce animation for improved visual feedback.
  • Watch target resolution refactored to discriminated union — Internal refactor of the watch target resolution logic for improved type safety and maintainability.

Fixed

  • Agent handshake and SSE validation failure — Fixed agent API returning redacted container data (with sensitive field on env entries) instead of raw data, causing controller-side Joi validation to reject the handshake and crash on real-time SSE container events. SSE payloads now prefer canonical raw store data with sanitization fallback. (#141)
  • Mangled argon2 hash detection — Docker Compose $ interpolation can strip $ delimiters from argon2 PHC hashes, producing an invalid hash that silently failed registration. Drydock now detects mangled hashes at startup and surfaces an actionable error message. (#147)
  • Anonymous auth fallback — When all configured auth providers fail to register (e.g. due to a mangled hash), Drydock now falls back to anonymous mode if DD_ANONYMOUS_AUTH_CONFIRM=true is set, instead of leaving the user with no way to log in. (#147)
  • Auth registration errors on login page — Registration warnings (e.g. invalid hash format) are now surfaced on the login page so users can see exactly what went wrong instead of a generic "No authentication methods configured" message. (#147)
  • CSP inline style violations — Replaced runtime element.style mutations (DataTable column resize, tooltip directive, theme transitions, preference restore) with CSS custom properties and class-based styling. Relaxed style-src to include 'unsafe-inline' for vendor libraries (iconify-icon, Vue Transition) that set element.style programmatically.
  • Compose trigger affinity across remapped roots — Compose trigger file-path matching now uses suffix-based comparison as a fallback when the container's compose label path (host mount) differs from the trigger's configured path (container-internal), preventing missed trigger associations in bind-mount setups.
  • Compose trigger association — Enforce compose-file affinity when associating triggers with containers, preventing incorrect trigger-container matching. (#139)
  • Update All button icon centering — Fixed icon-text alignment in the Update All group header button to match the split button pattern used elsewhere.
  • Selected card border clipping — Fixed card border clipping on the first grid column in card view.

Security

  • Username enumeration timing side-channel — Eliminated timing difference between valid and invalid usernames during authentication.
  • LokiJS metadata exposure — Stripped internal LokiJS fields ($loki, meta) from settings API responses, agent API container responses, and /app endpoint.
  • Permissions-Policy header — Added Permissions-Policy header to restrict browser feature access (camera, microphone, geolocation, etc.).
  • CSP and Cross-Origin-Embedder-Policy — Tightened Content Security Policy and added COEP header.
  • Production image hardening — Removed wget, nc, and npm from the production Docker image; upgraded zlib.

Dependencies

  • undici — Bumped to 7.24.1 (fixes 12 CVEs including WebSocket memory consumption, CRLF injection, and request smuggling).

1.4.0 — 2026-02-28

Breaking Changes

  • MQTT HASS_ATTRIBUTES default changed from full to short — This changes Home Assistant entity payloads by default, excluding large SBOM documents, scan vulnerabilities, details, and labels. To retain the previous payload behavior, set DD_TRIGGER_MQTT_{name}_HASS_ATTRIBUTES=full explicitly.

Added

  • Audit log for container state changes — External container lifecycle events (start, stop, restart via Portainer or CLI) now generate container-update audit entries with the new status, so the audit log reflects all state changes, not just Drydock-initiated actions. (#120)
  • mTLS client certificate support — Registry providers now accept CLIENTCERT and CLIENTKEY options for mutual TLS authentication with private registries that require client certificates.

Backend / Core

  • Container recent-status APIGET /api/containers/recent-status returns pre-computed update status (updated/pending/failed) per container, replacing the client-side audit log scan and reducing dashboard fetch payload size.
  • Dual-slot security scanning — "Scan Now" automatically scans both the current running image and the available update image when an update exists. Results are stored in separate slots (scan/updateScan) and the Security page shows a delta comparison badge (+N fixed, -N new) next to each image that has both scans.
  • DD_LOG_BUFFER_ENABLED toggle — Disable the in-memory log ring buffer via DD_LOG_BUFFER_ENABLED=false to reduce per-log processing overhead. When disabled, /api/log/entries returns an empty array. Defaults to true.
  • Scheduled security scanning — Set DD_SECURITY_SCAN_CRON to automatically scan all watched containers on a cron schedule. DD_SECURITY_SCAN_JITTER (default 60s) spreads load with random delay before each cycle.
  • Security scheduler shutdown on exit — Security scan scheduler is now explicitly shut down during graceful exit, preventing orphan timers from delaying process termination.
  • On-demand sensitive env value reveal — Container environment variables are redacted by default in API responses. Individual values can be revealed on-demand via /api/containers/:id/env/reveal with audit logging.
  • On-demand scans populate digest cache — Manual container scans now populate the digest-based dedup cache, preventing redundant rescans of the same image digest.
  • Per-container webhook opt-out — New dd.webhook.enabled=false container label to exclude individual containers from webhook triggers without disabling the webhook API globally.
  • Scan cancellation and mobile scan progress — Security batch scans can now be cancelled mid-flight. Mobile scan progress UI improved with responsive layout.
  • Security scan coverage counts — Security view header shows scanned/total container counts for at-a-glance scan coverage.
  • Notification rule management API and persistence/api/notifications CRUD endpoints backed by LokiJS-persisted notification rules for update-available, update-applied, update-failed, security-alert, and agent-disconnect event types.
  • Rule-aware runtime dispatch — Trigger event dispatch resolves notification rules at runtime so per-event enable/disable and trigger assignments actively control which triggers fire.
  • Security-alert and agent-disconnect events — New event types with audit logging and configurable deduplication windows. Security alerts fire automatically on critical/high vulnerability scan results.
  • Compose-native container updates — Compose-managed containers now update via docker compose up -d lifecycle instead of Docker API recreate, preserving compose ownership and YAML formatting.
  • Rename-first rollback with health gates — Non-self container updates use a rename-first strategy (rename old → create new → health-gate → remove old) with crash-recoverable state persisted in a new update-operation store collection. Rollback telemetry via dd_trigger_rollback_total{type,name,outcome,reason} counter.
  • Tag-family aware semver selection — Docker watcher infers the current tag family (prefix/suffix/segment style) and keeps semver updates within that family by default, preventing cross-family downgrades like 5.1.420.04.1. Added dd.tag.family label (strict default, loose opt-out) and imgset support. (#104)
  • Entrypoint/cmd drift detection — Docker trigger detects whether entrypoint/cmd were inherited from the source image vs user-set, replacing inherited values with target image defaults during update. Adds dd.runtime.entrypoint.origin and dd.runtime.cmd.origin labels.
  • Self-update controller with SSE ack flow — Dedicated controller container for self-update replaces the shell helper pattern. UI acknowledgment via SSE with operation ID tracking.
  • Server-issued SSE client identity — Replaced client-generated UUIDs with server-issued clientId/clientToken pairs for self-update ack validation, preventing spoofed acknowledgments.
  • config migrate CLInode dist/index.js config migrate converts legacy WUD_* and Watchtower env vars/labels to DD_*/dd.* format across .env and compose files. Supports --dry-run preview and --source / --file selection.
  • Legacy compatibility usage metric — Prometheus counter dd_legacy_input_total{source,key} tracks local runtime consumption of legacy inputs (WUD_* env vars, wud.* labels) without external telemetry. Startup warns when legacy env vars are detected; watcher/trigger paths emit one-time deprecation warnings on wud.* label fallback.
  • Bundled selfhst icons for offline startup — Common container icons (Docker, Grafana, Nextcloud, etc.) bundled in the image so the UI works without internet on first boot.
  • Runtime tool status endpoint/api/server/security/runtime reports Trivy/Cosign availability for the Security view.
  • Gzip response compression — Configurable via DD_SERVER_COMPRESSION_ENABLED and DD_SERVER_COMPRESSION_THRESHOLD (default 1024 bytes), with automatic SSE exclusion.
  • Container runtime details — Ports, volumes, and environment exposed in the container model and API for the detail panel.
  • Update detected timestampupdateDetectedAt field tracks when an update was first seen, preserved across refresh cycles.
  • No-update reason trackingresult.noUpdateReason field surfaces why tag-family or semver filtering suppressed an available update.
  • Remove individual skip entriesremove-skip policy action allows removing a single skipped tag or digest without clearing all skips.
  • Update-operation history APIGET /api/containers/:id/update-operations returns persisted update/rollback history for a container.
  • Settings backend/api/settings endpoints with LokiJS collection for persistent UI preferences (internetless mode). Icon proxy cache with atomic file writes and manual cache clear.
  • SSE real-time updates — Server-Sent Events push container state changes to the UI without polling.
  • Remember-me authentication — Persistent login sessions via remember-me checkbox on the login form.
  • Docker Compose trigger — Refresh compose services via Docker Compose CLI when updates are detected.
  • Advisory-only security scanningDD_SECURITY_BLOCK_SEVERITY=NONE runs vulnerability scans without blocking updates. Scan results remain visible in the Security view and audit log.
  • OpenAPI 3.1.0 specification and endpoint — Machine-readable API documentation available at GET /api/openapi.json, covering all v1.4 endpoints with request/response schemas.
  • Watcher agent support initialization — Watchers now initialize agent support on startup for distributed monitoring readiness.
  • Security vulnerability overview endpoint — New GET /api/containers/security/vulnerabilities returns pre-aggregated vulnerability data grouped by image with severity summaries, so the Security view no longer needs to load all containers.
  • MQTT attribute filtering for Home Assistant — MQTT trigger supports attribute-based filtering for Home Assistant integration, allowing selective publishing based on container attributes.
  • Docker Compose post_start env validation — Docker Compose trigger validates environment variables in post_start hooks before execution, preventing runtime errors from missing or invalid env var references.
  • MQTT HASS entity_picture from container icons — When Home Assistant HASS discovery is enabled, entity_picture is now automatically resolved from the container's dd.display.icon label. Icons with sh:, hl:, or si: prefixes map to jsDelivr CDN URLs for selfhst, homarr-labs, and simple-icons respectively. Direct HTTP/HTTPS URLs pass through unchanged. (#138)
  • dd.display.picture container label — New label to override the MQTT HASS entity_picture URL directly. Takes precedence over icon-derived pictures when set to an HTTP/HTTPS URL.

UI / Dashboard

  • Tailwind CSS 4 UI stack — Complete frontend migration from Vuetify 3 to Tailwind CSS 4 with custom shared components. All 13 views rebuilt with Composition API.
  • Shared data components — Reusable DataTable, DataCardGrid, DataListAccordion, DataFilterBar, DetailPanel, DataViewLayout, and EmptyState components used consistently across all views with table/cards/list view modes.
  • 6 color themes — One Dark (clean/balanced), GitHub (clean/familiar), Dracula (bold purple), Catppuccin (warm pastels), Gruvbox (retro earthy warmth), and Ayu (soft golden tones). Each with dark and light variants. Circle-reveal transition animation between themes.
  • 7 icon libraries — Phosphor Duotone (default), Phosphor, Lucide, Tabler, Heroicons, Iconoir, and Font Awesome. Switchable in Config > Appearance with icon size slider.
  • 6 font families — IBM Plex Mono (default/bundled), JetBrains Mono, Source Code Pro, Inconsolata, Commit Mono, and Comic Mono. Lazy-loaded from local /fonts/ directory with internetless fallback.
  • Command palette — Global Cmd/Ctrl+K search with scope filtering (/ pages, @ runtime, # config), keyboard navigation, grouped sections, and recent history.
  • Notification rules management view — View, toggle, and assign triggers to notification rules with direct save through /api/notifications.
  • Audit history view — Paginated audit log with filtering by container, event text, and action type. Includes security-alert and agent-disconnect event type icons.
  • Container grouping by stack — Collapsible sections grouping containers by compose stack with count and update badges.
  • Container actions tab — Detail panel tab with update preview, trigger list, backup/rollback management, and update policy controls (skip tags, skip digests, snooze).
  • Container delete action — Remove a container from tracking via table row or detail panel.
  • Container ghost state during updates — When a container is updated, stopped, or restarted, its position is held in the UI with a spinner overlay while polling for the recreated container, preventing the "disappearing container" UX issue. (#80)
  • Skip update action — Containers with pending updates can be individually skipped, hiding the update badge for the current session without requiring a backend endpoint.
  • Slide-in detail panels on all views — Row-click detail panels for Watchers, Auth, Triggers, Registries, Agents, and Security views.
  • Interactive column resizing — Drag-to-resize column handles on all DataTable instances.
  • Dashboard live data and drag-reorder — Stat cards (containers, updates, security, registries) computed from real container data with drag-reorderable layout and localStorage persistence. Security donut chart, host status, and update breakdown widgets.
  • Log viewer auto-fetch and scroll lock — Configurable auto-fetch intervals (2s/5s/10s/30s) with scroll lock detection and resume for both ConfigView logs and container logs.
  • Keyboard shortcuts — Enter/Escape for confirm dialogs, Escape to close detail panels.
  • SSE connectivity overlay — Connection-lost overlay with self-update awareness and auto-recovery.
  • Login page connectivity monitor — Polls server availability and shows connection status on the login screen.
  • Server name badge for remote watchers — Shows the watcher name instead of "Local" for multi-host setups.
  • Dynamic dashboard stat colors — Color-coded update and security stats based on severity ratio.
  • About Drydock modal — Version info and links accessible from sidebar.
  • View wiring — Watcher container counts, trigger Test buttons with success/failure feedback, host images count, and registry self-hosted port matching all wired to live API data.
  • Font size preference — Adjustable font size slider in Config > Appearance for UI-wide text scaling.
  • Announcement banner — Dismissible banner component for surfacing release notes and important notices in the dashboard.
  • Dashboard vulnerability sort by severity — Top-5 vulnerability list on the dashboard now sorted by total count descending with critical count as tiebreaker, so the most severe containers appear first.
  • Rollback confirmation dialog — Container rollback actions now require explicit confirmation through a danger-severity dialog before restoring from backup.
  • Update confirmation dialog — Container update actions now require explicit confirmation through a dialog before triggering an update.
  • SHA-1 hash deprecation banner — Dashboard shows a dismissible deprecation banner when legacy SHA-1 password hashes are detected, prompting migration to argon2id.
  • Config tab URL deep-linking — Config view tab selection syncs to the URL query parameter, enabling shareable direct links to specific config tabs.

Changed

  • Compose trigger uses Docker Engine API — Compose-managed container updates now use the Docker Engine API directly (pull, stop, recreate) instead of shelling out to docker compose / docker-compose CLI. Eliminates spawn docker ENOENT errors in environments without Docker CLI binaries installed.
  • Compose self-update delegates to parent orchestrator — Self-update for compose-managed Drydock containers now uses the parent Docker trigger's helper-container transition with health gates and rollback, instead of direct stop/recreate.
  • Compose runtime refresh extracted — Shared refreshComposeServiceWithDockerApi() helper eliminates the recursive recreateContainerupdateContainerWithCompose call chain. Both code paths now converge on the same explicit, non-recursive method.
  • Compose-file-once batch mode re-enabledCOMPOSEFILEONCE=true now works with the Docker Engine API runtime. First container per service gets a full runtime refresh; subsequent containers sharing the same service skip the refresh.
  • Self-update controller testable entrypoint — Extracted process-level entry logic to a separate entrypoint module, making the controller independently testable without triggering process-exit side effects.
  • Dockercompose YAML patching simplified — Removed redundant type guards and dead-code branches from compose file patching helpers, reducing code paths and improving maintainability.
  • Dashboard fetches recent-status from backend — Dashboard now fetches pre-computed container statuses from /api/containers/recent-status instead of scanning the raw audit log client-side.
  • Prometheus collect() callback pattern — Switched container gauge from interval-based polling to the Prometheus collect() callback, letting Prometheus control collection timing and eliminating the background 5s timer.
  • Container security API refactored — Container security routes refactored into a dedicated module with type-safe SecurityGate integration, concurrent scan limiting (max 1), and trivy DB status-based cache invalidation.
  • DashboardView composable extraction — Extracted 700+ line monolith into useDashboardData, useDashboardComputed, useDashboardWidgetOrder, and shared dashboardTypes for better testability and separation of concerns.
  • Event-driven connectivity polling — AppLayout SSE connectivity monitoring now starts on disconnect and stops on reconnect instead of running a fixed interval, reducing unnecessary network requests.
  • Vulnerability loading optimized — Vulnerability data loaded from the container list API payload (includeVulnerabilities flag) instead of separate per-container fetches, reducing API calls on the Security view.
  • Default log format is JSON — Official Docker image now defaults to DD_LOG_FORMAT=json for structured production logs. Override with DD_LOG_FORMAT=text for pretty logs.
  • Scan endpoint rate limit reducedPOST /api/containers/:id/scan rate limit lowered from 100 to 30 requests/min to prevent resource exhaustion during aggressive scanning.
  • Single Docker image — Removed thin/heavy image variants; all images now bundle Trivy and Cosign.
  • Removed Vuetify dependency — All Vuetify imports, components, and archived test files removed. Zero Vuetify references remain.
  • Fail-closed auth enforcement — Registry bearer-token flows error on token endpoint failures instead of falling through to anonymous. HTTP trigger auth errors on unsupported types. Docker entrypoint requires explicit DD_RUN_AS_ROOT + DD_ALLOW_INSECURE_ROOT for root mode.
  • Fail-closed anonymous auth on fresh installs — New installs with no authentication configured and no DD_ANONYMOUS_AUTH_CONFIRM=true fail closed at startup (all API calls return 401). Users upgrading from a previous version are allowed anonymous access with a startup warning. Set DD_AUTH_BASIC_<name>_USER / DD_AUTH_BASIC_<name>_HASH to configure authentication, or set DD_ANONYMOUS_AUTH_CONFIRM=true to explicitly allow anonymous access.
  • Dashboard streamlined — Stat cards reduced from 7 to 4 (Containers, Updates, Security, Registries). Recent Activity widget removed to fit on single viewport. Background refresh prevents loading flicker on SSE events.
  • Notifications view is full rule management — Editable notification rules (enable/disable and trigger assignments) that save directly through /api/notifications.
  • Standardized API responses with collection pattern — All collection endpoints (/api/containers, /api/registries, /api/watchers, /api/authentications, /api/audit) now return { data: [...], total } instead of raw arrays. Supports pagination via offset/limit query parameters.
  • Paginated API discoverability links — Paginated collection responses now include _links.self and _links.next where applicable (for example /api/containers and /api/audit) to make page traversal explicit for API consumers.
  • Versioned API base path with transition alias/api/v1/* is now the canonical API path for integrations. /api/* remains as a backward-compatible alias during migration and is planned for removal in a future major release (target: v2.0.0).
  • Agent-scoped path order normalized — Agent-qualified component and trigger routes now use /:type/:name/:agent (for example /api/triggers/:type/:name/:agent and /api/containers/:id/triggers/:triggerType/:triggerName/:triggerAgent) instead of the old agent-first order. This is a breaking change for external API clients using the old path shape.
  • Machine-readable API error contract — All error responses return a consistent { "error": "..." } JSON structure with an optional details object for contextual metadata.
  • 6 color themes — Replaced original Drydock theme with popular editor palettes: One Dark, GitHub, Dracula, Catppuccin, Gruvbox, and Ayu. Each with dark and light variants.
  • Argon2id password hashing — Basic auth now uses argon2id (OWASP recommended) via Node.js built-in crypto.argon2Sync() instead of scrypt for password hashing. Default parameters: 64 MiB memory, 3 passes, parallelism 4.
  • PUT /api/settings deprecatedPUT /api/settings now returns RFC 9745 Deprecation and RFC 8594 Sunset headers. Use PATCH /api/settings for partial updates. PUT alias removal targeted for v1.5.0.
  • Basic auth argon2id PHC compatibility — Basic authentication now accepts PHC-format argon2id hashes ($argon2id$v=19$m=...,t=...,p=...$salt$hash) in addition to the existing Drydock argon2id$memory$passes$parallelism$salt$hash format. Hash-generation guidance now recommends the standard argon2 CLI command first, with Node.js as a secondary option.
  • Borderless UI redesign — Removed borders from all views, config tabs, detail panels, and shared data components for a cleaner visual appearance.
  • Dashboard version column alignment — Version column in the dashboard updates table is now left-aligned for better readability.
  • Detail panel expand button redesigned — Full-page expand button in the detail panel now uses a frame-corners icon instead of the previous maximize icon.
  • Sidebar active indicator removed — Removed the blue active indicator bar from sidebar navigation items for a cleaner look.

Fixed

  • Log level setting had no effectDD_LOG_LEVEL=debug was correctly parsed but debug messages were silently dropped because pino's multistream destinations defaulted to info level. Stream destinations now inherit the configured log level. (#134)

  • Server feature flags not loaded after login — Feature flags (containeractions, delete) were permanently stuck as disabled when authentication was required, because the pre-login bootstrap fetch failure marked the flags as "loaded" and never retried. Now failed fetches allow automatic retry after login. (#120)

  • Compose trigger silently skips containers — Multiple failure paths in the compose trigger were logged at debug level, making it nearly impossible to diagnose why a trigger reports success but containers don't update. Key diagnostic messages (compose file mismatch, label inspect failure, no containers matched) promoted to warn level, and the "already up to date" message now includes container names. (#84)

  • Fallback icon cached permanently — The Docker placeholder icon was served with immutable cache headers, causing browsers to cache it permanently even after the real provider icon becomes available. Fallback responses now use no-store.

  • Basic auth upgrade compatibility restored — Basic auth now accepts legacy v1.3.9 Basic auth hashes ({SHA}, $apr1$/$1$, crypt, and plain fallback) to preserve smooth upgrades. Legacy formats remain deprecated and continue showing a migration banner, with removal still planned for v1.6.0.

  • Compose trigger rejects lowercase env var keys — Configuration keys like COMPOSEFILEONCE, DIGESTPINNING, and RECONCILIATIONMODE were lowercased by the env parser but the Joi schema expected camelCase. Schema now maps lowercase keys to their camelCase equivalents. (#120)

  • Compose trigger strips docker.io prefix — When a compose file uses an explicit docker.io/ registry prefix, compose mutations now preserve it instead of stripping it to a bare library path. (#120)

  • Compose trigger fails when FILE points to directoryDD_TRIGGER_DOCKERCOMPOSE_{name}_FILE now accepts directories, automatically probing for compose.yaml, compose.yml, docker-compose.yaml, or docker-compose.yml inside the directory. (#84)

  • Container healthcheck fails with TLS backend — The Dockerfile healthcheck now detects DD_SERVER_TLS_ENABLED=true and switches to curl --insecure https:// for self-signed certificates. Also skips the healthcheck entirely when DD_SERVER_ENABLED=false. (#120)

  • Agent CAFILE ignored without CERTFILE — The agent subsystem now loads the CA certificate from CAFILE even when CERTFILE is not provided, fixing TLS verification for agents behind reverse proxies with custom CA chains.

  • Service worker accepts cross-origin postMessage — The demo service worker now validates postMessage origins against the current host, preventing potential cross-origin message injection.

  • Action buttons disable and show spinner during in-progress actions — Container action buttons (Stop, Start, Restart, Update, Delete) now show a disabled state with a spinner while the action runs in the background, providing clear visual feedback. The confirm dialog closes immediately on accept instead of blocking the UI.

  • Command palette clears stale filter on navigation — Navigating to a container via Ctrl+K search now clears the active filterKind, preventing stale filter state from hiding the navigated container.

  • Manual update button works with compose triggers — The update container endpoint now searches for both docker and dockercompose trigger types, matching the existing preview endpoint behavior. Previously, users with only a compose trigger saw "No docker trigger found for this container".

  • CI: qlty retry on timeout — Changed retry_on from error to any so qlty timeouts trigger retries. Increased timeout from 5 to 8 minutes.

  • OIDC docs clarified DD_PUBLIC_URL requirement — OIDC documentation now explicitly marks DD_PUBLIC_URL as required and includes it in all provider example configurations (Authelia, Auth0, Authentik, Dex). Without this variable, the OIDC provider fails to register at startup.

  • Compose trigger docs updated for Engine API — Removed stale references to docker compose config --quiet CLI validation and docker compose pull/up commands. Docs now reflect in-process YAML validation and Docker Engine API runtime. Added callout that Docker Compose CLI is not required.

  • Container actions docs updated for compose triggers — Update action documentation now mentions both Docker and Docker Compose trigger support.

  • Compose trigger rejects compose files mounted outside app directory — Removed overly strict working-directory boundary enforcement from runComposeCommand that rejected compose files bind-mounted outside /home/node/app, breaking documented mount patterns like /drydock/docker-compose.yml. Compose file paths are operator-configured and already validated during resolution.

  • Compose trigger uses host paths instead of container paths — Docker label auto-detection (com.docker.compose.project.config_files) now remaps host-side paths to container-internal paths using Drydock's own bind mount information. Previously, host paths like /mnt/volume1/docker/stacks/monitoring/compose.yaml were used directly inside the container where they don't exist, causing "does not exist" errors even when the file was properly mounted.

  • Compose trigger logs spurious warnings for unrelated containers — When multiple compose triggers are configured, each trigger now silently skips containers whose resolved compose files don't match the trigger's configured FILE path, eliminating noisy cross-stack "does not exist" warnings.

  • Silent error on recheck failure — "Recheck for Updates" button now displays an error banner when the backend request fails instead of silently stopping the spinner with no feedback.

  • Silent error on env reveal failure — Environment variable reveal in the container detail panel now shows an inline error message when the API call fails instead of silently failing.

  • Security scans persist across navigation — Navigating away from the Security view no longer cancels in-flight batch scans. Module-scoped scan state survives unmount and the progress banner reappears on return.

  • SSE stale sweep timer on re-initialization — Stale client sweep interval now starts even when init() is called after a hot reload, preventing leaked SSE connections.

  • About modal documentation icon — Documentation link in the about modal now shows a book icon instead of the expand/maximize icon.

  • Log auto-fetch pauses in background tabsuseAutoFetchLogs now stops polling when the browser tab is hidden and automatically resumes when it becomes visible again.

  • SBOM download DOM isolation — Isolated DOM element creation and URL.createObjectURL references in the SBOM download composable, fixing potential memory leaks and test failures from uncleared object URLs. JSON serialization skipped when SBOM panel is hidden.

  • Dashboard resource fetch error propagation — Dashboard API fetch errors are now propagated consistently to the UI error state instead of being silently swallowed.

  • Docker image "latest" tag restricted to stable releases — CI release workflow no longer tags prerelease versions as latest, preventing unstable images from being pulled by default.

  • Security table refreshes progressively during batch scan — Security view table updates incrementally as each container scan completes instead of waiting for the entire batch to finish.

  • Vulnerability data fetched from per-container endpoint — Security view now fetches vulnerability data from the correct per-container endpoint instead of a missing bulk endpoint.

  • OIDC callback session loss with cross-site IdPs — Session cookies now default to SameSite=Lax for auth compatibility, fixing callback flows that could fail under SameSite=Strict. Added DD_SERVER_COOKIE_SAMESITE (strict|lax|none) for explicit control. (#52)

  • Compose trigger handles unknown update kinds — Containers with updateKind.kind === 'unknown' now trigger docker compose pull instead of silently skipping. (#91)

  • Compose image patching uses structured YAML edits — Replaced regex/indent heuristics with YAML parser targeting only services.<name>.image, preserving comments and formatting.

  • Hub/DHI public registries preserved with legacy token envs — Public registry fallback no longer lost when a private token is configured. Fail-closed behavior remains for private registry auth and runtime token exchange failures.

  • GHCR retries anonymously on credential rejection — Public image checks continue when configured credentials are rejected by GHCR/LSCR.

  • Partial registry registration failures isolatedPromise.allSettled prevents a single bad registry from taking down all registries including the public fallback.

  • Auth-blocked remote watchers stay registered — Remote watchers that fail auth now show as degraded instead of crashing watcher init.

  • Docker event stream reconnects with exponential backoff — Watcher reconnects automatically (1s doubling to 30s max) instead of staying disconnected after Docker socket interruption.

  • SSE frames flushed immediately — Added X-Accel-Buffering: no and explicit flush() to prevent nginx/traefik from buffering real-time events.

  • Store flushed on graceful shutdown — Explicit save() call on SIGTERM/SIGINT prevents data loss between autosave intervals.

  • Digest value populated on registration and refresh — Digest-watch containers no longer show undefined digest in the UI.

  • Icon fallback for missing upstream — Icon proxy returns bundled Docker fallback instead of 404 when upstream providers return 403/404. Fixes registry port parsing in icon URLs.

  • Container groups route no longer shadowed/containers/groups mounted before /containers/:id to prevent Express treating group requests as container ID lookups.

  • Runtime env values redacted in API responses — Container environment variable values no longer exposed through the API.

  • Logger init failure produces structured stderr — Falls back to structured JSON on stderr instead of silent no-op when logger init fails.

  • Mobile sidebar closes on route change — Safety-net watcher ensures mobile menu closes on any navigation.

  • Security badge counts only scan vulnerabilities — No longer inflated by major version updates.

  • Trigger test failure shows parsed error message — Actionable error reason displayed below trigger card on test failure.

  • Viewport scrollbar eliminated — Fixed double-nested scroll contexts; long tags truncated with tooltips.

  • Self-hosted registries ignore port when matching — Registry matching now respects port numbers in self-hosted registry URLs, preventing mismatches between registries on different ports of the same host.

  • Socket proxy ECONNREFUSED with :ro mount — Removed :ro flag from Docker socket mount in all socket proxy compose examples. The read-only flag can prevent the proxy from connecting to the Docker daemon on some Linux kernels; the proxy's environment variable filtering (CONTAINERS=1, EVENTS=1, etc.) is the actual security boundary. Added health check and depends_on: condition: service_healthy to all socket proxy examples for proper startup ordering. Added FAQ entry for troubleshooting ECONNREFUSED with socket proxies.

  • Apprise trigger missing Content-Type header — Apprise notify requests now set explicit Content-Type: application/json header, fixing failures with Apprise servers that require it.

  • Toggle switch contrast — Improved toggle thumb contrast across all themes for better visibility.

  • Empty env var values rejected during container validation — Joi schema on details.env[].value used .string().required() which implicitly disallows empty strings. Containers with empty environment variable values (e.g. FOO=) were silently skipped during watch cycles. Fixed with .allow(''). (#120)

  • OIDC login broken by same-origin redirect checkLoginView.vue restricted OIDC redirects to same-origin URLs, but OIDC Identity Provider URLs are always cross-origin (Authentik, Keycloak, etc.). Removed the same-origin check, keeping only HTTP/HTTPS protocol validation.

  • Blank white screen on plain HTTP deployments — Helmet.js defaults enabled HSTS and upgrade-insecure-requests CSP even when TLS was not configured, causing browsers to block sub-resource loads on plain HTTP. Now conditionally omits upgrade-insecure-requests and HSTS when TLS is off. Strict boolean check on tls.enabled prevents string coercion. (#120)

  • Stale theme preferences after upgrade from v1.3 — Preferences migration deepMerge overwrote defaults with persisted values unconditionally, so invalid enum values (e.g. removed drydock theme family) survived migration and caused rendering failures. Added sanitize() pass that strips invalid theme families, variants, font families, icon libraries, scales, radius presets, view modes, and table actions before merge.

  • OIDC discovery fails on HTTP IdP URLs — openid-client v6 enforces HTTPS by default for all OIDC requests. Users running IdPs behind reverse proxies or on private networks with HTTP discovery URLs got "only requests to HTTPS are allowed" errors. Now passes allowInsecureRequests when the discovery URL protocol is http:.

  • Docker Compose trigger fails with EBUSY on bind-mounted stackswriteComposeFileAtomic() used a single fs.rename() call that failed permanently when another process (e.g. Dockge) held the file or when Docker bind-mount overlay contention blocked the rename. Now retries up to 5 times with 200ms backoff, then falls back to a direct writeFile if rename remains blocked. (#84)

  • Drydock container display name hardcoded — The Docker watcher hardcoded drydock as the display name for drydock's own container instead of using the actual container name like every other container.

  • Non-existent DD_OIDC_ALLOW_HTTP env var referenced in UI — The UI OIDC HTTP banner referenced a DD_OIDC_ALLOW_HTTP env var that does not exist — the backend auto-detects http:// discovery URLs and passes allowInsecureRequests automatically. Removed the misleading reference.

  • Load test and start scripts broken by standardized API responsesjq queries in run-load-test.sh and start-drydock.sh used raw array syntax instead of .data[] to match the new collection response pattern.

  • Container status not reconciled on cron poll — Containers that changed state between Docker event stream updates (e.g. stopped externally) were not reconciled during periodic cron polls. Now reconciles container status on each poll cycle.

  • DD_AUTH_ANONYMOUS_CONFIRM env var alias rejectedDD_AUTH_ANONYMOUS_CONFIRM was not accepted as an alias for the canonical DD_ANONYMOUS_AUTH_CONFIRM env var, forcing users to use only the non-prefixed form.

  • LSCR cross-host auth failure — LinuxServer Container Registry (lscr.io) token exchange failed because the auth flow required cross-host authentication via the ghcr.io token endpoint. Now allows cross-host auth for lscr.io.

  • iPad sidebar clipping — Used dvh (dynamic viewport height) instead of vh for sidebar height to fix clipping on iPad and other mobile browsers where the toolbar reduces available viewport height.

  • Mobile dashboard scroll clipping — Dashboard content was clipped on mobile viewports when scrolling, preventing users from reaching all content.

  • Lockout counter reset on failed login — Brute-force lockout counter could reset prematurely, and strategy IDs were not protected from enumeration. Fixed counter persistence and added strategy ID protection.

  • Stale vulnerability data on fetch error — Security view retained stale vulnerability data when a fetch error occurred instead of clearing it, showing outdated information.

  • Audit service missing limit parameter — Audit service requests did not always send the limit query parameter, causing inconsistent pagination behavior.

  • Backup retention on failed updates — Backup entries are now pruned on the failure path, not just after successful updates, preventing indefinite accumulation of stale backups.

  • Backup pruning with undefined maxCountpruneOldBackups() no longer deletes all backups when maxCount is undefined (e.g. when DD_BACKUPCOUNT is not configured). Now correctly no-ops on invalid or non-finite values.

  • Auto-rollback audit fromVersion accuracy — Rollback audit entries now correctly record fromVersion as the failing new image tag (via updateKind.remoteValue) instead of the pre-update old tag.

  • HASS entity_picture URL broken after logo rename — MQTT HASS discovery payload referenced a renamed logo file (drydock.png instead of whale-logo.png), causing missing entity pictures in Home Assistant. (#138)

  • Watcher crashes on containers with empty names — Docker watcher's same-name deduplication filter threw errors when containers had empty or missing names. Now skips deduplication for unnamed containers.

  • Container names not reconciled after external recreate — Containers recreated externally (via Portainer or docker compose up) retained stale names in the store until the next full poll cycle. Now reconciles container names immediately on detection.

  • Nested icon prefixes fail proxy request — Icon proxy rejected icons with doubled prefixes like mdi:mdi-docker. Now normalizes nested prefixes before proxying.

  • Colon-separated icon prefixes rejecteddd.display.icon labels using colon separators (e.g., sh:nextcloud) were rejected by the API validation pattern. Validation now accepts colon-prefixed icon identifiers.

  • Bouncer-blocked state missing from container details — Container detail views didn't reflect bouncer-blocked status. Now correctly wires the blocked state into detail panel display.

Security

  • Security API error sanitization — SBOM and scan error responses now return generic messages (Error generating SBOM, Security scan failed) instead of leaking internal error details. Detailed errors logged server-side only.
  • Agent log query parameter validation — Agent log level and component query parameters are validated against an allowlist and safe-character pattern, returning 400 for invalid values.
  • TLS key/cert paths stripped from config response — Server configuration API response no longer includes filesystem paths for TLS private key and certificate files.
  • Type-safe store and auth modules — Settings, notification, and auth store modules upgraded from any to explicit typed interfaces, preventing implicit type coercion vulnerabilities.
  • Fail-closed auth enforcement across registries and triggers — Bearer token, OIDC, and credential flows use failClosedAuth with typed RequestOptions, rejecting requests when required credentials are missing.
  • Input validation hardened — Additional input validation and error redaction across auth, API, and configuration modules.
  • Mutation-only JSON body parser — Express JSON body parsing restricted to mutation methods (POST/PUT/PATCH) only on both API and auth routers, reducing attack surface on read requests.
  • CSRF Sec-Fetch-Site validation — CSRF middleware now rejects requests with Sec-Fetch-Site: cross-site header, blocking cross-site state-changing requests even when the Origin header is absent.
  • HTTPS enforcement for SameSite=none cookiesDD_SERVER_COOKIE_SAMESITE=none now requires HTTPS configuration (DD_SERVER_TLS_ENABLED=true or DD_SERVER_TRUSTPROXY) and throws at startup if neither is set.
  • Remember-me endpoint requires authentication/auth/remember POST moved after requireAuthentication middleware, preventing unauthenticated access.
  • Env reveal rate limit tightened/api/containers/:id/env rate limit reduced from 100/min to 10/min to prevent credential enumeration. Server error responses return generic messages instead of internal details.
  • Trivy command path validation — Trivy binary paths are validated against shell metacharacters and path traversal before execution.
  • Digest scan cache LRU eviction — Scan result cache uses LRU eviction (max 500 entries, configurable via DD_SECURITY_SCAN_DIGEST_CACHE_MAX_ENTRIES) to prevent unbounded memory growth. Trivy DB status lookups are deduplicated across concurrent calls.
  • CSP configured for Iconify CDN — Content-Security-Policy updated to allow connect-src for the Iconify CDN origin, preventing blocked icon fetches in the browser.
  • CSP connect-src restricted in internetless mode — Content-Security-Policy connect-src directive tightened to 'self' when running in internetless mode, blocking outbound connections from the browser.
  • Legacy auth methods endpoint rate-limited/api/auth/methods rate-limited to prevent enumeration of available authentication providers.
  • Removed plaintext credentials from login request body — The Basic auth login was redundantly sending username and password in both the Authorization header and the JSON body. The backend only reads the Authorization header via Passport, so the body credentials were unnecessary exposure.
  • Server-issued SSE client identity — Self-update ack requests validated against server-issued tokens, preventing spoofed acknowledgments.
  • Fail-closed auth across watchers, registries, and triggers — Token exchange failures no longer fall through to anonymous access.
  • Runtime env values redacted — Container environment variable values stripped from API responses to prevent credential leakage.
  • OIDC authorization redirect URL validation — Added allowlist-based validation for OIDC authorization redirect URLs, preventing open redirect attacks through crafted callback parameters.
  • Auth, registry token, and log sanitization hardening — Consolidated security pass hardening authentication flows, registry token validation, and log output sanitization.
  • Command trigger shell execution warning — Command trigger now logs a one-time security warning on first execution, reminding operators that commands run with drydock process privileges.
  • Login brute-force lockout — Per-account and per-IP lockout after configurable failed login attempts (DD_AUTH_ACCOUNT_LOCKOUT_MAX_ATTEMPTS, DD_AUTH_IP_LOCKOUT_MAX_ATTEMPTS) with configurable window and duration.
  • Concurrent session limits — Maximum authenticated sessions per user (default 5, configurable via DD_SERVER_SESSION_MAXCONCURRENTSESSIONS). Oldest sessions are revoked first when the limit is reached.
  • Destructive action confirmation header — Dangerous operations (delete container, restore backup, delete all containers) now require an X-DD-Confirm-Action header, returning 428 when missing.
  • Native argon2id password hashing — Replaced abandoned pass package with node:crypto argon2Sync for Basic auth. OWASP-aligned parameters with timing-safe comparison. Legacy {SHA} hashes accepted with deprecation warnings.
  • Full credential redactionComponent.mask() now returns [REDACTED] instead of leaking prefix/suffix characters in logs and API responses.
  • Trigger infrastructure config redaction — Webhook URLs, hostnames, channels, and usernames are redacted from trigger configuration in API responses.
  • Website SRI integrity hashes — Post-build script injects subresource integrity hashes for static assets on the documentation website.
  • Fail-closed webhook token enforcement — Per-endpoint webhook tokens now fail closed when a token is configured but the request provides no token or a mismatched token, preventing bypass through missing headers.
  • API error message sanitization — API routes no longer expose raw Joi validation or exception messages to clients. New sanitizeApiError() helper returns generic messages while logging real details server-side.
  • Trigger request body schema validationPOST /api/triggers/:type/:name and remote trigger endpoints now validate request bodies with Joi (require id string, reject type coercion via convert: false).
  • HTTP trigger auth schema enforcement at startup — HTTP trigger Joi schema now conditionally requires user+password for BASIC auth and bearer for BEARER auth at registration time, catching misconfigurations before first trigger execution.
  • CORS implicit wildcard origin deprecation warning — Startup warning when DD_SERVER_CORS_ENABLED=true without explicit DD_SERVER_CORS_ORIGIN. Default wildcard will require explicit opt-in in v1.5.0.
  • Identity-aware rate limit keying — Opt-in DD_SERVER_RATELIMIT_IDENTITYKEYING=true keys authenticated route rate limits by session/username instead of IP, preventing collisions for multiple users behind shared proxies. Unauthenticated routes remain IP-keyed. Disabled by default.
  • Reactive server feature flags in UI — Container action buttons (update, rollback, scan, triggers) are now gated by server-side feature flags via a useServerFeatures composable. When features like DD_SERVER_FEATURE_CONTAINERACTIONS are disabled, buttons show a disabled state with tooltip explaining why instead of silently failing at runtime.
  • Compose trigger hardening — Auto compose file detection from container labels (com.docker.compose.project.config_files) with Docker inspect fallback, pre-commit docker compose config --quiet validation before writes, compose file reconciliation (warn/block modes for runtime vs compose image drift), optional digest pinning (DIGESTPINNING trigger config), compose-file-once batch mode for multi-service stacks, multi-file compose chain awareness with deterministic writable target selection, compose metadata in update preview API, and compose file path display in container detail UI.
  • Unsupported hash formats fail closed — Basic auth now rejects unsupported hash formats instead of falling through to plaintext comparison, preventing accidental plaintext password acceptance.

Performance

  • Production source maps disabled — UI production builds now exclude source maps, reducing bundle size and improving deployment efficiency.
  • Audit store indexed date-range queries — Audit entries now store a pre-parsed timestampMs index for numeric comparisons. Date-range queries use the LokiJS chain API with indexed filtering instead of full-collection scans. Automatic 30-day retention with periodic pruning.
  • Backup store indexed lookups — Backup collection adds indices on data.containerName and data.id, using findOne() for single-document lookups and indexed find() for name-filtered queries instead of full scans.
  • LokiJS autosave interval set to 60 seconds — Fixed autosave interval at 60s instead of the LokiJS default, reducing disk I/O while maintaining acceptable data durability.
  • SSE shared heartbeat interval — Deduplicated per-client SSE heartbeat timers into a single shared interval that starts on first connection and stops when all clients disconnect.
  • LoginView exponential backoff — Login page connectivity retry uses exponential backoff (5s doubling to 30s max) instead of fixed intervals, reducing server load during outages.
  • Gzip response compression — API responses compressed above configurable threshold with automatic SSE exclusion.
  • Skip connectivity polling when SSE connection is active — Eliminates unnecessary /auth/user fetches every 10s during normal operation.
  • Set-based lookups replace linear scans — Repeated array lookups converted to Set operations in core paths.
  • Vite chunk splitting — Production build now splits vendor code into framework, icons, and vendor chunks for better browser cache efficiency across deployments.
  • Session index cache with atomic login locks — Auth session lookups use an indexed cache for O(1) access. Login operations use atomic locks to prevent race conditions during concurrent authentication attempts.
  • Proactive store cache eviction — LokiJS store proactively evicts stale cache entries before insertion, reducing memory pressure during high-throughput container watch cycles.
  • Async secret file loading — Secret file loading (DD_*__FILE env vars) uses fs/promises for non-blocking reads during startup configuration, improving initialization time with many secret files.

Dependencies

  • cron-parser v2 to v5 — Upgraded cron-parser from v2 to v5 (CronExpressionParser API). Note: joi-cron-expression still uses cron-parser v2 internally as its own dependency.
  • Removed pass package — Replaced abandoned pass password hashing with native node:crypto (covered under Security below).
  • Trivy upgraded to 0.69.3 — Bumped Trivy version in qlty configuration for latest vulnerability database and scanner improvements.

Deprecated

  • SHA-1 basic auth password hashes — Legacy {SHA}<base64> password hashes are accepted with deprecation warnings. SHA-1 support will be removed in v1.6.0. Migrate to argon2id hashing.

1.3.9 — 2026-02-22

Fixed

  • Release signing broken by cosign v3 API changecosign sign-blob v3 silently ignores --output-signature and --output-certificate in keyless OIDC mode, producing an empty .sig file that fails upload. Release workflow now extracts signature and certificate from the cosign .bundle JSON as a fallback, handling both old (base64Signature/cert) and new (messageSignature.signature/verificationMaterial.certificate.rawBytes) bundle formats.
  • Shellcheck SC2086 in release signing step — Unquoted ${TAGS} expansion in container image signing replaced with read-loop into array to eliminate word-splitting/globbing risk.

Changed

  • CI and lefthook now run identical lint checks — CI lint job previously ran qlty check --filter biome (1 plugin) while lefthook ran qlty check (17 plugins). Both now run qlty check --all from the repo root, ensuring local pre-push catches exactly what CI catches.
  • Pre-commit hook auto-fixes lint issuesqlty check --fix runs on staged files at commit time, followed by a verify step. Lint drift no longer accumulates until push time.
  • Lefthook pre-push is sequential fail-fast — Switched from piped: false (parallel) to piped: true with priority ordering so failures surface immediately with clear output.

1.3.8 — 2026-02-22

Fixed

  • Docker Compose trigger silently no-ops for updateKind: unknown — When the update model classifies a change as unknown (e.g. created-date-only updates, unrecognized tag formats), getNewImageFullName resolved the update image identically to the current image, causing both compose-update and runtime-update filters to return empty arrays and log "All containers already up to date". The runtime-update filter now also triggers when container.updateAvailable === true, ensuring containers with confirmed updates are recreated regardless of updateKind classification. Compose file rewrites remain gated on explicit tag deltas. (#91)
  • Digest watch masks tag updates, pulling old image — When digest watch was enabled on a container with both a tag change and a digest change (e.g. v2.59.0-s6v2.60.0-s6), the update model gave digest unconditional priority, returning kind: 'digest' instead of kind: 'tag'. The trigger then resolved the image to the current tag (correct for digest-only updates) instead of the new tag, pulling the old image. Tag updates now take priority over digest when both are present. This bug was inherited from the upstream project (WUD). (#91)
  • Database not persisted on container shutdown — LokiJS relies on its autosave interval to flush data to disk, but the graceful shutdown handler called process.exit() before the next autosave tick could fire, causing any in-memory changes since the last autosave to be lost. This manifested as stale version numbers, lost update policies, and missing audit log entries after restarting the drydock container. Now explicitly saves the database during shutdown before exiting. This bug was inherited from the upstream project (WUD) but made deterministic by our graceful shutdown changes. (#96)

1.3.7 — 2026-02-21

Fixed

  • Tag regex OOM crash with re2-wasm — Replaced re2-wasm with re2js (pure JavaScript RE2 port). The WASM binary had a hard 16 MB memory ceiling with no growth allowed, causing abort() crashes on valid regex patterns like ^v(\d+\.\d+\.\d+)-ls\d+$. Since re2-wasm is abandoned (last npm publish Sep 2021) with no path to a fix, re2js provides the same linear-time ReDoS protection without WASM memory limits or native compilation requirements. (#89)
  • Self-signed/private CA support for self-hosted registries — Added optional CAFILE and INSECURE TLS options for self-hosted registry providers (Custom, Gitea, Forgejo, Harbor, Artifactory, Nexus). This allows private registries with internal or self-signed certificates to pass TLS validation via a mounted CA bundle, or to explicitly disable verification for trusted internal networks. (#88)
  • Docker Compose trigger silently no-ops on digest updates — Digest-only updates (same tag, new image hash) were filtered out entirely because the compose image string didn't change, causing the trigger to report success without recreating the container. Now digest updates skip the compose file write (correct — tag hasn't changed) but still trigger container recreation to pull the new image. (#91)

Changed

  • Gitea refactored to shared base class — Gitea now extends SelfHostedBasic directly instead of duplicating its logic from Custom, reducing code and ensuring consistent behavior with Harbor, Nexus, and Artifactory.
  • Lint tooling migrated from biome CLI to qlty — Removed @biomejs/biome as a direct devDependency from all workspaces; biome is now managed centrally via qlty. Lint and format scripts updated to use qlty check/qlty fmt.
  • Dependabot replaced with Renovate — Switched dependency update bot for better monorepo grouping, auto-merge of patch updates, and pinned GitHub Actions digests.
  • Socket Firewall switched to free mode — The CI supply chain scan now uses firewall-free (blocks known malware, no token required) instead of firewall-enterprise.
  • CI pipeline improvements — Added npm and Docker layer caching, parallelized e2e/load-test jobs, reordered job dependencies for faster feedback, added harden-runner to all workflow jobs.
  • CI credential hardening — Bumped harden-runner v2.11.1 → v2.14.2 (fixes GHSA-cpmj-h4f6-r6pq) and added persist-credentials: false to all actions/checkout steps across all workflows to prevent credential leakage through artifacts.
  • Zizmor added to local pre-push checks — GitHub Actions security linter now runs via qlty alongside biome, catching workflow misconfigurations before push.
  • Lefthook pre-push runs piped — Commands now run sequentially with fail-fast instead of parallel, so failures surface immediately instead of hanging while other commands complete.

1.3.6 — 2026-02-20

Fixed

  • GHCR anonymous auth returns 401 on public repos — The v1.3.3 fix for anonymous bearer tokens (Og==) removed the auth header entirely, but GHCR requires a token exchange even for unauthenticated pulls. Replaced direct bearer auth with proper token exchange via https://ghcr.io/token, matching the Hub/Quay pattern. Authenticated requests add Basic credentials to the token request; anonymous requests omit them. LSCR inherits the fix automatically. (#85, #86)

1.3.5 — 2026-02-19

Fixed

  • Container exits immediately when socket GID has no named groupDocker.entrypoint.sh treated getent group <gid> failures as fatal under set -e -o pipefail, so mounts where /var/run/docker.sock had a numeric GID not present in /etc/group caused an immediate exit (status=exited, exit=2) before app startup. The group lookup is now tolerant and falls back to creating a matching group as intended. (#82)
  • Log pretty-printing no longer depends on shell pipes — Moved human-readable formatting from the entrypoint pipeline (node | pino-pretty) into the app logger configuration. This preserves proper exec/signal behavior under tini while keeping DD_LOG_FORMAT=json support.

1.3.4 — 2026-02-19

Fixed

  • Backup lookup broken after container update — Backups were keyed by Docker container ID, which changes on every recreate (e.g. after an update). Switched all backup queries to use the stable container name, so backups are always found regardless of container ID changes. (#79)
  • Image prune deletes backup imagecleanupOldImages removed the previous image tag after updates, making rollback impossible. Now checks retained backup tags before pruning and skips images that are needed for rollback.
  • Auto-rollback monitor uses stale container ID — After an update recreates the container, maybeStartAutoRollbackMonitor passed the old (now-deleted) container ID to the health monitor. Now looks up the new container by name and passes the correct ID.
  • Backup stores internal registry name instead of Docker-pullable name — Backup imageName was stored as the internal registry-prefixed name (e.g. hub.public/library/nginx) which is not a valid Docker image reference. Rollback would fail with DNS lookup errors. Now stores the Docker-pullable base name (e.g. nginx) using the registry's getImageFullName method.
  • Rollback API docs incorrect endpoint — Fixed documentation showing /api/backup/:id/rollback instead of the correct /api/containers/:id/rollback.

1.3.3 — 2026-02-18

Fixed

  • Self-update leaves container stopped — When drydock updated its own container, stopping the old container killed the Node process before the new one could be created, leaving the UI stuck on "Restarting..." indefinitely. Now uses a helper container pattern: renames old container, creates new container, then spawns a short-lived helper that curls the Docker socket to stop old → start new → remove old. (#76)
  • Stale digest after container updates — After a container was updated (new image pulled, container recreated), the next watch cycle still showed the old digest because the early-return path in addImageDetailsToContainer skipped re-inspecting the Docker image. Now re-inspects the local image on each watch cycle to refresh digest, image ID, and created date. (#76)
  • express-rate-limit IPv6 key generation warning — Removed custom keyGenerator from the container scan rate-limiter that bypassed built-in IPv6 normalization, causing ERR_ERL_KEY_GEN_IPV6 validation errors.
  • express-rate-limit X-Forwarded-For warning — Added validate: { xForwardedForHeader: false } to all 6 rate-limiters to suppress noisy ERR_ERL_UNEXPECTED_X_FORWARDED_FOR warnings when running without trust proxy (e.g. direct Docker port mapping).
  • Quay auth token extraction broken — Fixed authenticate() reading response.token instead of response.data.token, causing authenticated pulls to silently run unauthenticated. Also affects Trueforge via inheritance.
  • GHCR anonymous bearer token — Fixed anonymous configurations sending Authorization: Bearer Og== (base64 of :) instead of no auth header, which could break public image access.
  • Created-date-only updates crash trigger execution — Fixed getNewImageFullName() crashing on .includes() of undefined when a container had only a created-date change (no tag change). Now rejects unknown update kind in threshold logic.
  • Compose write failure allows container updates — Fixed writeComposeFile() swallowing errors, allowing processComposeFile() to proceed with container updates even when the file write failed, causing runtime/file state desynchronization.
  • Self-update fallback removes running old container — Fixed helper script running removeOld after the fallback path (startOld), which would delete the running old container. Now only removes old after successful new container start.
  • Registry calls have no timeout — Added 30-second timeout to all registry API calls via Axios. Previously a hung registry could stall the entire watch cycle indefinitely.
  • HTTP trigger providers have no timeout — Added 30-second timeout to all outbound HTTP trigger calls (Http, Apprise, Discord, Teams, Telegram). Previously a slow upstream could block trigger execution indefinitely.
  • Kafka producer connection leak — Fixed producer connections never being disconnected after send, leaking TCP connections to the broker over time. Now wraps send in try/finally with disconnect.
  • Rollback timer labels not validated — Invalid dd.rollback.window or dd.rollback.interval label values (NaN, negative, zero) could cause setInterval to fire continuously. Now validates with Number.isFinite() and falls back to defaults.
  • Health monitor overlapping async checks — Added in-flight guard to prevent overlapping health checks from triggering duplicate rollback executions when inspections take longer than the poll interval.
  • Anonymous login double navigation guard — Fixed beforeRouteEnter calling next() twice when anonymous auth was enabled, causing Vue Router errors and nondeterministic redirects.
  • Container API response not validated — Fixed getAllContainers() not checking response.ok before parsing, allowing error payloads to be treated as container arrays and crash computed properties.

Security

  • fast-xml-parser DoS via entity expansion — Override fast-xml-parser 5.3.4→5.3.6 to fix CVE GHSA-jmr7-xgp7-cmfj (transitive dep via @aws-sdk/client-ecr, upstream hasn't released a fix yet).
  • tar arbitrary file read/write — Removed tar from dependency graph entirely by replacing native re2 (which pulled in node-gyptar) with re2-wasm (v1.3.3), later replaced by re2js (v1.3.7) due to WASM memory limits. Previously affected by CVE GHSA-83g3-92jg-28cx.
  • Unauthenticated SSE endpoint — Moved /api/events/ui behind requireAuthentication middleware and added per-IP connection limits (max 10) to prevent connection exhaustion.
  • Session cookie missing sameSite — Set sameSite: 'strict' on session cookie to mitigate CSRF attacks.
  • Predictable session secret — Added DD_SESSION_SECRET environment variable override so deployments can provide proper entropy instead of the default deterministic UUIDv5.
  • Global error handler leaks internal details — Replaced err.message with generic 'Internal server error' in the global error handler to prevent leaking hostnames, paths, and Docker socket info to unauthenticated callers.
  • Entrypoint masks crash exit codes — Enabled pipefail in Docker.entrypoint.sh so node | pino-pretty correctly propagates non-zero exit codes for restart policies.

1.3.2 — 2026-02-16

Added

  • Log viewer auto-fetch polling — Configurable auto-fetch interval (Off / 2s / 5s / 10s / 30s) for both application and container log viewers, replacing manual-only refresh. Defaults to 5 seconds for a near-real-time tail experience. (#57)
  • Log viewer scroll lock — Scrolling away from the bottom pauses auto-scroll, showing a "Scroll locked" indicator and "Resume" button. New log data continues to load in the background without yanking the user's scroll position. (#57)
  • Log viewer auto-scroll — New log entries automatically scroll the view to the bottom when the user is near the end, providing a tail-like experience. (#57)
  • Shared log viewer composable — Extracted useLogViewerBehavior composable with useLogViewport (scroll management) and useAutoFetchLogs (interval timer lifecycle) to eliminate duplication between application and container log views.
  • 7 new registry providers — Added OCIR (Oracle Cloud), IBMCR (IBM Cloud), ALICR (Alibaba Cloud), GAR (Google Artifact Registry), Harbor, JFrog Artifactory, and Sonatype Nexus. Includes a shared SelfHostedBasic base class for self-hosted registries with basic auth.
  • 4 new trigger providers — Added Mattermost, Microsoft Teams (Adaptive Cards), Matrix, and Google Chat notification triggers.

Fixed

  • v1 manifest digest watch using image ID instead of repo digest — Fixed handleDigestWatch() incorrectly reading Config.Image (the local image ID) as the digest for v1 manifest images, causing perpetual false "update available" notifications. Now uses the repo digest from RepoDigests instead. (getwud/wud#934)
  • Discord trigger broken after request→axios migration — Fixed sendMessage() using request-style properties (uri, body) instead of axios properties (url, data), causing "Invalid URL" errors on all Discord webhook calls. (getwud/wud#933)

1.3.1 — 2026-02-15

Fixed

  • Release SBOM generation for multi-arch images — Replaced anchore/sbom-action (which fails on manifest list digests from multi-platform builds) with Docker buildx native SBOM generation (sbom: true), producing per-platform SBOMs embedded in image attestations.

Security

  • Pin Trivy install script by commit hash — Replaced mutable main branch reference in Dockerfile curl | sh with a pinned commit SHA to satisfy OpenSSF Scorecard pinned-dependencies check and prevent supply-chain risk from upstream changes.

1.3.0 — 2026-02-15

Fixed

  • OIDC session resilience for WUD migrations — Corrupt or incompatible session data (e.g. from WUD's connect-loki store) no longer causes 500 errors. Sessions that fail to reload are automatically regenerated. All OIDC error responses now return JSON instead of plain text, preventing frontend parse errors. Added a global Express error handler to ensure unhandled exceptions return JSON.
  • Disabled X-Powered-By header — Removed the default Express X-Powered-By header from both the main API and agent API servers to reduce information exposure.
  • Trivy scan queue — Serialized concurrent Trivy invocations to prevent "cache may be in use by another process" errors when multiple containers are scanned simultaneously (batch triggers, on-demand scans, SBOM generation).
  • Login error on wrong passwordloginBasic() attempted to parse the response body as JSON even on 401 failures, causing Unexpected token 'U', "Unauthorized" is not valid JSON errors instead of the friendly "Username or password error" message.
  • Snackbar notification colors ignoring level — The SnackBar component had a hardcoded color="primary" instead of binding to the level prop, causing error and warning notifications to display as blue instead of red/amber.
  • SBOM format key mismatch — Fixed container model schema validating SBOM formats against cyclonedx instead of the correct cyclonedx-json key.

Added

  • Snyk vulnerability monitoring — Integrated Snyk for continuous dependency scanning of app/package.json and ui/package.json. Added Snyk badge to README with targetFile parameter for monorepo support.
  • Update Bouncer (Trivy safe-pull gate) — Added pre-update vulnerability scanning for Docker-triggered updates. Candidate images are scanned before pull/restart, updates are blocked when vulnerabilities match configured blocking severities, and latest scan data is persisted on container.security.scan. Added GET /api/containers/:id/vulnerabilities endpoint for retrieving scan results.
  • Update Bouncer signature verification (cosign) — Added optional pre-update image signature verification. When enabled, Docker-triggered updates are blocked if candidate image signatures are missing/invalid or verification fails.
  • Update Bouncer SBOM generation — Added Trivy SBOM generation (spdx-json, cyclonedx-json) for candidate images with persistence in container.security.sbom and a new GET /api/containers/:id/sbom API endpoint (with format query support).
  • Container card security status chip — Added a vulnerability chip on container cards showing Update Bouncer scan status (safe, blocked, scan error) with severity summary tooltip data from container.security.scan.
  • On-demand security scan — Added POST /api/containers/:id/scan endpoint for triggering vulnerability scan, signature verification, and SBOM generation on demand. Broadcasts dd:scan-started and dd:scan-completed SSE events for real-time UI feedback. Added shield button to container card actions and mobile overflow menu.
  • Direct container update from UI — Added POST /api/containers/:id/update endpoint that triggers a Docker update directly without requiring trigger configuration. The "Update now" button in the UI now calls this single endpoint instead of looping through configured triggers.
  • Trivy and cosign in official image — The official drydock image now includes both trivy and cosign binaries, removing the need for custom images in local CLI mode.

Changed

  • README badge layout — Added line breaks to badge rows for a cleaner two-line layout across all three badge sections.
  • Grafana dashboard overhaul — Updated overview dashboard with standard datasource naming (DS_PROMETHEUS), added bar chart and pie chart panels, and restructured panel layout for better monitoring coverage.
  • Mobile responsive dashboard — Stat cards now stack full-width on small screens with tighter vertical spacing for a cleaner mobile layout.
  • Self-update overlay rendering — Switched logo images from v-if to v-show to avoid re-mount flicker during self-update phase transitions.
  • Container sort simplification — Simplified null-group sorting in ContainersView using sentinel value instead of multi-branch conditionals.
  • Test coverage improvements — Expanded app test coverage for API routes (backup, container-actions, preview, webhook), OIDC authentication, registry component resolution, tag parsing, and log sanitization. Expanded UI test coverage across 38 spec files with improved Vuetify stub fidelity (v-tooltip activator slot, v-list-item slots, app-bar-nav-icon events).
  • Vitest coverage config — Narrowed coverage to .js/.ts files only (excluding .vue SFCs) to avoid non-actionable template branch noise.
  • Prometheus counter deduplication — Extracted shared createCounter factory in app/prometheus/counter-factory.ts, reducing boilerplate across audit, webhook, trigger, and container-actions counter modules.
  • API error handler deduplication — Extracted shared handleContainerActionError helper in app/api/helpers.ts, consolidating duplicate catch-block logic across backup, preview, and container-actions routes.
  • Lint and code quality fixes — Fixed biome noPrototypeBuiltins warning in OIDC tests, added id attributes to README HTML headings to resolve markdownlint MD051, and tuned qlty smell thresholds.

Security

  • CodeQL alert fixes — Fixed log injection vulnerabilities by sanitizing user-controlled input before logging. Removed unused variables flagged by static analysis. Added rate limiting to the on-demand scan endpoint.
  • Build provenance and SBOM attestations — Added supply chain attestations to release workflow for verifiable build provenance.

1.2.0

Added

  • Grafana dashboard template — Importable Grafana JSON dashboard with panels for overview stats, watcher activity, trigger execution, registry response times, and audit entries. Uses datasource templating for portable Prometheus configuration.
  • Audit log backendAuditEntry model, LokiJS-backed store with pagination and pruning, GET /api/audit endpoint with filtering, dd_audit_entries_total Prometheus counter, and automatic logging of container lifecycle events (update-available, update-applied, update-failed, rollback, preview, container-added, container-removed).
  • Font Awesome 6 migration — Replaced all Material Design Icons (mdi-*) with Font Awesome 6 equivalents. Configured Vuetify FA icon set, updated all service icon getters, component templates, and 54 test files.
  • Dry-run preview APIPOST /api/containers/:id/preview returns what an update would do (current/new image, update kind, running state, networks) without performing it.
  • Pre-update image backup and rollback — LokiJS-backed backup store records container image state before each Docker trigger update. GET /api/backups, GET /api/:id/backups, and POST /api/:id/rollback endpoints. Configurable retention via DD_TRIGGER_DOCKER_{name}_BACKUP_COUNT (default 3).
  • Frontend wiring — Preview dialog with loading/error/success states wired to dry-run API. Full audit log table with filtering, pagination, and responsive column hiding replacing the MonitoringHistory placeholder. Recent Activity dashboard card showing latest 5 audit entries.
  • Container action bar refactor — Replaced 3-column text button layout with compact icon-button toolbar and tooltips (desktop) or overflow menu (mobile).
  • Dashboard second row — Added Recent Activity and stats cards as a second row on the dashboard.
  • UI modernization — Consistent pa-4 padding, outlined/rounded cards, tonal chips, styled empty states, and Font Awesome icons across all views and components.
  • Container actions (start/stop/restart) — New API endpoints and UI buttons to start, stop, and restart Docker containers directly from the dashboard. Gated by DD_SERVER_FEATURE_CONTAINERACTIONS (default: enabled). Includes audit logging, Prometheus counter (dd_container_actions_total), desktop toolbar buttons with disabled-state awareness, and mobile overflow menu integration.
  • Webhook API for on-demand triggers — Token-authenticated HTTP endpoints (POST /api/webhook/watch, /watch/:name, /update/:name) for CI/CD integration. Gated by DD_SERVER_WEBHOOK_ENABLED and DD_SERVER_WEBHOOK_TOKEN. Includes rate limiting (30 req/15min), audit logging, Prometheus counter (dd_webhook_total), and a configuration info panel on the Server settings page.
  • Container grouping / stack views — New GET /api/containers/groups endpoint returns containers grouped by stack. Supports explicit group assignment via dd.group / wud.group labels with automatic fallback to com.docker.compose.project. Collapsible ContainerGroup component with group header showing name, container count, and update badges. "Smart group" filter option for automatic stack detection (dd.group > wud.group > compose project). "Update all in group" action to batch-update all containers in a group.
  • Graceful self-update UI — Self-update detection when drydock updates its own container. Server-Sent Events (SSE) endpoint at /api/events/ui for real-time browser push. Full-screen DVD-style bouncing whale logo overlay during self-updates with smooth phase transitions (updating, restarting, reconnecting, ready). Automatic health polling and page reload after restart.
  • Lifecycle hooks (pre/post-update commands) — Execute shell commands before and after container updates via dd.hook.pre and dd.hook.post labels. Pre-hook failures abort the update by default (dd.hook.pre.abort=true). Configurable timeout via dd.hook.timeout (default 60s). Environment variables exposed: DD_CONTAINER_NAME, DD_IMAGE_NAME, DD_TAG_OLD, DD_TAG_NEW, etc. Includes audit logging for hook success/failure and UI display in ContainerDetail panel.
  • Automatic rollback on health check failure — Monitors container health after updates and automatically rolls back to the previous image if the container becomes unhealthy. Configured via dd.rollback.auto=true, dd.rollback.window (default 300s), and dd.rollback.interval (default 10s). Requires Docker HEALTHCHECK on the container. Uses existing backup store for rollback images. Includes audit logging and UI display in ContainerDetail panel.
  • selfhst/icons as primary icon CDN — Switched to selfhst/icons as the primary icon CDN with homarr-labs as fallback, improving icon availability and coverage.

Fixed

  • Navigation drawer not visible — Used computed model for permanent/temporary modes; passing model-value=undefined caused Vuetify to treat the drawer as closed.
  • Dark theme missing colors — Added info, success, and warning color definitions to the dark theme.
  • ContainerPreview updateKind display — Fixed structured updateKind object rendering with semver-diff color coding.
  • Invalid text-body-3 CSS class — Replaced with valid text-body-2 in ConfigurationItem and TriggerDetail.
  • 404 catch-all route — Added catch-all redirect to home for unknown routes.
  • False downgrade suggestion for multi-segment tags — Fixed semver parsing/comparison for numeric tags like 25.04.2.1.1 so newer major tags are no longer suggested as downgrades. (#47)
  • Configured path hardening for filesystem reads — Added validated path resolution helpers and applied them to store paths, watcher TLS files, and MQTT TLS files before filesystem access.

Changed

  • Audit event wiring — Wired audit log entries and Prometheus counter increments for rollback, preview, container-added, container-removed, update-applied, and update-failed events. Registered ContainerUpdateFailed event with try/catch in Docker trigger.
  • Test updates — 20+ test files updated for v1.2.0 icon changes, CSS selectors, HomeView data model, theme toggle relocation, and audit module wiring. Removed obsolete specs.
  • Updated doc icon examples — Switched icon examples to prefer hl: and si: prefixes over deprecated mdi:.
  • Code quality tooling consolidation — Replaced Codacy + SonarCloud with Qlty + Snyk. Rewrote lefthook.yml pre-push hooks to run qlty check, snyk test, snyk code test (informational), builds, and tests. Added scripts/snyk-code-gate.sh wrapper.
  • Biome formatting — Applied biome format across entire codebase for consistent code style.
  • README badges — Replaced Codacy/SonarCloud badges with CI status, Qlty maintainability, and Snyk badges.
  • ConfigurationItem redesign — Icon moved to the left with name as prominent text and type as subtitle, replacing the old badge/chip pattern across all configuration pages.
  • TriggerDetail redesign — Same modern layout treatment as ConfigurationItem (icon left, name prominent, type subtitle).
  • Registry page brand colors — Added brand-colored icon backgrounds for each registry provider (Docker blue, GitHub purple, AWS orange, Google blue, etc.) via getRegistryProviderColor() helper and new iconColor prop on ConfigurationItem.
  • Consistent card styling — Unified variant="outlined" rounded="lg" across ContainerItem, ContainerGroup, ContainerTrigger, and WebhookInfo cards for a cohesive look.
  • Home page severity badges removed — Removed redundant MAJOR/MINOR severity badges from the container updates list; version chip color already indicates severity.
  • History page filter bar — Removed redundant "Update History" heading (already in app bar) and added a collapsible filter bar with active filter chips.
  • Logs page spacing — Fixed spacing between the config item and logs card.
  • Self-update overlay responsive — Mobile-responsive self-update overlay uses static top-center positioning with fade-in animation on small screens instead of DVD bounce.
  • QA compose enhancements — Added HTTP trigger, basic auth, and webhook configuration to test/qa-compose.yml for integration testing.
  • Login page redesign — Redesigned login page with new font, icon colors, and layout polish.
  • Docker Hub and Quay.io multi-registry publishing — Container images now published to Docker Hub and Quay.io alongside GHCR for broader registry availability.
  • Mobile responsive dashboard — Per-type colored update badges (major=red, minor=warning, patch=success, digest=info) and icon-only tabs on mobile viewports.
  • Dark mode app bar logo inversion — App bar logo now inverts correctly in dark mode for improved visibility.
  • History page mobile improvements — Shorter timestamps, hidden status column, and truncated container names on mobile viewports.
  • Container filter mobile labels — Short labels ("Updates", "Time") on mobile breakpoint for compact filter display.
  • Biome and Qlty config alignment — Aligned Biome and Qlty configurations for consistent code quality enforcement.

Security

  • RE2 regex engine — Replaced native RegExp with Google's RE2 (re2 npm package) for all user-supplied regex patterns (includeTags, excludeTags, transformTags). RE2 uses a linear-time matching algorithm that is inherently immune to ReDoS catastrophic backtracking.
  • Docs dependency vulnerability fixes — Fixed 9 CVEs in docs/ transitive dependencies via npm overrides (dompurify 2→3, marked 1→4, got 9→11).

Removed

  • Dead code removal — Deleted unused AppFooter and ConfigurationStateView components, dead computed props (filteredUpdates, upToDateCount), duplicate isTriggering reset, dead mdi: prefix replacement in IconRenderer, dead container-deleted listener, and Maintenance Windows placeholder.
  • Removed @mdi/font dependency — Dropped unused Material Design Icons package.
  • Removed Codacy and SonarCloud — Replaced with Qlty (local code quality) and Snyk (dependency + SAST scanning) for a unified local-first quality gate.
  • Removed stale tracking docs — Deleted SONARQUBE-ISSUES.md, docs/sonar-smells-tracking.md, and docs/codacy-high-findings-tracking.md.

Documentation

  • Popular imgset presets — Added a curated preset guide at docs/configuration/watchers/popular-imgsets.md and linked it from watcher docs.

1.1.3

Fixed

  • ERR_ERL_PERMISSIVE_TRUST_PROXY on startup — Express trust proxy was hard-coded to true, which triggers a validation error in express-rate-limit v8+ when the default key generator infers client IP from X-Forwarded-For. Replaced with a configurable DD_SERVER_TRUSTPROXY env var (default: false). Set to 1 (hop count) when behind a single reverse proxy, or a specific IP/CIDR for tighter control. (#43)

1.1.2

Fixed

  • Misleading docker-compose file error messages — When a compose file had a permission error (EACCES), the log incorrectly reported "does not exist" instead of "permission denied". Now distinguishes between missing files and permission issues with actionable guidance. (#42)
  • Agent watcher registration fails on startup — Agent component path resolved outside the runtime root (../agent/components instead of agent/components), causing "Unknown watcher provider: 'docker'" errors and preventing agent watchers/triggers from registering. (#42)

Changed

  • Debug logging for component registration — Added debug-level logging showing resolved module paths during component registration and agent component registration attempts, making path resolution issues easier to diagnose.

1.1.1 - 2026-02-11

Fixed

  • Read-only Docker socket support — Drydock's privilege drop prevented non-root users from connecting to :ro socket mounts. Added DD_RUN_AS_ROOT=true env var to skip the drop, improved EACCES error messages with actionable guidance, and documented socket proxy as the recommended secure alternative. (#38)
  • Prometheus container gauge crash with agent containers — The container gauge used a blacklist filter that let unknown properties (like agent) slip through and crash prom-client. Switched to a whitelist of known label names so unknown properties are silently ignored. (#39)
  • Snackbar toast transparency — Used flat variant for solid background on toast notifications.
  • Container filter layout broken on narrow viewports — Filter columns rendered text vertically when the nav drawer was open because all 8 v-col elements had no width constraints. Added responsive breakpoints (cols/sm/md) so filters wrap properly across screen sizes. (#40)

1.1.0 - 2026-02-10

Added

  • Application log viewer — New Configuration > Logs page with a terminal-style viewer for drydock's own runtime logs (startup, polling, registry checks, trigger events, errors). Backed by an in-memory ring buffer (last 1,000 entries) exposed via GET /api/log/entries. Supports level filtering (debug/info/warn/error), configurable tail count (50/100/500/1,000), color-coded output, and auto-scroll to newest entries. An info tooltip shows the configured server log level.
  • Agent log source selector — When agents are configured, a "Source" dropdown appears in the log viewer to switch between the controller's own logs and any connected agent's logs. Disconnected agents are shown but disabled. Agent logs are proxied via GET /api/agents/:name/log/entries.
  • Container log viewer — New "Logs" tab in the container detail expansion panel to view container stdout/stderr output directly in the UI with tail control and refresh.

1.0.2 - 2026-02-10

Fixed

  • Registry and trigger crashes in agent modegetSummaryTags() and getTriggerCounter() also return undefined in agent mode. Added optional chaining to all remaining Prometheus call sites so agent mode doesn't crash when processing containers or firing triggers. (Fixes #33)

1.0.1 - 2026-02-10

Fixed

  • Prometheus gauge crash in agent modegetWatchContainerGauge() returns undefined in agent mode since Prometheus is not initialized. Added optional chaining so the .set() call is safely skipped. This was the root cause of containers not being discovered in agent mode. (Fixes #23, #31)

Changed

  • su-exec privilege dropping — Entrypoint detects the docker socket GID and drops from root to the node user via su-exec when possible. Stays root only for GID 0 sockets (Docker Desktop / OrbStack). (Refs #25)
  • tini init system — Added tini as PID 1 for proper signal forwarding to the Node process.
  • Graceful shutdownSIGINT/SIGTERM handlers now call process.exit() after cleanup so the container actually stops.

1.0.0 - 2026-02-10

First semver release. Drydock adopts semantic versioning starting with this release, replacing the previous CalVer (YYYY.MM.PATCH) scheme.

Security

  • ReDoS prevention — Replaced vulnerable regexes in trigger template evaluation (Trigger.ts) with linear-time string parsing (parseMethodCall, isValidPropertyPath). Added MAX_PATTERN_LENGTH guards in tag transform (tag/index.ts) and Docker watcher (Docker.ts) to reject oversized user-supplied regex patterns.
  • XSS prevention — Added escapeHtml() sanitizer to Telegram trigger bold() method, preventing HTML injection via container names or tag values.
  • Workflow hardening — Set top-level permissions: read-all in release.yml and codeql.yml. Pinned all CodeQL action refs to commit hashes. Added CodeQL config to exclude js/clear-text-logging false positives.
  • CVE-2026-24001 — Updated diff dependency in e2e tests (4.0.2 → 4.0.4).

Changed

  • +285 UI tests — 15 new spec files and 7 expanded existing specs covering configuration views, container components, trigger detail, services, router, and app shell. UI test count: 163 → 285.
  • +59 app tests — New edge-case tests for ReDoS guard branches, parseMethodCall parsing, and Docker watcher label resolution. App test count: 1,254 → 1,313.
  • Complexity refactors — Extracted helpers from high-complexity functions: parseTriggerList/applyPolicyAction (container.ts), resolveLabelsFromContainer/mergeConfigWithImgset (Docker.ts).
  • Biome lint fixesimport type corrections and unused variable cleanup across 17 files.
  • Fixed doc links — Corrected broken fragment links in docs/_coverpage.md.

Removed

  • Removed legacy vue.config.js — Dead Vue CLI config file; project uses Vite.

2026.2.3 - 2026-02-10

Fixed

  • NTFY trigger auth 401 — Bearer token auth used unsupported axios.auth.bearer property; now sends Authorization: Bearer <token> header. Basic auth property names corrected to username/password. (#27)
  • Agent mode missing /health — Added unauthenticated /health endpoint to the agent server, mounted before the auth middleware so Docker healthchecks work without the agent secret. (#27)

Changed

  • Lefthook pre-push hooks — Added lefthook.yml with pre-push checks (lint + build + test).
  • Removed startup warning — Removed "Known Issue" notice from README now that container startup issues are resolved.

2026.2.2 - 2026-02-10

Security

  • Cosign keyless signing — Container image releases are now signed with Sigstore cosign keyless signing for supply chain integrity.
  • Least-privilege workflow permissions — Replaced overly broad read-all with minimum specific permissions across all CI/CD workflows.
  • CodeQL and Scorecard fixes — Resolved all high-severity CodeQL and OpenSSF Scorecard security alerts.
  • Pinned CI actions — All CI action references pinned to commit hashes with Dockerfile base image digest.

Added

  • Auto-dismiss notifications after container update — New resolvenotifications option for triggers (default: false). When enabled, notification triggers automatically delete the sent message after the Docker trigger successfully updates the container. Implemented for Gotify via its deleteMessage API. Other providers (Slack, Discord, ntfy) can add support by overriding the new dismiss() method on the base Trigger class. New containerUpdateApplied event emitted by the Docker trigger on successful update.

Fixed

  • Agent mode Prometheus crash — Guard getWatchContainerGauge().set() against undefined in Agent mode where Prometheus is not initialized, fixing "Cannot read properties of undefined (reading 'set')" crash (#23)
  • Sanitize version logging — Sanitize version strings from env vars before logging to resolve CodeQL clear-text-logging alerts in index.ts and store/migrate.ts
  • Broken event test assertion — Fix expect() without matcher in event test

Changed

  • 97% test coverage — Boosted from 76% to 97% with 449 new tests (1,254 total across 95 test files).
  • Fuzz testing — Added property-based fuzz tests with fast-check for Docker image name parsing.
  • Static analysis fixes — Optional chaining, String#replaceAll(), readonly modifiers, Number.NaN, concise regex syntax, removed unused imports, moved functions to outer scope.
  • Reduced code duplication — Refactored duplicated code in registries, triggers, and store test files flagged by SonarCloud.
  • Pino logging — Replaced bunyan with pino to eliminate vulnerable transitive dependencies. Added pino-pretty for human-readable log output.
  • Renamed wud to drydock — Project references updated from upstream naming across Dockerfile, entrypoint, package files, scripts, and test fixtures.
  • CONTRIBUTING.md — Added contributor guidelines.
  • OpenSSF Best Practices badge — Added to README.
  • SonarCloud integration — Added project configuration.
  • Multi-arch container images — Docker images now built for both linux/amd64 and linux/arm64 architectures, published to GHCR.
  • Lefthook pre-push hooks — Added lefthook config with pre-push checks (lint + build + test) and npm run check convenience script.
  • CodeQL query exclusion — Exclude js/clear-text-logging query (false positives on DD_VERSION env var).

2026.1.0

Added

  • Agent mode — Distributed monitoring with remote agent architecture. Agent components, SSE-based communication, dedicated API routes.
  • OIDC token lifecycle — Remote watcher HTTPS auth with Basic + Bearer token support. TLS/mTLS compatibility for DD_WATCHER_{name}_HOST.
  • OIDC device-flow (Phase 2) — RFC 8628 Device Authorization Grant for headless remote watcher auth. Auto-detection, polling with backoff, and refresh token rotation.
  • Per-image config presetsimgset defaults for per-image configuration. Added watchDigest and inspectTagPath imgset properties.
  • Hybrid triggers — Trigger group defaults (DD_TRIGGER_{name}_THRESHOLD) shared across providers. Name-only include/exclude for multi-provider trigger management.
  • Container update policy — Skip/snooze specific update versions. Per-container policy stored in DB, exposed via API and UI.
  • Metrics auth toggleDD_SERVER_METRICS_AUTH env var to disable auth on /metrics endpoint.
  • Trigger thresholds — Digest and no-digest thresholds for triggers.
  • NTFY provider-level threshold — Provider-level threshold support for ntfy trigger.
  • Docker pull progress logging — Rate-limited pull progress output during docker-compose updates.
  • Registry lookup image overridelookupImage field on registry config to override the image used for tag lookups.
  • Docker inspect tag path — Support custom tag path in Docker inspect output.
  • Anonymous LSCR and TrueForge registries — Allow anonymous access to LSCR (LinuxServer) and Quay-backed TrueForge.
  • DHI registry — New dhi.io registry provider with matcher, auth flow, and docs.
  • Custom URL icons — Support URL-based icons via dd.display.icon label.
  • Version skip — Skip specific versions in the UI.
  • Log viewer — In-app container log viewer. View Docker container stdout/stderr output directly in the UI via a new "Logs" tab on each container. Supports configurable tail line count (50/100/500), manual refresh, and Docker stream demultiplexing. Works for both local and remote agent containers.
  • Semver tag recovery — Recover include-filter mismatched semver tags from watchers. Extended to advise best semver tag when current tag is non-semver (e.g., latest).
  • Dashboard update chips — Replaced verbose update status text with compact colored chips: green "up to date" or warning "N update(s)" (clickable).

Fixed

  • eval() code injection — Replaced eval() in trigger template rendering with safe expression evaluator supporting property paths, method allowlist, ternaries, and string concatenation.
  • Digest-only update prune crash — Docker trigger prune logic now correctly excludes current image during digest-only updates and handles post-prune errors gracefully.
  • Swarm deploy-label debug logging — Added warn-level logging when Swarm service inspect fails, and debug logging showing which label sources contain dd.* labels.
  • OIDC session state races — Serialized redirect session checks, multiple pending callback states per session.
  • semverDiff undefined — Normalized semverDiff for non-tag (digest-only/created-date-only) updates.
  • Docker event stream crash — Buffered and parsed split Docker event stream payloads.
  • Multi-network container recreate — Reconnects additional networks after container recreation.
  • Remote watcher delayed first scanwatchatstart now checks watcher-local store for new remote watchers.
  • docker-compose post_start hooks — Hooks now execute after updates.
  • docker-compose image-only triggers — Only trigger on compose services with actual image changes.
  • docker-compose imageless services — Skip compose services without an image field.
  • docker-compose implicit latest tag — Normalize image: nginx to image: nginx:latest so compose triggers don't treat implicit latest as a version mismatch.
  • Express 5 wildcard routes — Named wildcard route params for express 5 compatibility.
  • Semver filtering — Fixed semver part filtering and prefix handling.
  • SMTP TLS_VERIFY invertedrejectUnauthorized was inverted; TLS_VERIFY=false now correctly allows self-signed certificates.
  • HA MQTT deprecated object_id — Replaced object_id with default_entity_id for Home Assistant 2025.10+ compatibility.
  • Open redirect on authenticated pages — Validate next query parameter to only allow internal routes.
  • Trigger test updateKind crash — Test-button triggers no longer crash with "Cannot read properties of undefined (reading 'updateKind')" on unvalidated containers.
  • Docker rename event not captured — Added rename to Docker event listener so container name updates are captured after compose recreates.
  • UI duplicate drawer logo — Removed duplicate logo in navigation drawer.

Changed

  • TypeScript migration (app) — Entire backend converted from JavaScript to TypeScript with ES Modules (NodeNext). 232 .ts files added/renamed, all .js source files removed.
  • TypeScript migration (UI) — Vue 3 frontend migrated from JS to TS. 29 .vue files updated, component props/emits typed.
  • Jest → Vitest (app) — All 64 app test files (664 tests) migrated from Jest to Vitest. Test runner unified across app and UI.
  • Jest → Vitest (UI) — UI unit tests migrated from Jest to Vitest with improved coverage.
  • Vitest 4 + modern deps — Upgraded vitest 3→4, uuid 11→13, flat 5→6, snake-case 3→4. Fixed vitest 4 mock constructor breaking change.
  • ESM baseline — Cut over to NodeNext module resolution. Removed Babel, added tsconfig.json.
  • Biome linter — Replaced ESLint with Biome for formatting and linting.
  • CI cleanup — Removed Code Climate config, renamed Travis config to ci.config.yml.

Dependencies

PackageUpstream (8.1.1)drydock
vitest3.x (Jest)4.x
uuid9.x13.x
flat5.x6.x
snake-case3.x4.x
express4.x5.x
typescript5.9
biome2.3

Stats: 392 files changed, +25,725 insertions, -25,995 deletions, 872 total tests (709 app + 163 UI).

Upstream Backports

The following changes from upstream/main (post-fork) have been ported to drydock:

DescriptionStatus
Add Codeberg to default registriesPorted (new TS provider)
Increase maxAliasCount in YAML parsingPorted
Fix authentication for private ECR registry (async getAuthPull)Ported across all registries
Prometheus: add DD_PROMETHEUS_ENABLED configPorted
Fix Authelia OIDC docs (field names)Ported
Buffer Docker event stream before JSON parseAlready fixed independently
SMTP trigger: allow display name in from address (#908)Ported

Remaining upstream-only changes (not ported — not applicable to drydock):

DescriptionReason
Fix e2e tests (x2)JS-based, drydock tests are TS
Fix prettierdrydock uses Biome
Fix codeberg testsCovered by drydock's own tests
Update changelogUpstream-specific

On this page

Unreleased[1.5.1-rc.2] — 2026-06-28ChangedFixedSecurity[1.5.1-rc.1] — 2026-06-26AddedChangedFixedSecurityUpgrade Notes1.5.0 — 2026-06-22AddedChangedFixedSecurity1.5.0-rc.38 — 2026-06-19AddedChangedSecurity1.5.0-rc.37 — 2026-06-15SecurityChanged1.5.0-rc.36 — 2026-06-15AddedFixed1.5.0-rc.35 — 2026-06-10FixedSecurityPerformanceChanged1.5.0-rc.34 — 2026-06-07AddedChangedDeprecatedRemovedFixedSecurityDependenciesDocumentation1.5.0-rc.29 — 2026-05-30Fixed1.4.5 — 2026-03-17AddedFixedSecurityChangedDocumentationDependenciesChore1.4.4 — 2026-03-16AddedChangedFixed1.4.3 — 2026-03-16FixedSecurityDocumentationDependencies1.4.2 — 2026-03-15AddedChangedDocumentationFixedSecurityDependencies1.4.1 — 2026-03-14AddedChangedFixedSecurityDependencies1.4.0 — 2026-02-28Breaking ChangesAddedBackend / CoreUI / DashboardChangedFixedSecurityPerformanceDependenciesDeprecated1.3.9 — 2026-02-22FixedChanged1.3.8 — 2026-02-22Fixed1.3.7 — 2026-02-21FixedChanged1.3.6 — 2026-02-20Fixed1.3.5 — 2026-02-19Fixed1.3.4 — 2026-02-19Fixed1.3.3 — 2026-02-18FixedSecurity1.3.2 — 2026-02-16AddedFixed1.3.1 — 2026-02-15FixedSecurity1.3.0 — 2026-02-15FixedAddedChangedSecurity1.2.0AddedFixedChangedSecurityRemovedDocumentation1.1.3Fixed1.1.2FixedChanged1.1.1 - 2026-02-11Fixed1.1.0 - 2026-02-10Added1.0.2 - 2026-02-10Fixed1.0.1 - 2026-02-10FixedChanged1.0.0 - 2026-02-10SecurityChangedRemoved2026.2.3 - 2026-02-10FixedChanged2026.2.2 - 2026-02-10SecurityAddedFixedChanged2026.1.0AddedFixedChangedDependenciesUpstream Backports