v1.7.0-rc.8
Full Changelog: v1.7.0-rc.7...v1.7.0-rc.8
[1.7.0-rc.8] — 2026-09-03
Fixed
dd.registry.lookup.image(and its legacy aliasdd.registry.lookup.url) did nothing for any container reported by a controller-Docker-transport agent (e.g. Portwing).AgentClient.ts'sbuildContainerReport()callsnormalizeContainer()to pick a registry provider for these agents, but that function readsimage.registry.lookupImageas an already-populated field. The controller's own Docker watcher does translate the label, but only when it discovers the container first, and every later agent report replaced the storedimageblock wholesale, so the field never survived. A container mirrored through a private registry and labeled to divert update checks to its real upstream image (the #336 scenario) silently checked the mirror's own sparse tag copy instead, with no error.buildContainerReport()now applies the same label (falling back to the legacy alias, matchingcontainer-init.ts's own precedence) before normalizing, and never overwrites alookupImage/lookupUrlthe agent already reported.- Compose updates ran the post-pull security gate after the service had already been replaced. The Docker-native path pulled, scanned, and enforced policy before stopping the old container, but the Compose path called the same gate only after it had stopped, removed, and recreated the service. A blocked image was therefore already running and already written into the compose file by the time the block verdict arrived. Compose now separates preflight from runtime mutation: it pulls once, captures the exact repository digest, runs signature verification, vulnerability scanning, and SBOM generation against that immutable reference, and only then permits any stop, remove, or create; the per-service platform check still runs before that service is touched. If preflight fails after the candidate compose-file mutation, the existing rollback restores the file before the error is surfaced. Compose-file-once mode preflights every affected service before the first runtime mutation, and a failed preflight terminalizes every queued operation. If the daemon cannot bind a scan to the pulled image, block mode and signature verification fail closed; availability
warnrecords a skipped scan and continues without pretending a mutable-tag scan protected the replacement. Scaled services reuse the same captured identity, private registries are matched exactly, and recovery preserves the operator-facing tag from the hybridtag@digestreference. - The ordinary Docker update pulled by mutable tag, then verified, scanned and deployed whatever that tag pointed at next.
ContainerUpdateExecutorpulledrepo:tagand handed the same mutable reference to signature verification, the vulnerability scan and SBOM, the runtime-config compatibility lookup, and finallyPOST /containers/create. Nothing tied those steps to the image the pull had actually fetched, so a registry retag landing anywhere in that window meant one image was gated and a different one was deployed. The Compose path's equivalent was closed in #952; this is the same hole on the Docker-native path, which is the default for every non-Compose container. The executor now inspects the pulled image once, matches itsRepoDigestsagainst the reference's own repository so a lookalike entry for another registry cannot satisfy it, and pins an immutablerepo:tag@sha256:...that signature verification, the scan, SBOM, the config lookup and the replacement create all use. Signature verification moved from before the pull into the post-pull gate, because verifying a mutable tag ahead of the pull says nothing about what arrived. If the daemon cannot bind the pulled image to a manifest digest, block mode and signature verification fail closed and terminalize the operation before anything is renamed or created, while availabilitywarnrecords a skipped scan and continues rather than pretending a mutable-tag scan protected the replacement. Moving the gate behind the pull moved the pre-update hook and the prune/backup step behind the gate with it, so an image the gate goes on to reject no longer fires an operator hook, deletes cached images or writes a rollback row on its way to being refused, and a container failing the gate on every cycle can no longer push a real rollback point out of history. A reference that is already digest-pinned is left alone, the tag the operator configured is still what the operation record, the compose-file sync and the UI show, and the identity binding is now one implementation shared with the Compose path instead of two. - A Docker Hub image pulled through the
index.docker.ioalias could not be bound to its pulled digest, so the update failed instead of proceeding. The post-pull identity binding matches the daemon'sRepoDigestsagainst the repository forms the pulled reference could legitimately be recorded under, and that candidate list knewdocker.ioandregistry-1.docker.iobut not the legacyindex.docker.iohost. The daemon rewrites that alias todocker.ioand records Hub images under their short name, soindex.docker.io/nginx:1.27was matched against a candidate list holding onlyindex.docker.io/nginxand never matched thenginx@sha256:...entry it had just pulled. Signature verification and block mode then failed the update closed on an image that was perfectly bindable, and availabilitywarnskipped a scan it could have run. This reaches any container whose registry provider resolves through that host, including a custom registry configured atindex.docker.ioand add.registry.lookup.imagediversion that leaves the deploy reference on the alias. Every other Docker Hub alias check in the codebase already covers all three hosts; this one now matches them. - A compose update ran the operator's pre-update hook, pruned images and wrote a rollback row before the security gate had looked at the candidate. Moving signature verification behind the pull put the gate behind those three steps as well, and the compose path kept that ordering when the Docker-native path was changed to defer them alongside the digest pinning. A compose image that failed verification had therefore already fired
dd.hook.pre, deleted cached images underPRUNE=trueand inserted a backup record on its way to being refused, and a service failing the gate every cycle pushed real rollback points out of history through the backup retention prune that runs after a failed update. The compose trigger now defers the same two steps behind its post-pull gate, except in compose-file-once mode, where the preflight has already gated every service in the batch before the first runtime mutation and there is no post-pull hook left to hang them off. The unbound-image path was the other half of it: when the daemon cannot bind the pulled image to a digest and availabilitywarnallows the update anyway, compose skipped the post-pull hook outright, which under the new ordering would have skipped the pre-update hook and the prune/backup step with it, so it now runs that hook with only the gate half suppressed, exactly as the Docker path already did. - The watcher-snapshot handler still pruned on an empty container list, and a test had frozen that in place.
AgentClienthas four entry points that ingest a container list, and three of them (handshake(),watch()andhandleContainerSync()) already refuse to prune when that list is empty, the first for the cold-start race in #386 andwatch()most recently in the rc.6 sweep (#922).handleWatcherSnapshotEvent(), which handles thedd:watcher-snapshotserver-sent event, never got one. A reconnecting agent legitimately reports zero containers on this path:filterPendingDiscoveries()only bypasses the discovery-settling delay for container ids already in the agent's own local store, and an agent restart just reset that store, so every genuinely running container is held back on the first cycle. An empty incoming list then made every stored container look like a real removal, sodeleteContainer()ran withoutreplacementExpected,stashUpdatePolicyForReplacement()never fired, and the container's maturity policy override was deleted with nothing left to restore it from. This is why the durability fix in #743 did not cover it: that made the stash survive a process restart, and on this path nothing was ever stashed. Note the existing test asserted the broken behaviour outright, having been written from the implementation back in 1.5.0 before the ambiguity was understood, so it is inverted here rather than extended. (#565) - The Trivy supply-chain advisory told readers to expect a version the image stopped shipping. The page names a bundled Trivy version in three present-tense places and quotes the Dockerfile's digest-pinned
trivy-binstage, and all four had drifted: it said0.72.0and quotedsha256:cffe3f51…while the image ships0.73.0fromsha256:7cced7ca…. Caught by running the published rc.6 image and following the page's own instruction,docker exec drydock trivy --version, which is the exact check the advisory tells a reader to run. Someone doing the responsible thing got a mismatch, and the Dockerfile they were invited to compare against on GitHub showed a different digest again, so the only two conclusions available were "the advisory is wrong" or "my image is tampered with". The dated correction paragraphs keep their historical framing rather than being restamped with today's number. The underlying cause was that a Renovate digest bump moves the pin and nothing checked the page against it, so a coupling test now reads the Dockerfile's pin and fails when the advisory disagrees, in the same style as the existing docs version coupling test. - A bad URL on the website left visitors with no way back. There was no
not-found.tsxanywhere in the site, so an unknown docs slug fell through to Next's built-in fallback: bare text, no header, no footer, no link into the site. Every real page renders through the marketing or docs shell. Stale docs links are not hypothetical here, the trigger-prefix restructure in v1.5.0 invalidated a batch of them, so the people most likely to hit this were the ones following an old bookmark or a search result. The 404 now renders in the marketing shell with links to the homepage and the docs. - The audit log's search box reported a match count against a total it never searched. The container, action and date filters all query the server, but the free-text box filters only the rows already fetched, because the audit API has no full-text parameter. The filter bar compared that page-scoped match count against the server's full unfiltered total, rendering as
2/500, which reads as "2 of your 500 entries matched" when it meant "2 of the 50 on this page, and nothing looked at the other 450". A user searching for an older event got a near-empty result that positively asserted the event did not exist. While a search is active the count is now against the page and labelled as such. Audit is the only server-paginated list view, so no other view carried this. - The servers detail panel ignored its own Refresh button. Refetching rebuilds every entry, and the selected server was captured at click time and never re-synced, so the panel kept showing the container counts, status and last-seen from whenever the row was clicked while the table row behind it updated. Clicking the panel's own Refresh made that worse rather than better, since it triggered exactly the refetch that orphaned the panel's copy. The agents and containers views already do this re-lookup, so this was a missed site rather than a missing pattern. The panel now re-resolves the selected server by id after every refetch and closes if it is gone.
- The self-update helper could destroy a health-verified replacement when removing the old controller failed. The main Docker update path already treated old-container cleanup as best effort after its health gate, but the helper still sent a 409, timeout, or other removal failure into rollback. That rollback force-removed the healthy replacement first and could then fail to restore an old container that Docker had already reaped. The helper now treats a missing old container as already cleaned up and records every other cleanup failure on the successful operation without rolling back the replacement.
- A container seen before its registry was configured stayed stamped
unknownforever.shouldRepairStoredImageReference()only re-derived a stored image reference when the tag wasunknownor digest-shaped, so a container whoseimage.registry.namewas alreadyunknownbut whose tag was otherwise normal never re-entered the repair path on refresh, and only recovered if it was recreated. The repair now also runs when the stored registry name isunknown, re-resolving it from the live image inspect on the next refresh cycle. Reported by @depuits in #945. DD_AGENT_ALLOW_INSECURE_SECRETwas parsed as an agent namedallow.getAgentConfigurations()handed the wholeddEnvVarsmap to the genericdd.agentprefix parser, and that documented flat flag matches the prefix asDD_AGENT_ALLOW_INSECURE_SECRET, producing an{insecure: {secret: 'true'}}agent literally namedallow. Registration then rejected it every boot withAgent allow failed to register ("host" is required), on every install that set the flag regardless of whether any real agents were configured. The flag is now excluded before the map is parsed. Reported by @depuits in #945.- Debug dumps no longer expose Apprise service URLs, Rocket.Chat user IDs, or Telegram chat IDs. These provider-specific credential fields are now redacted without hiding ordinary
parse.urlsconfiguration flags.