Skip to content

v1.6.0-rc.1

Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 17 Jul 00:50
cd5b18c

v1.6.0-rc.1

Full Changelog: v1.5.2...v1.6.0-rc.1

[1.6.0-rc.1] — 2026-07-15

Added

  • Actionable dry-run and update-preview UX (Discussion #321). Docker and Compose action triggers with DRYRUN=true are now marked in trigger cards, container actions, and the Update Status panel; the action is labeled Preview only, prevented replacements log at WARN, and History records update-applied-dryrun instead of claiming the container was replaced. Preview API failures now carry stable codes, sanitized details, and safe deep links for trigger/registry configuration, which the UI preserves in an accessible 44 px action.

  • Fail-closed no-worse-than-current security gating (Discussion #321). DD_SECURITY_GATE_RELATIVE=true keeps the configured absolute severity threshold, but permits an otherwise-blocked candidate when its CRITICAL, HIGH, MEDIUM, LOW, and UNKNOWN counts are each less than or equal to the exact running image's saved scan. Missing, failed, or stale current scans remain blocked. The persisted candidate scan, runtime API, OpenAPI contract, and audit row expose the comparison decision and evidence.

  • Rolling release-candidate image channel (Discussion #321). Canonical vX.Y.Z-rc.N cuts now publish X.Y-rc alongside the immutable exact tag on GHCR, Docker Hub, and Quay. Stable X.Y, X, and latest tags remain GA-only. Quick Start documents the tag matrix and recommends exact RC tags for reproducible reports.

  • Provider-neutral scanner runtime and off-heap SBOM lifecycle. Vulnerability scanning now supports trivy, grype, or normalized/deduplicated both mode across the compatibility command backend, hardened digest-pinned ephemeral Docker workers, and a Trivy-server remote composition that still models its client leg honestly. Scanner availability defaults fail-closed, with an explicit audited warn opt-in that never bypasses known blocking findings. The Security view and API expose provider/worker health plus audited pull/warm actions. Grype-only SBOM generation automatically uses Syft. New and migrated current/update SBOM bodies live in atomic checksum-validated, content-addressed /store/sbom blobs; LokiJS rows retain references and the API lazily dereferences them.

  • Global update mode and actionable Update Status panel (Discussion #325). Settings → General now selects one server-wide update mode: notify detects and notifies but refuses every Drydock-managed update, manual allows UI/API updates but suppresses automatic action-trigger dispatch, and auto allows both configured automatic actions and manual updates. The container side panel and full-page detail view replace the legacy eligibility-badge stack with one plain-language status plus a structured condition list covering all 16 eligibility reasons; actionable conditions open the relevant in-app policy, operation, security, or audit surface, or the corresponding configuration documentation when no editor exists. Maintenance-window deferrals are enriched into container-list and SSE status for local and agent-owned containers. Hard blockers disable the manual CTA, soft blockers retain the warn-and-confirm override, maturity/snooze conditions retain their lift time, and notifier-only mode collapses the eligibility detail behind an optional disclosure. Fresh installations default to manual; existing installations that predate the setting migrate to auto so an upgrade does not silently disable configured automatic updates.

  • Per-rule, per-trigger notification templates and bell preferences. The Notifications view can override simple title, simple body, and batch title independently for each notification rule and provider, render a live preview against representative event data, and save the overrides through the versioned notification API. The same rule editor now controls whether each supported event category appears in the in-app bell; update-available entries can be limited to major, minor-and-up, patch-and-up, or all updates. Existing trigger-level templates remain the fallback, so current delivery behavior is unchanged until an override is saved. (Discussion #205)

  • Zero-dependency custom dashboard grid. The dashboard no longer depends on grid-layout-plus. Its CSS Grid implementation packs layouts deterministically, supports edit-mode drag/reorder and bounded pointer resizing, preserves per-breakpoint layouts and hidden widgets, handles touch drag gestures, and keeps the existing reset/persistence behavior while fixing awkward same-row reorder interactions. (#281)

  • Declarative update policy with three-tier precedence. Docker containers can declare dd.updatePolicy.maturityMode, dd.updatePolicy.maturityMinAgeDays, dd.updatePolicy.skipTags, and dd.updatePolicy.skipDigests; Docker watchers can set maturity defaults with DD_WATCHER_<name>_MATURITY_MODE and _MATURITY_MIN_AGE_DAYS. Each field resolves watcher environment → container label → persistent UI/API override, with effective/declarative/override/source metadata exposed by the container API. The policy editor marks values that materially override their declaration and can revert one field or all fields without disturbing snoozeUntil. Overrides survive agent refreshes and container recreation, label removal remains authoritative beneath them, maturity blockers name their active source, and override set/clear operations are audited with every tier value. Motivated by Discussion #307 and tracked by #320.

  • Maturity stabilization countdown and override UX (Discussion #406). A candidate held by maturityMode: mature is visible immediately in container list, card, detail, and dashboard update surfaces instead of looking current or up to date. The maturity blocker shows a live minute-by-minute countdown plus the exact local date and time when the gate lifts. If a newer tag, digest, or mutable-tag image supersedes the waiting candidate, the soak clock and displayed ETA restart. Update Now remains available as an explicit soft-policy override, with a warning that names the maturity blocker; automatic enqueue paths continue to respect the gate. The displayed time is the eligibility/gate-lift time, not a guaranteed deployment time: automatic application still occurs on a subsequent watcher check and can be delayed by other blockers or maintenance windows.

  • Informational version visibility for pinned tags (#498). A specific-precision, unlabeled, non-loose pinned tag (e.g. nginx:1.25.3) still gets digest-only comparison for update actions — that pin-gate behavior from rc.2 is unchanged — but the container result now carries a new updateInsight: { tag, kind } field showing the best newer same-family tag that exists in the registry, purely as information. It reuses the exact same strict-style family matching used for actionable updates (prefix + suffix/variant compatibility + matching numeric-segment count + CalVer leading-zero rules) — no exception is carved out for major-version jumps; strict matching never restricted those to begin with. One narrow widening is scoped to this informational channel only: a prerelease-pinned tag (e.g. 1.5.2-rc.1) can see its own bare GA release (1.5.2) here, but that never makes the bare GA release an actionable update candidate — the actionable path rejects it just as before, even under dd.tag.family=loose or a permissive dd.tag.include filter. Ties at the same numeric version prefer the tag whose suffix template exactly matches the pinned tag's. This is additive only: updateAvailable, updateKind, and trigger dispatch are all unaffected. v1.6 adds the full policy chains — dd.tag.pin.info label → matching imgset → watcher TAG_PIN_INFOtrue, plus watcher-level TAG_FAMILY as the lowest configured actionable default beneath labels and imgsets (strict remains the built-in default) — and replaces the hover-only badge with an at-a-glance informational treatment: grey current tag → blue newer tag, a neutral Pinned state, and a Major/Minor/Patch chip across list, card, and detail surfaces. The v2.7.5-openvinov3.0.2 Major report in #498 was traced to an explicit dd.tag.family=loose label on that one Immich container, not the default pinned path; the historical bug that let loose mode cross from a suffixed variant to a bare tag is fixed and regression-covered, so variant comparisons remain variant-compatible (for example, -openvino stays -openvino).

  • Opt-in wud-card compatibility endpoints. The unversioned /api/* alias is removed in v1.6.0 (see the Removed section below and DEPRECATIONS.md), but the Home Assistant wud-card integration (and Homepage's native whatsupdocker widget, which shares the same contract) only speaks that unversioned, WUD-shaped surface. Setting DD_COMPAT_WUDCARD=true (default false) mounts a narrow, genuinely self-sufficient compatibility layer at /api/* — served by the compat router's own internal API router instance, not by falling through to the removed alias, so it keeps working now that alias is gone — covering exactly the four endpoints wud-card calls (GET /containers, GET /containers/:id/triggers, POST /containers/watch, POST /containers/:id/triggers/:triggerType/:triggerName). Three of those four are reshaped into WUD's bare-array response instead of drydock v1's { data, total, limit, offset, hasMore, _links } envelope; the trigger-run endpoint already returns a small non-enveloped body and passes through unmodified. Everything else under /api/* now returns 410 Gone. Off by default, subject to the same authentication and rate limiting as the rest of the API, and best-effort with no compatibility guarantee. (Discussion #469)

  • Experimental Portwing edge agents now actually serve container logs and deletes over the WS tunnel. EdgeAgentAdapter is wired into AgentClient's getContainerLogs()/deleteContainer() dispatch — a container hosted behind an edge agent (DD_EXPERIMENTAL_PORTWING=true) now routes log-tail and delete requests over the existing wss:// connection instead of falling through to a nonexistent HTTP endpoint on an edge-agent placeholder host. (#470)

  • Edge log/delete responses are correlated by echoed requestId, and the timestamps option is forwarded over the tunnel. A current Portwing agent echoes back the requestId drydock sends on dd:container_log_response / dd:container_delete_response, so EdgeAgentAdapter now resolves the exact originating request — correct even when two requests for the same container complete out of order — instead of the previous oldest-outstanding-by-containerId (FIFO) heuristic, which is retained only as a fallback for older agents that don't echo. Log downloads through an edge agent also honor the timestamps query parameter now (the wire message carries a timestamps field the agent reads), so the UI "show timestamps" toggle works over the edge path the same as for HTTP/SSE agents. Resolves the transport gaps previously documented as Bug 4 and punch-list #5.

  • Portwing WS connections are now proactively closed when an agent stops answering pings. The server sends a ping every 30s and expects a pong in return; an agent that misses two consecutive cycles (60s, matching portwing's own readDeadline) is treated as dead and the connection is force-closed, freeing the agent slot immediately instead of leaving a zombie connection registered until the underlying transport eventually notices. (#470)

  • Bidirectional MQTT: Home Assistant can now trigger updates back, not just observe them. Setting DD_NOTIFICATION_MQTT_{trigger_name}_HASS_COMMANDS=true adds a command_topic to each container's discovery payload, so the update entity's Install button in Home Assistant becomes clickable. Clicking it publishes to a per-container command topic that drydock subscribes to, which dispatches through the same update path the /update/:containerName webhook already uses, so all existing eligibility checks, trigger resolution, and agent routing apply unchanged. Commands are rate-limited per container (one accepted click every 30s) and every outcome (accepted, rejected, unexpected error) is recorded in the audit log under the new mqtt-command-update action. (#210)

  • Edge agents can choose their own display name via hello.agentName. Previously every edge agent was named portwing-edge-<agentId> unconditionally. A supplied name is now sanitized to a safe slug (lowercase, alphanumeric + hyphen, max 63 chars) and used as the registry/display name, falling back to the old portwing-edge-<agentId> form when absent or empty. The name is bound to the authenticating Ed25519 key on first use — a later hello reusing the same name is only admitted under the same key (rejected with an agent-name-claimed error otherwise), and the binding is released when its owning key is revoked — so one registered key can no longer squat or steal another agent's chosen name. The binding is persisted (a name-bindings store collection, reloaded into memory on startup) so this guarantee holds across a server restart too, not just for the lifetime of one process. (#470)

  • Opt-in Ed25519 request signing for standard-mode agents. A controller agent's config gains an authmode setting (token | ed25519, default token, so existing configs are unchanged). Setting DD_AGENT_{name}_AUTHMODE=ed25519 — plus DD_AGENT_{name}_SIGNINGKEYID and a PEM PKCS#8 DD_AGENT_{name}_SIGNINGKEY (or _SIGNINGKEY__FILE) — makes the controller sign every request to that agent with an Ed25519 key instead of sending the shared X-Dd-Agent-Secret. Each request carries four X-Portwing-* headers (Key-ID, Timestamp, Nonce, and a base64url Signature) over the canonical METHOD\nPATH\nSHA-256-hex(body)\ntimestamp\nnonce message, matching Portwing's verifier, so a captured signature can't be replayed and no long-lived secret crosses the wire. The signing key is parsed and validated once at startup (fail-fast on a malformed key), masked in configuration dumps like the secret token, and a missing key id/key skips just that agent with a warning rather than crashing the controller. Because an ed25519 agent transmits no secret, it connects over plain HTTP without DD_AGENT_ALLOW_INSECURE_SECRET. (#470)

  • Cross-device preference sync. An opt-in Sync across devices toggle in Config > Appearance (off by default, hidden for anonymous sessions) stores the full UI preference set — theme, layout, dashboard, language, and more — server-side per user via a new GET/PATCH /api/v1/preferences endpoint, and propagates changes to a user's other signed-in devices in real time over the existing SSE connection. Turning sync off keeps the server-side copy but stops syncing, and the device's own localStorage becomes authoritative again. (Discussion #220)

  • Health-status event notifications. A new container-unhealthy notification rule fires your existing notification triggers (Slack, SMTP, webhook, etc.) the moment a Docker health check enters the unhealthy state — detected on both the per-event Docker health_status listener (near-instant) and the 6-hour cron fallback, for a container that has previously been observed in a non-unhealthy state within the same instantiation. Never fires for a container with no HEALTHCHECK configured, for a container discovered already unhealthy with no prior baseline, or repeatedly while a container stays continuously unhealthy — but does fire again after a fast restart/crash-loop resets the container's boot timestamp, even if the health status reads unhealthy on both observations. Disabled by default, matching the agent-disconnect/agent-reconnect precedent; enable the container-unhealthy rule and assign triggers (or leave the trigger list empty to fire to every notification trigger) to receive it. No recovery/"healthy again" companion event, no dd.autoheal* label, and no corrective action ship with this — the full auto-heal loop stays a later, separately-scoped feature. (Discussion #198)

Changed

  • Registry requests now retry transient network failures. Response-less network errors — timeouts (ECONNABORTED/ETIMEDOUT), connection resets (ECONNRESET), and temporary DNS failures (EAI_AGAIN) — are retried up to twice with exponential backoff before a watch error is recorded, complementing the existing 429/503 Retry-After handling. A brief registry blip no longer marks affected containers as errored until the next hourly cycle.

  • Legacy trigger-taxonomy inputs now emit error-level migration signals in their final compatibility release. Every detected DD_TRIGGER_* variable and deprecated dd.trigger.include / dd.trigger.exclude label remains functional in v1.6, but is logged at error level and remains scheduled for removal in v1.7. Use DD_ACTION_* / DD_NOTIFICATION_* and category-scoped labels, or run config migrate --source trigger.

  • Registry polling and hot container summaries do less duplicate work. Tag-list requests for the same registry repository are now shared across containers within a poll, including concurrent lookups, without leaking mutable cached arrays to callers. Dashboard summary uses the lightweight stats projection, and security overview reads the collection without deep-cloning every container before building its aggregate response.

  • Scheduled security scans no longer flood History with unchanged container updates. Security results still refresh the container store, UI, and notification paths, but the generic container-update audit row is now emitted only when meaningful lifecycle, image, update, error, health, or policy state changes. Transition state uses the persisted compose-aware container identity, stays isolated across agents, watchers, and same-name Compose siblings, and is cleared when that exact container is removed.

  • The shared application log viewer stays responsive with large histories. JSON highlighting now uses the existing bounded tokenizer cache, and collections above 200 entries render through a measured, overscanned virtual window while preserving search navigation, wrapping, line numbers, auto-scroll, and live row-height changes.

  • CI dependency metadata is cleaner. The Playwright workflow comment now matches its actual Chromium-only matrix (PR #486), and the unused @types/node-cron package was removed from the backend workspace (PR #489). Both fixes are applied directly on dev/v1.6; the Renovate PRs remain open until default-branch convergence.

  • Base image bumped to Alpine 3.24. The healthcheck-build stage in the Dockerfile moves from alpine:3.21 to alpine:3.24.

  • Every list view toggles between table and cards, and the choice sticks per view. The v1.6 UI refactor rebuilt all list views on a shared DataTable, and each filter bar's view switch is now table⇄cards across Containers, Agents, Notifications, Security, Triggers, Watchers, Servers, Registries, Audit, and Auth. The selected mode is persisted per view; below ~640px the layout automatically reflows to cards and the now-redundant toggle is hidden. The old three-way list (accordion) mode has been removed — it's table or cards.

  • Container resource shortcuts are now consistent across views (Discussion #295). The existing source-project and release-note capabilities now render through one Source → Release notes → Registry toolbar in container table/cards/details, Security table/cards/detail, and Dashboard Recent Updates. The Containers table has a required Resources column separate from lifecycle Actions; cards and compact layouts wrap resources onto their own row. Every resource target is 44×44 px, and registry opens the filtered internal Registries view instead of a raw OCI API endpoint. Release-note dialogs stay inside the viewport, contain touch scrolling and keyboard focus, reset when the selected row changes, allow only one open dialog, and restore focus when dismissed with Escape or Close. The removed accordion list mode is not a current target.

  • dd.action.* and dd.notification.* trigger labels are now strictly category-scoped. A container's dd.action.include/dd.action.exclude labels now gate only action triggers (docker, dockercompose, command); dd.notification.include/dd.notification.exclude gate only notification triggers. Previously, whichever of the two was set on a container silently won for both categories, so a lone dd.action.include also filtered notification triggers (and vice versa) — see the Fixed entry below. The deprecated dd.trigger.include/dd.trigger.exclude labels still apply to both categories as a shared fallback beneath the scoped labels, unchanged. See the Upgrade Notes entry below before upgrading if you rely on a single scoped label to gate both trigger categories.

Deprecated

  • GET /api/auth/methods. This unversioned auth-discovery alias now logs on every request, returns Deprecation/Sunset headers, and points callers directly to canonical GET /api/v1/auth/status. It is documented in DEPRECATIONS.md — deprecated in v1.6.0, removed in v1.7.0.

  • Legacy auth strategies response shape (GET /auth/strategies). This endpoint's older { strategies, warnings } response shape is superseded by GET /api/v1/auth/status's { providers, errors } shape. It now logs on every request and returns Deprecation/Sunset headers; removal is scheduled for v1.8.0.

Removed

  • Legacy v1.4-era authentication compatibility. Basic authentication now accepts only argon2id hashes; {SHA}, APR1/MD5, crypt, and plain-text hashes fail validation. OIDC discovery now requires https://; HTTP discovery no longer enables an insecure client workaround.

  • Legacy WUD configuration runtime aliases. WUD_* environment variables and wud.* Docker labels are ignored. The migration CLI intentionally still recognizes them so existing files can be rewritten to DD_* and dd.* before startup.

  • Obsolete Docker watcher switches. DD_WATCHER_<name>_WATCHDIGEST and WATCHATSTART are no longer configuration keys. Use dd.watch.digest=true per container; startup watches are always scheduled.

  • Legacy trigger-template aliases. $id, $name, $watcher, $kind, $semver, $local, $remote, $link, and $count are no longer populated. Templates use the canonical container/update context and $containers.length.

  • Kafka clientId and token-only public-registry compatibility handling. Kafka validation accepts only lowercase clientid; malformed Hub/DHI public instances such as PUBLIC_TOKEN without PUBLIC_LOGIN now fail closed instead of silently falling back to anonymous pulls. Valid LOGIN+TOKEN, LOGIN+PASSWORD, and AUTH configurations remain supported.

  • Unversioned /api/* alias removed. Deprecated since v1.4.0, the unversioned /api/* prefix now returns 410 Gone with a JSON body pointing callers at the /api/v1/ equivalent instead of serving the request. This is a breaking change for the Home Assistant wud-card integration and Homepage's native whatsupdocker widget — both hardcode the unversioned /api/* base path with no way to configure a different one, so they start getting 410s the moment they hit drydock v1.6.0. Their supported path forward is DD_COMPAT_WUDCARD=true (default false), which mounts a narrow, self-sufficient compatibility layer serving exactly the four endpoints those integrations call (see the Added section above and DEPRECATIONS.md). Everyone else migrates to /api/v1/*. GET /api/auth/methods is not part of this removal — it's mounted directly on the app, ahead of the /api/* mounts, and stays on its own separate deprecation schedule (removed in v1.7.0).

  • Unversioned WebSocket alias WS /api/log/stream removed. Same removal as the REST /api/* alias above, applied to the log-stream upgrade: connecting to the unversioned path now fails fast with a 410-style upgrade rejection instead of completing the WebSocket handshake. Use the canonical WS /api/v1/log/stream endpoint.

Fixed

  • Recovering from a registry outage no longer corrupts a container's stored image identity. When a container's stored record carries a watch error, the next watcher cycle rebuilds it from scratch — and that rebuild blindly reset image.digest.value to the raw local image digest, discarding the registry-reconciled manifest digest recorded on the last successful cycle. The reset flipped the security scheduler's scan-group key, forcing a needless fresh vulnerability scan and emitting a spurious container-update audit row for every container sharing the image. Surfaced by the v1.6 24-hour store-size acceptance run, where ten such rows during a Docker Hub outage were exactly the audit-growth margin of failure (Discussion #321). The rebuild now preserves the reconciled digest value unless the local repo digest genuinely changed (a real re-pull), matching the clean-path refresh semantics.

  • User-set update-policy overrides survive error-state rebuilds. The same error-state rebuild stamped updatePolicyOverrides to {} on the rebuilt record, silently destroying stored snoozes and skipped tags/digests for any container that had a watch error. Overrides are now seeded from the existing stored record before declarative policy is re-applied.

  • Per-container update policy survives Drydock-triggered updates (#535). During an update, Drydock renames the outgoing container to a transient <name>-old-<timestamp> rollback alias; the Docker rename event wrote that alias onto the still-tracked container record (its display name too, when no custom dd.display.name label is set). The poisoned name then defeated the v1.5.2 policy-retention safeguard: post-update pruning could no longer match the record by name, and the rollback-alias guard skipped the policy hand-off, so the replacement container came up with no active update policy. The rename event now recognizes Drydock's own transient rollback alias — provable because stripping the alias suffix reconstructs the record's current name — and ignores it, while genuine renames (including containers legitimately named with an -old-<digits> suffix) propagate unchanged.

  • Failed container recreates now reclaim orphaned replacements before rollback. If creation succeeded but a later network-connect or start step failed, every update, self-update, health-monitor, backup-restore, and Compose rollback path now recovers and removes the partial replacement before restoring the original name. Containers already stranded under nested -old-<timestamp> names are blocked from another cascading recreate with a manual-cleanup error. Recreates also stop re-pinning daemon-assigned MAC addresses, while preserving explicitly configured primary-network MACs. Locally built images skip registry lookups, and unprefixed Docker Hub throttle warnings identify docker.io. This patch also ships on the consolidated v1.5.2 line. (PR #503)

  • Store growth is attributable and repeated update audits are bounded (Discussion #321). Diagnostic dumps now report UTF-8 serialized bytes for every LokiJS collection and the store total. Identical update-available reports are deduplicated by agent, watcher, container, and version transition for a configurable window (DD_AUDIT_UPDATE_AVAILABLE_DEDUPE_MS, one hour by default); target changes and no→yes transitions are recorded immediately.

  • Large-image Trivy scans no longer race their own timeout or discard the pulled candidate on scanner errors. The default DD_SECURITY_TRIVY_TIMEOUT is now 10 minutes, while Node gives Trivy an additional 30-second process grace so Trivy can report its own deadline instead of being killed as exit=unknown. The update gate retries one classified transient scanner failure, retains the pulled image when scanning itself errors, and still prunes images that are genuinely blocked by vulnerability policy. Local Trivy mode performs a serialized, single-flight --download-db-only warm-up outside the scan command's budget; server mode skips local warm-up. Scanner failures remain fail-closed. (#490)

  • The bundled Trivy binary now comes from an immutable official multi-architecture image digest. The release image, default Docker scanner worker, and release-cut SBOM job use the same pinned Trivy release instead of installing a floating Alpine edge package. Cosign remains version-pinned from Alpine 3.24; curl remains in v1.6 for compatibility with user-defined health checks and is scheduled for removal in v1.7.

  • Image builds no longer fail on a stale Alpine tzdata pin. Alpine 3.21's repositories replaced tzdata 2026b-r0 with 2026c-r0, which made every image build fail at apk add with an unsatisfiable exact pin. The base-image pin now installs the available 2026c-r0, and a regression test asserts the current revision is pinned (and the stale one absent) so a future rotation fails fast in CI instead of at release time. (PR #523; applied to main after the v1.5.2 cut as PR #533)

  • Registry and runtime hardening is consistent across provider-specific paths. Credential refresh, custom TLS settings, redirect handling, pagination cursors, and Docker Hub metadata requests now use the same bounded/fail-closed rules across supported registries. Secret-file loading, hook command policy, template property access, proxy-aware throttling, and API error responses were hardened in the same review pass.

  • Agent reconnect and security-digest state are bounded and lifecycle-safe. Removing an agent can no longer leave a reconnect queued, edge reconnect notifications reflect the real reconnect state, and digest notification buffers now expire and enforce configured limits across active and restored state.

  • A stalled auth bootstrap request no longer leaves the app blank indefinitely. The /auth/user request times out after eight seconds and falls through to the existing logged-out redirect path.

  • Old preference schemas no longer skip intermediate migrations. Schema-v3 data now advances through each numbered migration, so later additions such as the softwareVersion column are applied before reaching the current schema.

  • Open tabs recover from stale lazy-loaded chunks after an upgrade. Vite preload errors and matching Vue Router dynamic-import failures request one guarded page reload; successful navigation clears the session guard so a future upgrade can recover independently.

  • Repeated stale-chunk failures are no longer silently swallowed after the guarded recovery attempt is exhausted. The first matching preload failure still requests one session-guarded reload; later failures continue through Vite's normal error path so monitoring and the host page can surface them.

  • Notification-bell controls now match their audit-backed event coverage. container-unhealthy joins the bell query, rules without a corresponding bell audit action (currently agent-reconnect) no longer show controls that could not take effect, and health-only changes now propagate through local and agent lifecycle events. Unhealthy audit dedupe is scoped by agent + watcher + container, and a replayable health-transition SSE is ordered after audit processing so the bell refetch sees any newly inserted row. Severity thresholds remain scoped to update-available; notification-delivery failures remain always visible.

  • Digest-only updates remain visible under every notification-bell severity setting. Update audit entries now retain whether the change is tag- or digest-based, so major, minor, and patch settings no longer discard digest changes whose semantic-version severity is necessarily unknown. Unrelated unknown-severity tag entries remain filtered as before.

  • The live system-log viewer keeps advancing after its browser buffer fills. Rolling past the 2,000-entry client cap now replaces the reactive array instead of mutating it in place, so newest-first sorting and virtualization observe every rollover rather than freezing the visible newest row while the WebSocket continues receiving entries.

  • Virtualized log position is stable while reading older entries. Measured row-height changes and newest-first batch prepends compensate both the virtual window and DOM scroll position; a screen-reader-only status also reports rendered versus total lines without reintroducing thousands of DOM nodes.

  • dd.tag.family=loose no longer bypasses the suffix/variant guard in isSemverFamilyMatch(). A pinned nginx:1.2.3-ls132 container could previously be offered a bare 1.2.4 (wrong variant) as an update candidate under loose policy — loose mode was only ever meant to relax prefix equality and CalVer leading-zero rules, not let updates cross a suffix/variant boundary entirely. The guard now applies unconditionally regardless of policy.

  • The candidate sort in sortSemverDescending() now prefers the exact-suffix-template match over a merely-compatible one when two candidates tie at the same numeric version (e.g. preferring 1.2.5-alpine over 1.2.5-alpine3.21 for a 1.2.3-alpine reference). Semver treats the suffix as a prerelease field, so without this fix the wrong variant could outrank the exact match purely on prerelease-string ordering.

  • Pinned semver tags are now compared by digest by default, as originally announced in v1.5.0-rc.36. A rebuilt image republished under the same tag is detected again. Previously digest watching was silently disabled for fully-pinned tags (e.g. nginx:1.25.3), leaving them with no update detection at all and a misleading "compared by digest only" notice on every pinned container. Version climbing for pinned tags remains opt-in via dd.tag.include or dd.tag.family=loose. In agent deployments, agents perform the registry checks — update agents alongside the controller to restore digest detection for agent-watched containers. (#498)

  • The "no update detection" notice is honest when digest watching is explicitly disabled. When dd.watch.digest=false (or an imgset watch.digest=false) is set on a pinned or floating tag, the notice no longer claims a digest comparison is happening. (#498)

  • Removed the unreachable flat tagFamily imgset config key. Env-derived config keys are lowercased, so the camelCase key could never match any real configuration; the documented DD_WATCHER_{watcher}_IMGSET_{name}_TAG_FAMILY form is unaffected. (#498)

  • GET /api/v1/containers/backups (list all backups) is now reachable. The route was previously registered as router.get('/', getBackups) in the backup router, which was mounted at /containers after the container router — so the container router's own GET / handler shadowed it and the list-backups endpoint was never reachable. The route is now router.get('/backups', getBackups) and the backup router is mounted before the container router, making GET /api/v1/containers/backups accessible. The per-container backup endpoints (GET /api/v1/containers/:id/backups) were not affected.

  • App favicon now matches the refreshed website branding. The v1.5.1 brand refresh (#439) updated the website to the cropped whale "headshot" icon but left the in-app tab icon, Apple touch icon, and PWA manifest icons on the old full-body whale. The app now ships the same icon set as the website. The stale favicon.svg — which modern browsers preferred over the PNGs, so it kept showing the old mark — was removed, and the icon links carry a ?v=2 cache-buster so existing installs re-fetch instead of serving the aggressively cached old icon. (#439)

  • Deprecation lifecycle, warnings, banners, and docs now match the v1.6 runtime. The audit removed retired OIDC/hash banners, narrowed the consolidated UI banner to active v1.7 trigger-prefix inputs, corrected stale CORS/stats dates, documented the permanent auth-status alias, deferred PUT /api/v1/settings to API v2, fixed the auth migration target and Sunset metadata, added request-level signals to the legacy auth-strategies response, and moved every completed v1.6 item out of the active schedule. The published docs now distinguish removed runtime aliases from the migration CLI, which intentionally retains knowledge of old names solely to rewrite files.

  • A stale cached page no longer white-screens after an upgrade. The UI's SPA history-mode fallback used to return index.html (an HTML 200) for any unmatched path, including a request for a content-hashed /assets/*.js bundle that a previous version referenced but the upgrade deleted. Browsers refuse to run a module script served as text/html, so a page cached from the old build (typically by a reverse proxy that ignores the HTML's Cache-Control: no-store) painted nothing, and caching proxies could poison the asset URL with that HTML. Requests under /assets/ that miss now return a clean 404 (with no-store) instead of the shell, so a stale page fails fast rather than blank. A new Reverse proxy caching docs section covers the browser/proxy cache settings that avoid this. This does not require a stale entry-script to recover on its own — clearing the browser/proxy cache is still the fix for an already-blank page — but it stops drydock from turning a missing bundle into a silent white screen. (#466)

  • Responsive containers table: sticky columns and auto-hide column budget corrected. The auto-hide column budget now measures the table's real available width (ResizeObserver + content-box measurement) instead of an estimate that ran ~23px too generous whenever the detail panel was open, so columns no longer hide too aggressively or too little. The icon and name columns are now pinned together as a single sticky-left cluster so the icon can no longer scroll out from under the name column, the sticky separator border only appears on genuine horizontal overflow, and the sticky actions column no longer paints on top of the last data column when a table legitimately overflows.

  • Icon column no longer clips container icons. The icon column was 40px wide with 20px of padding, leaving only ~20px of content box for the 32px container icon — about 11.9px of every icon silently hung past the cell edge. The column is now sized (40→56px) to actually fit the icon.

  • Edge agent memoryGb was reported in decimal GB, not GiB. EdgeAgentAdapter divided the reported byte count by 1e9; drydock's own convention (and portwing's canonical MemoryTotalGB()) is binary GiB (1024³). An 8 GiB host previously showed as ~8.59 in the UI and API; it now reports the correct ~8.00.

  • Portwing drydockCompat mismatches now warn in both directions. The server previously only logged a warning when a connecting agent's compat major version was newer than the server's; an older agent connecting to a newer server produced no signal at all. Any major-version mismatch, in either direction, now logs a warning pointing operators at the compat matrix — the connection is still accepted either way.

  • Malformed hello.agentName on the portwing edge WS could crash the drydock process. The hello handler called .trim() on hello.agentName with no type check; a number, boolean, array, or object in that field threw a TypeError that surfaced as an unhandled promise rejection capable of taking down the whole process. The field is now validated for type and length before use — a malformed value closes just that connection with an invalid-agent-name error frame instead.

  • A reconnecting edge agent could be evicted by its own stale connection. The server-side ping-liveness check's forced close ran the same disconnect cleanup as a real WebSocket close event, but the underlying close/error listeners were never detached — so when the transport eventually noticed the connection was already gone, the disconnect cleanup ran a second time. Since agent removal matched by name only, that delayed second cleanup could evict a different, newly-reconnected agent sharing the same identity-derived name. Disconnect handling is now idempotent, listeners are detached before a forced close, and agent removal is instance-checked (only fires if the registry still holds that same connection instance under the name).

  • Concurrent log or delete requests for the same container over the edge tunnel no longer clobber each other. Pending edge requests were keyed by container id alone, so a second in-flight request for the same container overwrote the first's entry; the first request's timeout then deleted the second's entry, silently dropping the real response and leaving the caller with a spurious timeout. Requests now carry a unique per-call id embedded in both the pending-request key and the outgoing wire frame, correlated on response via a per-container FIFO queue.

  • dd.action.include/dd.action.exclude no longer leak into notification trigger filtering (and vice versa). Label resolution collapsed dd.action.* and dd.notification.* into a single container.triggerInclude/triggerExclude field at parse time, first-match-wins — so a container with only dd.action.include set had that value applied to every trigger regardless of category, silently gating notification triggers the label was never meant to touch. The two categories now resolve into independent actionTriggerInclude/actionTriggerExclude/notificationTriggerInclude/notificationTriggerExclude fields, and trigger matching reads the field scoped to its own category. The deprecated triggerInclude/triggerExclude fields are still populated (for /api/v1 readers, the persisted store, and mixed-version agents) but are no longer read by matching code. (#494, discussion #493)

  • HASS latest_version template no longer reports "Unknown" when no update is pending. The Home Assistant MQTT discovery latest_version template rendered an empty string for any container with no pending update (no result in the flattened state payload), which HA silently discards — and because HA blanks the whole update.* entity state when either version is missing, the entity read "Unknown" forever instead of resolving to "up to date". The template now falls back to the installed tag (image_tag_value) in both the digest and tag branches, and guards the digest slice so a digest-kind report carrying no digest no longer trips HA's Undefined[:15] slice error. (#491)

  • Home Assistant MQTT discovery entities now respect the same per-container trigger gating as state publishes. Discovery entity creation used to run unconditionally off container-added/updated events, independent of the mustTrigger() gating (rollback/agent scoping and the per-category dd.notification.include/dd.notification.exclude filters) that already governed the per-container state publish — so a container excluded from an mqtt trigger still got a Home Assistant entity whose state topic never received a message, leaving a permanent "Unknown" ghost entity in HA. Entity creation, state publishes, and #210 Install commands now all honor the same gate: a previously-created entity for a container that becomes excluded is cleaned up (its discovery config removed) the first time the container is seen, and an Install command addressed to an excluded container is ignored. (#491)

  • Container update policy is no longer lost when a container is recreated (#496). Container documents are keyed by Docker's container ID, which changes every time a container is recreated (image pull, docker compose up -d, or an update trigger firing). The replacement was stored as a brand-new document and the per-container update policy — maturity gate, skipped tags/digests, snooze — was silently dropped along with the old one. Because an absent policy means "no gating" rather than "default gating", affected containers then updated immediately instead of respecting their maturity soak. The policy now survives a recreate, for containers watched locally and through a remote agent alike. Present since 1.4.1, when per-container maturity policies were introduced.

  • The remote-agent prune path now distinguishes a recreated container from a removed one, so a container that reappears under a new ID keeps its update policy and its Home Assistant state topic, while a genuinely deleted container still has its discovery topics cleaned up.

Upgrade Notes

  • Existing installs preserve automatic updates; fresh installs start in manual mode. The new global updateMode setting defaults to manual only when Drydock creates a settings record for the first time. An existing settings record with no updateMode is migrated to auto, preserving the pre-v1.6 behavior in which configured action triggers could apply updates automatically. Review Settings → General → Update mode after upgrading if you prefer notifier-only or manual-only operation.

  • A lone dd.action.include/dd.action.exclude (or dd.notification.include/dd.notification.exclude) label stops filtering the other trigger category. Before this release, setting only dd.action.include on a container also filtered notification triggers as a side effect of the two labels collapsing into one internal field (#494). As of v1.6, each label filters only its own category. If you relied on the cross-category leak, previously-suppressed notification (or action) triggers may fire once on the first scan after upgrading. drydock logs a one-time warning per affected container naming the missing label; set the matching dd.notification.include/dd.notification.exclude (or dd.action.include/dd.action.exclude) to the same value to restore the previous filtering.

Documentation

  • Podman and Docker socket security docs refreshed. Added the v1.5.2 guidance for Podman's Docker-compatible API path (#152), clarified direct socket vs proxy/TCP behavior, documented remote TLS/OIDC watcher auth, and tightened the Docker socket security/FAQ/security-guide cross-links without claiming native Podman support has shipped.

  • Documentation parity audits completed. Reconciled the current configuration/env-var reference tables against runtime schemas and refreshed the API/OpenAPI/docs contract for the canonical /api/v1 surface. The API docs now cover the recently added authentication component endpoints, registry webhook signature auth, notification outbox actions, bulk container updates, backup listing, auth status aliases, and container summary/update-response details; the OpenAPI spec now matches the route security modes and response codes for those surfaces.