v1.6.0-rc.1
Pre-releasev1.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=trueare now marked in trigger cards, container actions, and the Update Status panel; the action is labeled Preview only, prevented replacements log atWARN, and History recordsupdate-applied-dryruninstead 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=truekeeps the configured absolute severity threshold, but permits an otherwise-blocked candidate when itsCRITICAL,HIGH,MEDIUM,LOW, andUNKNOWNcounts 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.Ncuts now publishX.Y-rcalongside the immutable exact tag on GHCR, Docker Hub, and Quay. StableX.Y,X, andlatesttags 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/deduplicatedbothmode across the compatibilitycommandbackend, hardened digest-pinned ephemeral Docker workers, and a Trivy-serverremotecomposition that still models its client leg honestly. Scanner availability defaults fail-closed, with an explicit auditedwarnopt-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/sbomblobs; 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:
notifydetects and notifies but refuses every Drydock-managed update,manualallows UI/API updates but suppresses automatic action-trigger dispatch, andautoallows 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 tomanual; existing installations that predate the setting migrate toautoso 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, anddd.updatePolicy.skipDigests; Docker watchers can set maturity defaults withDD_WATCHER_<name>_MATURITY_MODEand_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 disturbingsnoozeUntil. 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: matureis 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 newupdateInsight: { 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 underdd.tag.family=looseor a permissivedd.tag.includefilter. 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.infolabel → matching imgset → watcherTAG_PIN_INFO→true, plus watcher-levelTAG_FAMILYas the lowest configured actionable default beneath labels and imgsets (strictremains 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. Thev2.7.5-openvino→v3.0.2Major report in #498 was traced to an explicitdd.tag.family=looselabel 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,-openvinostays-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 nativewhatsupdockerwidget, which shares the same contract) only speaks that unversioned, WUD-shaped surface. SettingDD_COMPAT_WUDCARD=true(defaultfalse) 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 returns410 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.
EdgeAgentAdapteris wired intoAgentClient'sgetContainerLogs()/deleteContainer()dispatch — a container hosted behind an edge agent (DD_EXPERIMENTAL_PORTWING=true) now routes log-tail and delete requests over the existingwss://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 thetimestampsoption is forwarded over the tunnel. A current Portwing agent echoes back therequestIddrydock sends ondd:container_log_response/dd:container_delete_response, soEdgeAgentAdapternow 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 thetimestampsquery parameter now (the wire message carries atimestampsfield 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=trueadds acommand_topicto 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/:containerNamewebhook 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 newmqtt-command-updateaction. (#210) -
Edge agents can choose their own display name via
hello.agentName. Previously every edge agent was namedportwing-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 oldportwing-edge-<agentId>form when absent or empty. The name is bound to the authenticating Ed25519 key on first use — a laterhelloreusing the same name is only admitted under the same key (rejected with anagent-name-claimederror 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 (aname-bindingsstore 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
authmodesetting (token|ed25519, defaulttoken, so existing configs are unchanged). SettingDD_AGENT_{name}_AUTHMODE=ed25519— plusDD_AGENT_{name}_SIGNINGKEYIDand a PEM PKCS#8DD_AGENT_{name}_SIGNINGKEY(or_SIGNINGKEY__FILE) — makes the controller sign every request to that agent with an Ed25519 key instead of sending the sharedX-Dd-Agent-Secret. Each request carries fourX-Portwing-*headers (Key-ID,Timestamp,Nonce, and a base64urlSignature) over the canonicalMETHOD\nPATH\nSHA-256-hex(body)\ntimestamp\nnoncemessage, 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 aned25519agent transmits no secret, it connects over plain HTTP withoutDD_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/preferencesendpoint, 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-unhealthynotification rule fires your existing notification triggers (Slack, SMTP, webhook, etc.) the moment a Docker health check enters theunhealthystate — detected on both the per-event Dockerhealth_statuslistener (near-instant) and the 6-hour cron fallback, for a container that has previously been observed in a non-unhealthystate within the same instantiation. Never fires for a container with noHEALTHCHECKconfigured, 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 readsunhealthyon both observations. Disabled by default, matching theagent-disconnect/agent-reconnectprecedent; enable thecontainer-unhealthyrule and assign triggers (or leave the trigger list empty to fire to every notification trigger) to receive it. No recovery/"healthy again" companion event, nodd.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 existing429/503Retry-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 deprecateddd.trigger.include/dd.trigger.excludelabel remains functional in v1.6, but is logged aterrorlevel and remains scheduled for removal in v1.7. UseDD_ACTION_*/DD_NOTIFICATION_*and category-scoped labels, or runconfig 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-updateaudit 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-cronpackage was removed from the backend workspace (PR #489). Both fixes are applied directly ondev/v1.6; the Renovate PRs remain open until default-branch convergence. -
Base image bumped to Alpine 3.24. The
healthcheck-buildstage in theDockerfilemoves fromalpine:3.21toalpine: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-waylist(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
listmode is not a current target. -
dd.action.*anddd.notification.*trigger labels are now strictly category-scoped. A container'sdd.action.include/dd.action.excludelabels now gate only action triggers (docker,dockercompose,command);dd.notification.include/dd.notification.excludegate only notification triggers. Previously, whichever of the two was set on a container silently won for both categories, so a lonedd.action.includealso filtered notification triggers (and vice versa) — see the Fixed entry below. The deprecateddd.trigger.include/dd.trigger.excludelabels 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, returnsDeprecation/Sunsetheaders, and points callers directly to canonicalGET /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 byGET /api/v1/auth/status's{ providers, errors }shape. It now logs on every request and returnsDeprecation/Sunsetheaders; 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 requireshttps://; HTTP discovery no longer enables an insecure client workaround. -
Legacy WUD configuration runtime aliases.
WUD_*environment variables andwud.*Docker labels are ignored. The migration CLI intentionally still recognizes them so existing files can be rewritten toDD_*anddd.*before startup. -
Obsolete Docker watcher switches.
DD_WATCHER_<name>_WATCHDIGESTandWATCHATSTARTare no longer configuration keys. Usedd.watch.digest=trueper container; startup watches are always scheduled. -
Legacy trigger-template aliases.
$id,$name,$watcher,$kind,$semver,$local,$remote,$link, and$countare no longer populated. Templates use the canonical container/update context and$containers.length. -
Kafka
clientIdand token-only public-registry compatibility handling. Kafka validation accepts only lowercaseclientid; malformed Hub/DHI public instances such asPUBLIC_TOKENwithoutPUBLIC_LOGINnow fail closed instead of silently falling back to anonymous pulls. ValidLOGIN+TOKEN,LOGIN+PASSWORD, andAUTHconfigurations remain supported. -
Unversioned
/api/*alias removed. Deprecated since v1.4.0, the unversioned/api/*prefix now returns410 Gonewith 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 nativewhatsupdockerwidget — 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 isDD_COMPAT_WUDCARD=true(defaultfalse), 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/methodsis 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/streamremoved. 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 canonicalWS /api/v1/log/streamendpoint.
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.valueto 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 spuriouscontainer-updateaudit 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
updatePolicyOverridesto{}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 customdd.display.namelabel 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 identifydocker.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-availablereports 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_TIMEOUTis now 10 minutes, while Node gives Trivy an additional 30-second process grace so Trivy can report its own deadline instead of being killed asexit=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-onlywarm-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;
curlremains 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
tzdatapin. Alpine 3.21's repositories replacedtzdata2026b-r0with2026c-r0, which made every image build fail atapk addwith an unsatisfiable exact pin. The base-image pin now installs the available2026c-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 tomainafter 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/userrequest 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
softwareVersioncolumn 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-unhealthyjoins the bell query, rules without a corresponding bell audit action (currentlyagent-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 toupdate-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, andpatchsettings 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=looseno longer bypasses the suffix/variant guard inisSemverFamilyMatch(). A pinnednginx:1.2.3-ls132container could previously be offered a bare1.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. preferring1.2.5-alpineover1.2.5-alpine3.21for a1.2.3-alpinereference). 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 viadd.tag.includeordd.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 imgsetwatch.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
tagFamilyimgset config key. Env-derived config keys are lowercased, so the camelCase key could never match any real configuration; the documentedDD_WATCHER_{watcher}_IMGSET_{name}_TAG_FAMILYform is unaffected. (#498) -
GET /api/v1/containers/backups(list all backups) is now reachable. The route was previously registered asrouter.get('/', getBackups)in the backup router, which was mounted at/containersafter the container router — so the container router's ownGET /handler shadowed it and the list-backups endpoint was never reachable. The route is nowrouter.get('/backups', getBackups)and the backup router is mounted before the container router, makingGET /api/v1/containers/backupsaccessible. 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=2cache-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/settingsto API v2, fixed the auth migration target andSunsetmetadata, 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 HTML200) for any unmatched path, including a request for a content-hashed/assets/*.jsbundle that a previous version referenced but the upgrade deleted. Browsers refuse to run a module script served astext/html, so a page cached from the old build (typically by a reverse proxy that ignores the HTML'sCache-Control: no-store) painted nothing, and caching proxies could poison the asset URL with that HTML. Requests under/assets/that miss now return a clean404(withno-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
memoryGbwas reported in decimal GB, not GiB.EdgeAgentAdapterdivided the reported byte count by1e9; drydock's own convention (and portwing's canonicalMemoryTotalGB()) 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
drydockCompatmismatches 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.agentNameon the portwing edge WS could crash the drydock process. The hello handler called.trim()onhello.agentNamewith no type check; a number, boolean, array, or object in that field threw aTypeErrorthat 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 aninvalid-agent-nameerror 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
closeevent, but the underlyingclose/errorlisteners 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.excludeno longer leak into notification trigger filtering (and vice versa). Label resolution collapseddd.action.*anddd.notification.*into a singlecontainer.triggerInclude/triggerExcludefield at parse time, first-match-wins — so a container with onlydd.action.includeset 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 independentactionTriggerInclude/actionTriggerExclude/notificationTriggerInclude/notificationTriggerExcludefields, and trigger matching reads the field scoped to its own category. The deprecatedtriggerInclude/triggerExcludefields are still populated (for/api/v1readers, the persisted store, and mixed-version agents) but are no longer read by matching code. (#494, discussion #493) -
HASS
latest_versiontemplate no longer reports "Unknown" when no update is pending. The Home Assistant MQTT discoverylatest_versiontemplate rendered an empty string for any container with no pending update (noresultin the flattened state payload), which HA silently discards — and because HA blanks the wholeupdate.*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'sUndefined[: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-categorydd.notification.include/dd.notification.excludefilters) 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 since1.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
updateModesetting defaults tomanualonly when Drydock creates a settings record for the first time. An existing settings record with noupdateModeis migrated toauto, 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(ordd.notification.include/dd.notification.exclude) label stops filtering the other trigger category. Before this release, setting onlydd.action.includeon 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 matchingdd.notification.include/dd.notification.exclude(ordd.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/v1surface. 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.