v1.7.0-rc.1
Pre-release
Pre-release
v1.7.0-rc.1
Full Changelog: v1.6.0...v1.7.0-rc.1
[1.7.0-rc.1] — 2026-08-14
Added
- Edge Portwing polling cadence is configurable (#688).
DD_PORTWING_POLL_INTERVALsets the controller-owned Edge container-refresh interval in positive integer seconds; the authenticated welcome frame and reported agent metadata use the same value, while absent or invalid values retain the 300-second default. - Installable PWA support (Roadmap Phase 6.9). Drydock is now an installable Progressive Web App via
vite-plugin-pwa: a web app manifest (Drydock, standalone display, theme/background color matched to the One Dark default--dd-bg, 192/512 icons plus dedicated maskable variants with safe-zone padding) and an auto-updating service worker (registerType: 'autoUpdate') that precaches the SPA shell so the dashboard boots offline./api/**is explicitly excluded from all service-worker handling — no navigation fallback, no runtime caching — so a live dashboard never serves stale API data from cache; those requests always hit the network and surface a normal error if it's unreachable. A dismissible install banner (newInstallBannercomponent, following the existingAnnouncementBannerpattern) listens for the browser'sbeforeinstallpromptevent and offers a one-click install, with the dismissal persisted under a versioned localStorage key. iOS home-screen install is supported viaapple-mobile-web-app-capableand the existingapple-touch-icon. The backend's static UI server now servessw.jswithCache-Control: no-cacheso a new deploy is never masked by a browser-cached service worker script. - Clickable port links in the container list and detail views. Each host-published port in a container's
details.portsnow renders as a link (opened in a new tab,rel="noopener noreferrer") instead of inert text — in the side panel, the full-page detail tabs, and new opt-in "Ports" columns/rows in the table and card views. The scheme is auto-detected from the container-side port (443/8443→https://, everything else →http://); the link target host prefers the port's own boundHostIpwhen it's a real address (not0.0.0.0/::/::0), falling back to the agent's configured host for agent-watched containers, or the browser's own hostname otherwise. Internal-only (unpublished) ports still render as plain text. A newdd.port.labelcontainer label lets you attach a friendly name to a specific port (dd.port.label=80=Web UI,443=Admin Console) shown in place of the rawhostPort->containerPort/protocolmapping. - Container uptime, with a live-refreshing display. The existing
details.startedAtfield (from Docker'sState.StartedAt) now also drives an opt-in "Uptime" tooltip showing the exact start timestamp in the container list, and a live "Up …" indicator in the card view's footer — both refresh on a timer and update immediately on SSE container-state changes, matching the full-page detail view's existing uptime display. - Keyboard shortcuts.
/focuses the search bar from anywhere (unless focus is already in a text input),Escapecloses the search bar, and?opens a new shortcut-reference overlay listing the available shortcuts. A/hint now sits next to the existing⌘Khint on the sidebar search button. - Container dependency ordering — data model and detection (v1.7, discussion #219). New
dd.depends_on(comma-separated container names) anddd.depends_on.action(updateorrestart, defaultupdate) container labels. Whendd.depends_onis absent, drydock detects dependencies automatically from a compose-managed container's owndepends_onkey (both short-form arrays and long-form objects; thecondition:key is not yet consulted). A present label always overrides compose detection entirely rather than merging with it. Self-references and unknown compose-service targets are dropped with a logged warning, never a hard error; both fields are re-derived from the container's labels/compose file every watch cycle rather than persisted independently, so they self-heal automatically across container recreation. This lands the data model and detection only — using the resulting graph to order updates/restarts is a separate, later change. - Container dependency ordering — pure graph engine (v1.7, discussion #219). New
app/dependencies/dependency-graph.ts:buildDependencyGraphresolves each container's detecteddependsOnnames against the rest of the fleet (dropping unknown targets and cross-agent edges with a logged warning, never a hard error — cross-host dependency chains remain out of scope for v1.7), andtopologicalSortruns Kahn's algorithm to produce deterministic topological "waves" — arrays of containers safe to dispatch in parallel, tie-broken alphabetically for stable output. A dependency cycle is never a deadlock: cycle members are grouped and scheduled together as one unordered wave, while any non-cycle container downstream of that cycle still resolves correctly in its own later wave. Pure, dependency-free, and unwired — no watcher, trigger, or dispatch behavior changes yet; this only lands the engine that a later change will use to order updates. - Container dependency ordering — execution integration (v1.7, discussion #219). Accepted bulk container updates now dispatch wave-by-wave through
runAcceptedContainerUpdates(app/updates/request-update.ts) instead of all at once, so a container never starts updating before every container it depends on has finished. AdependsOnAction: 'restart'dependent is admitted through the same admission gates as any manual request (widenedupdateAvailablecheck) and, once its dependency finishes updating, is restarted rather than re-pulled via the newrestartDependentContainerprimitive (app/updates/dependency-restart.ts); operations that never got a chance to run because an earlier wave failed land in a newskipped-dependencystatus/phase instead of silently vanishing. The Docker Compose trigger (app/triggers/providers/dockercompose/Dockercompose.ts) reorders multi-servicedocker compose upinvocations by dependency order (sortMappingsByDependencyOrder) so compose itself never fights the same ordering. Maintenance-window batches (app/triggers/providers/Trigger.ts'srunAcceptedUpdateBatch) cascade dependents through the same wave logic once their window opens. - Container dependency ordering — API exposure (v1.7, discussion #219). The container list response gains per-container
dependencyCount/dependentCountbadge counts. New endpoints:GET /api/v1/containers/dependenciesreturns the full resolved dependency graph (nodes, edges, detected cycles, unresolved targets, cross-host-ignored edges);POST /api/v1/containers/:id/update-chain-previewdry-runs the topological waves for the dependency chain rooted at a container without dispatching anything;POST /api/v1/dependency-groups/:rootId/updatebulk-accepts every container in that chain, annotated with the wave index it will actually run in. The preview and dispatch endpoints call the exact samebuildDependencyGraph/topologicalSortpair over the same input set, so the preview can never drift from what an accepted update actually runs. - Container dependency ordering — UI (v1.7, discussion #219). The container list now carries
dependencyCount/dependentCountthrough to the UIContainertype. A new "Update dependency chain" action (confirmDependencyGroupUpdateinuseContainerActions) previews the resolved update waves for a container's dependency chain and shows them in a confirm dialog — including any detected cycle or unresolved-target warnings — before bulk-accepting the chain through the new dependency-groups endpoint. A dedicated dependency-hierarchy grouping view for the container list is deferred to a follow-up. - Debounced container discovery (#156). Docker briefly exposes transient rename aliases while a container is being recreated; drydock previously registered whatever it saw the instant
listContainersreturned it, so a container could momentarily register under its<hex-prefix>_<name>alias. First-seen containers (identified by Docker container ID, not present in the store) now enter a configurable "pending" state and must remain visible for a settling window —DD_WATCHER_{watcher_name}_DISCOVERY_SETTLE_MS, default30000(30s),0disables settling — before they're added to the store, triggers, or the API/UI; pending containers are visible in debug logs only. A deduplicated follow-up watch is scheduled for the earliest pending deadline, so an event-discovered container still registers on time when no further Docker event arrives before the next cron scan. If a pending container is renamed mid-window it registers under the final name once settled; if it disappears mid-window it's silently discarded (debug log only). Containers already known to the store are unaffected and continue to update immediately — settling applies exclusively to first-seen containers, so a same-ID recreation is never blocked from updating for 30 seconds. This complements, and does not replace, the unconditional hex-prefix alias stripping shipped in v1.5 for the same issue (getContainerName/canonicalizeContainerNameinapp/watchers/providers/docker/docker-helpers.ts, and the name-shape-triggered transient-alias suppression infilterRecreatedContainerAliases) — that mechanism is keyed off the container's name looking like a recreate alias, while the new settling window (filterPendingDiscoveriesinapp/watchers/providers/docker/container-init.ts) gates any first-seen container regardless of name shape.
Changed
- Dependency-graph engine: iterative cycle detection, single-pass wave resolution, and indexed candidate lookup (v1.7, discussion #219).
stronglyConnectedComponents(app/dependencies/dependency-graph.ts) no longer recurses — an explicit work-stack replaces the recursivestrongConnect— so a singledependsOnchain or cycle around 5-10k+ containers no longer risks a stack-overflowRangeError; a fleet's dependency graph has no size bound a recursive implementation could safely assume.topologicalSort's cycle-resolution loop no longer rebuilds the remaining subgraph and recomputes strongly-connected components from scratch every round (O(N²) on fleets with several chained/dependent cycles) — a single upfront SCC computation plus one forward pass over the resulting condensation now produces byte-identical wave/cycle output in O(V+E).buildDependencyGraph's label and compose candidate lookups (findLabelCandidates/findComposeCandidates) are now backed by a once-per-call watcher/name and compose-project/service index instead of a full container-list scan perdependsOnentry.
Removed
- BREAKING: The legacy
DD_TRIGGER_*environment variable prefix anddd.trigger.include/dd.trigger.excludecontainer labels are removed (deprecated v1.5.0, warned aterrorlevel throughout v1.6.0, removed per the published v1.7.0 schedule inDEPRECATIONS.md). AnyDD_TRIGGER_*environment variable now fails startup outright — the error lists every detected variable next to its exactDD_ACTION_*(docker/dockercompose/command) orDD_NOTIFICATION_*(every other provider) replacement, plus theconfig migrate --source triggercommand and a link to the deprecations page, so a config can be fixed in one pass.dd.trigger.include/dd.trigger.excludecontainer labels no longer resolve to anything — only the scopeddd.action.*/dd.notification.*labels are read; a container still carrying either legacy label logs a one-timeerror-level warning and keeps incrementing thedd_legacy_input_total{source="label"}counter (surfaced in the existing deprecation banner) so unmigrated fleets stay visible even though the label is no longer honored. TheusesLegacyPrefixtrigger-metadata field, the now-always-empty legacy-prefix tracking inapp/configuration/index.ts, and the superseded startup warning are removed along with it. Thedrydock config migrate --source triggerCLI is unaffected — it's a standalone, offline config-file rewriter and remains the recommended migration path.
Security
- The 2026-08-13 security pass closes six resource and credential-exposure gaps. Login admission now caps concurrent password verification before Argon2 runs; standard agent JSON requests have time, body, response, and redirect bounds; unterminated agent SSE events have a finite buffer; container log downloads and initial WebSocket history have finite line/byte limits; slow local log viewers are disconnected; registry data requests refuse redirects; and command/hook strings are redacted from component APIs and execution logs. The dated findings, evidence, and validation record are in
security_best_practices_report.md. - Added a root
.trivyignore.yamlsuppressing AVD-DS-0002 (Dockerfile missingUSER) with the same rationale already documented for the Dockerfile'scheckov:skip=CKV_DOCKER_3comment and the existing qltytrivy:DS-0002triage rule: the entrypoint drops privileges at runtime viasu-exec(Docker.entrypoint.sh), so no staticUSERinstruction is needed. - Service-worker
NetworkOnlyrule for/api/**now actually matches.ui/vite.config.ts'sruntimeCachingentry used a^-anchored pathname regex (/^\/api\//), but workbox-routing tests aRegExpRoute'surlPatternagainst the fullurl.href(always startinghttp:///https://), never the pathname alone, so the rule could never match and silently fell through. It was harmless today only because no otherruntimeCachingrule exists to catch the fallthrough — any future catch-all caching rule would have started silently caching authenticated/apiresponses. Replaced with an exportedisApiRequestmatch-callback function that testsurl.pathname.startsWith('/api/'), so the rule actually engages. - Dependency-group bulk update now requires destructive-action confirmation and binds to its own preview (v1.7, discussion #219).
POST /api/v1/dependency-groups/:rootId/updatecould update or restart every container in a resolved dependency chain with no confirmation step and no binding to whatever chain the UI last previewed — a container added to the chain between preview and confirm was silently swept into the update. The route now requires theX-DD-Confirm-Action: dependency-group-updateheader, matching the existingcontainer-deletepattern, and accepts an optionalexpectedContainerIdsarray in the request body; when present, a live chain that no longer matches it exactly (order-insensitive) is rejected with 409 and the actual current chain, instead of running against a chain the caller never saw.
Fixed
- Edge exec completion reasons now reach the controller-side consumer (#635). String
exec_end.reasonvalues are forwarded through the internalstartExecend callback; sessions are removed before consumer code runs, and a throwing callback cannot leak state or stop disconnect cleanup. - Controller-owned Portwing containers now use the controller's configured registry identity before native checks (#687). Complete Docker-transport inventory and event records are normalized to the canonical registry name, URL, credentials, and image identity, so watch-now no longer delegates registry work to Portwing's intentional 501 stub; traditional agents and partial events keep their existing behavior.
- Manifest
createdmetadata failures no longer become successful missing dates (#606). Exhausted 4xx, 5xx, network, and non-object failures now propagate the original failure; only 301/302/303/307/308 responses on optional manifest/blob metadata may omitcreated, preserving an already resolved digest while redirect following stays disabled. - Demo site favicon now matches the refreshed branding. The v1.5.1 brand refresh (#439) moved the website to the cropped whale "headshot" icon and the app UI followed, but demo.getdrydock.com kept showing the old full-body whale: its stale
favicon.svg— which modern browsers preferred over the PNGs — was never replaced. The demo now ships the same headshot icon set as the website and app UI, thefavicon.svgis removed, and the icon links carry a?v=2cache-buster so browsers re-fetch instead of serving the aggressively cached old icon. (#689, forward-ported in #690) - Audit nav icon no longer renders blank for the Lucide icon-library preference, and the icon bundle no longer silently drops aliased or renamed icons at image build time. PR #668's icon-bundle regeneration against tabler 1.2.38 dropped
lucide:historyfromui/src/boot/icon-bundle.jsonbecause the installed@iconify-json/lucide(1.2.121) demotedhistoryto an alias ofrotate-ccw-clock;ui/scripts/extract-icons.mjs's bundler only ever looked up plain icon entries, never aliases, so every image built since that pin shipped without it (no network fallback, since the iconify API module is offline-only). The extractor now resolves alias chains (parent-following, depth-capped at 5), refusing any alias that carries arotate/hFlip/vFliptransform the body-only bundle can't represent.ui/src/icons.ts'siconMap.audit.lucideis repointed tolucide:rotate-ccw-clockdirectly (the same clock-with-counter-clockwise-arrow conceptfa6-solid:clock-rotate-left/ph:clock-counter-clockwisealready use for the same entry) rather than relying on alias resolution for that one. A bundle-consistency sweep against the currently installed iconsets turned up more references that never resolved against the locked collections at all:iconoir:history→iconoir:clock-rotate-right,iconoir:gitlab→iconoir:gitlab-full,iconoir:stack→iconoir:multiple-pages,lucide:more-vertical→lucide:ellipsis-vertical,iconoir:key-alt→iconoir:key, plus fourfa6-brands:*icons that were never bundled because@iconify-json/fa6-brands(now exact-pinned as a devDependency) was missing fromui/package.jsonentirely — all fixed or backfilled so every<library>:<icon>reference iniconMapnow resolves. Two guard tests now cover this class of regression:ui/tests/icons.spec.tsasserts everyiconMapentry has a matchingicon-bundle.jsonkey, andui/tests/boot/icon-bundle.spec.tsindependently asserts every'prefix:name'reference found inicons.tsexists in the generated bundle with a body. - Compose-derived dependency detection now works when drydock itself runs in a container (v1.7, discussion #219).
resolveComposeDependsOn(app/dependencies/compose-dependency-resolver.ts) read a container's compose file at its host-side label path — which doesn't exist inside drydock's own container filesystem unless a bind mount happens to line up 1:1 — so most non-trivial layouts silently detected zero dependencies. Host compose-file paths are now translated through drydock's own bind mounts (reusing the existing Docker Compose trigger's translation logic,ComposePathBindMounts.ts, cached per Docker API instance) before being read; when none of a container's configured compose file paths can be translated and read, that now surfaces as a single warning naming every path tried instead of a silent empty result. - Dependency-group update confirm dialog now binds to what it actually runs, and marks restart-only members (v1.7, discussion #219). Pairs with the destructive-confirmation entry above: the confirm dialog previously fetched a wave preview but never bound the eventual dispatch to it.
confirmDependencyGroupUpdateStatenow forwards every previewed container id asexpectedContainerIdson accept and surfaces a distinct "chain has changed" toast on a 409 divergence response instead of a generic failure message or a silent retry. The wave list in the confirm message now suffixes each restart-kind member with(restart)so restart-only dependents are visually distinguished from update targets before confirming. - Update age and hot/mature/established classification now always use the same trust-aware clock as the maturity gate itself (#556). The gate (
resolveMaturityClockinapp/model/maturity-policy.ts) already checkedresult.publishedAtTrustedbefore trusting a registry'spublishedAt, falling back toupdateDetectedAt/firstSeenAtotherwise — but three other call sites computed age independently and skipped that check:getRawUpdateAge(app/model/container.ts, feedingcontainer.updateAge/updateMaturityLevel) blindlyMath.min'dfirstSeenAtandresult.publishedAt;app/api/container/update-age.ts's uncached fallback ran its own three-way blend; and the UI'scontainer-mapper.ts/useContainerPolicy.tsfallback branches (used whenever the eligibility payload has no activematurity-not-reachedblocker to read the resolved clock off of) and the age tooltip formatter hand-rolled anupdateDetectedAt-only heuristic. All four now delegate to the shared resolver (getUpdateAgeMson the app side, a portedresolveMaturityClockmirror on the UI side) instead of re-deriving it. Behavior change: an untrusted earlypublishedAt(e.g. Docker Hub/GHCR OCI build dates on other registries, or any pre-#-trust-flag data) is no longer blended into the displayed age — containers whoseupdateAge/maturity badge previously looked artificially older can now show a smaller age and flip frommature/establishedback tohot;?sort=ageand?maturity=hot|mature|establishedbucketing/ordering shift accordingly. This is the intended, correct direction (fail-closed: an untrusted date is never trusted for display any more than it is for gating) but is user-visible. No documented OpenAPI field changed —updateAge/updateMaturityLevel/updateDetectedAt/firstSeenAt/result.publishedAt*were never part ofContainerResource's documented schema (additionalProperties: true). - GHCR version-history pagination no longer silently caps out at 1,000 versions (#556).
fetchVersionsPagedForOwnerused to guess "another page exists" fromversions.length === perPageand gave up after a hardcoded 10 pages, so any GHCR package with more than 1,000 published versions (routine for a CI-heavy repo doing per-commit/nightly tags over a couple of years) silently lost trustedpublishedAtlookups for older tags with no operator-visible signal. Pagination now follows the literalLink: rel="next"URL GitHub's REST API returns (RFC 5988) — correct on the exact boundary the length heuristic got wrong — against a much higher, configurable ceiling (DD_GHCR_VERSIONS_MAX_PAGES, default 500 pages / 50,000 versions). Hitting that ceiling while a next page still exists now logs awarndistinguishing "truncated" from "confirmed absent"; thestring | undefinedreturn contract is unchanged, so a truncated scan still fails closed (no trustedpublishedAt), it just stops being invisible when it happens. updateLifecycleCachenow survives drydock's own self-update instead of being wiped by it (#556). The maturity-clock carry-forward cache (app/store/container.ts) that lets a recreated container inherit its predecessor'supdateDetectedAt/firstSeenAt/maturityGatePendingSincelived only in a bare process-memoryMap, invisible to the SIGTERMshutdown()handler'sstore.save()flush — every collection except this one got persisted. Since a drydock self-update is definitionally a cross-process container recreation (recreate action → SIGTERM → new process), the stash was reliably lost on the exact restart that needed it, silently re-stampingupdateDetectedAtas "now" and restarting any in-progress maturity soak. A newapp/store/update-lifecycle-cache.tsmodule (modeled on the existingname-bindings.tspersistence precedent, which solved the identical bug class for the agent name→key binding cache) mirrors the in-memory cache into a LokiJS collection: write-through on stash, delete-through on consume/expiry/signature-mismatch and on size-based eviction, and a newrehydrateUpdateLifecycleCacheFromStore()repopulates the Map from non-expired persisted records once at startup, right after the collection is created. No new flush hook was needed — LokiJS collection writes are synchronous, so the existingstore.save()call already picks up the persisted cache along with every other collection.