github CodesWhat/drydock v1.7.0-rc.1

pre-release4 hours ago

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_INTERVAL sets 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 (new InstallBanner component, following the existing AnnouncementBanner pattern) listens for the browser's beforeinstallprompt event and offers a one-click install, with the dismissal persisted under a versioned localStorage key. iOS home-screen install is supported via apple-mobile-web-app-capable and the existing apple-touch-icon. The backend's static UI server now serves sw.js with Cache-Control: no-cache so 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.ports now 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/8443https://, everything else → http://); the link target host prefers the port's own bound HostIp when it's a real address (not 0.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 new dd.port.label container 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 raw hostPort->containerPort/protocol mapping.
  • Container uptime, with a live-refreshing display. The existing details.startedAt field (from Docker's State.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), Escape closes the search bar, and ? opens a new shortcut-reference overlay listing the available shortcuts. A / hint now sits next to the existing ⌘K hint on the sidebar search button.
  • Container dependency ordering — data model and detection (v1.7, discussion #219). New dd.depends_on (comma-separated container names) and dd.depends_on.action (update or restart, default update) container labels. When dd.depends_on is absent, drydock detects dependencies automatically from a compose-managed container's own depends_on key (both short-form arrays and long-form objects; the condition: 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: buildDependencyGraph resolves each container's detected dependsOn names 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), and topologicalSort runs 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. A dependsOnAction: 'restart' dependent is admitted through the same admission gates as any manual request (widened updateAvailable check) and, once its dependency finishes updating, is restarted rather than re-pulled via the new restartDependentContainer primitive (app/updates/dependency-restart.ts); operations that never got a chance to run because an earlier wave failed land in a new skipped-dependency status/phase instead of silently vanishing. The Docker Compose trigger (app/triggers/providers/dockercompose/Dockercompose.ts) reorders multi-service docker compose up invocations by dependency order (sortMappingsByDependencyOrder) so compose itself never fights the same ordering. Maintenance-window batches (app/triggers/providers/Trigger.ts's runAcceptedUpdateBatch) 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/dependentCount badge counts. New endpoints: GET /api/v1/containers/dependencies returns the full resolved dependency graph (nodes, edges, detected cycles, unresolved targets, cross-host-ignored edges); POST /api/v1/containers/:id/update-chain-preview dry-runs the topological waves for the dependency chain rooted at a container without dispatching anything; POST /api/v1/dependency-groups/:rootId/update bulk-accepts every container in that chain, annotated with the wave index it will actually run in. The preview and dispatch endpoints call the exact same buildDependencyGraph/topologicalSort pair 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/dependentCount through to the UI Container type. A new "Update dependency chain" action (confirmDependencyGroupUpdate in useContainerActions) 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 listContainers returned 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, default 30000 (30s), 0 disables 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/canonicalizeContainerName in app/watchers/providers/docker/docker-helpers.ts, and the name-shape-triggered transient-alias suppression in filterRecreatedContainerAliases) — that mechanism is keyed off the container's name looking like a recreate alias, while the new settling window (filterPendingDiscoveries in app/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 recursive strongConnect — so a single dependsOn chain or cycle around 5-10k+ containers no longer risks a stack-overflow RangeError; 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 per dependsOn entry.

Removed

  • BREAKING: The legacy DD_TRIGGER_* environment variable prefix and dd.trigger.include / dd.trigger.exclude container labels are removed (deprecated v1.5.0, warned at error level throughout v1.6.0, removed per the published v1.7.0 schedule in DEPRECATIONS.md). Any DD_TRIGGER_* environment variable now fails startup outright — the error lists every detected variable next to its exact DD_ACTION_* (docker/dockercompose/command) or DD_NOTIFICATION_* (every other provider) replacement, plus the config migrate --source trigger command and a link to the deprecations page, so a config can be fixed in one pass. dd.trigger.include / dd.trigger.exclude container labels no longer resolve to anything — only the scoped dd.action.* / dd.notification.* labels are read; a container still carrying either legacy label logs a one-time error-level warning and keeps incrementing the dd_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. The usesLegacyPrefix trigger-metadata field, the now-always-empty legacy-prefix tracking in app/configuration/index.ts, and the superseded startup warning are removed along with it. The drydock config migrate --source trigger CLI 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.yaml suppressing AVD-DS-0002 (Dockerfile missing USER) with the same rationale already documented for the Dockerfile's checkov:skip=CKV_DOCKER_3 comment and the existing qlty trivy:DS-0002 triage rule: the entrypoint drops privileges at runtime via su-exec (Docker.entrypoint.sh), so no static USER instruction is needed.
  • Service-worker NetworkOnly rule for /api/** now actually matches. ui/vite.config.ts's runtimeCaching entry used a ^-anchored pathname regex (/^\/api\//), but workbox-routing tests a RegExpRoute's urlPattern against the full url.href (always starting http:///https://), never the pathname alone, so the rule could never match and silently fell through. It was harmless today only because no other runtimeCaching rule exists to catch the fallthrough — any future catch-all caching rule would have started silently caching authenticated /api responses. Replaced with an exported isApiRequest match-callback function that tests url.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/update could 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 the X-DD-Confirm-Action: dependency-group-update header, matching the existing container-delete pattern, and accepts an optional expectedContainerIds array 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.reason values are forwarded through the internal startExec end 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 created metadata 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 omit created, 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, the favicon.svg is removed, and the icon links carry a ?v=2 cache-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:history from ui/src/boot/icon-bundle.json because the installed @iconify-json/lucide (1.2.121) demoted history to an alias of rotate-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 a rotate/hFlip/vFlip transform the body-only bundle can't represent. ui/src/icons.ts's iconMap.audit.lucide is repointed to lucide:rotate-ccw-clock directly (the same clock-with-counter-clockwise-arrow concept fa6-solid:clock-rotate-left/ph:clock-counter-clockwise already 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:historyiconoir:clock-rotate-right, iconoir:gitlabiconoir:gitlab-full, iconoir:stackiconoir:multiple-pages, lucide:more-verticallucide:ellipsis-vertical, iconoir:key-alticonoir:key, plus four fa6-brands:* icons that were never bundled because @iconify-json/fa6-brands (now exact-pinned as a devDependency) was missing from ui/package.json entirely — all fixed or backfilled so every <library>:<icon> reference in iconMap now resolves. Two guard tests now cover this class of regression: ui/tests/icons.spec.ts asserts every iconMap entry has a matching icon-bundle.json key, and ui/tests/boot/icon-bundle.spec.ts independently asserts every 'prefix:name' reference found in icons.ts exists 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. confirmDependencyGroupUpdateState now forwards every previewed container id as expectedContainerIds on 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 (resolveMaturityClock in app/model/maturity-policy.ts) already checked result.publishedAtTrusted before trusting a registry's publishedAt, falling back to updateDetectedAt/firstSeenAt otherwise — but three other call sites computed age independently and skipped that check: getRawUpdateAge (app/model/container.ts, feeding container.updateAge/updateMaturityLevel) blindly Math.min'd firstSeenAt and result.publishedAt; app/api/container/update-age.ts's uncached fallback ran its own three-way blend; and the UI's container-mapper.ts/useContainerPolicy.ts fallback branches (used whenever the eligibility payload has no active maturity-not-reached blocker to read the resolved clock off of) and the age tooltip formatter hand-rolled an updateDetectedAt-only heuristic. All four now delegate to the shared resolver (getUpdateAgeMs on the app side, a ported resolveMaturityClock mirror on the UI side) instead of re-deriving it. Behavior change: an untrusted early publishedAt (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 whose updateAge/maturity badge previously looked artificially older can now show a smaller age and flip from mature/established back to hot; ?sort=age and ?maturity=hot|mature|established bucketing/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 of ContainerResource's documented schema (additionalProperties: true).
  • GHCR version-history pagination no longer silently caps out at 1,000 versions (#556). fetchVersionsPagedForOwner used to guess "another page exists" from versions.length === perPage and 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 trusted publishedAt lookups for older tags with no operator-visible signal. Pagination now follows the literal Link: 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 a warn distinguishing "truncated" from "confirmed absent"; the string | undefined return contract is unchanged, so a truncated scan still fails closed (no trusted publishedAt), it just stops being invisible when it happens.
  • updateLifecycleCache now 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's updateDetectedAt/firstSeenAt/maturityGatePendingSince lived only in a bare process-memory Map, invisible to the SIGTERM shutdown() handler's store.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-stamping updateDetectedAt as "now" and restarting any in-progress maturity soak. A new app/store/update-lifecycle-cache.ts module (modeled on the existing name-bindings.ts persistence 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 new rehydrateUpdateLifecycleCacheFromStore() 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 existing store.save() call already picks up the persisted cache along with every other collection.

Don't miss a new drydock release

NewReleases is sending notifications on new releases.