Changelog
All notable changes to this project will be documented in this file.
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Fork point: upstream post-8.1.1 (2025-11-27) Upstream baseline: WUD 8.1.1 + 65 merged PRs on
main(Vue 3 migration, Alpine base image, Rocket.Chat trigger, threshold system, semver improvements, request→axios migration, and more)
Unreleased
1.7.0-rc.18 — 2026-10-04
Security
- Login lockout now keys on the credentials actually presented. A request could previously name a different username in its body to dodge an account's failed-login lock, leaving only the per-IP limit.
- Container start, stop and restart responses now redact sensitive environment values the same way the container detail endpoint does.
- The demo site now sends a full Content-Security-Policy.
apps/demo/vercel.jsonsent onlyframe-ancestors, soscript-srcanddefault-srcfell open and ZAP raised rules 10055-4, 10055-5 and 10055-13 on every scan. The policy now pins scripts, the mock service worker and form targets to'self', and allows only the jsDelivr and Iconify hosts the mock icon and font handlers fetch from. - Add
Cross-Origin-Opener-Policy: same-originand a static strict CSP on/apiresponses to the website, and stop sendingX-Powered-By.
Changed
- ZAP alerts in code scanning are now keyed per scanned site.
scripts/zap-json-to-sarif.mjsstripped the origin from every location, so getdrydock.com, the demo and the app scan shared one alert per rule and path, and dismissing it for the public site hid the same finding on the app. Locations now readgetdrydock.com/robots.txt. Expect one round of reopened and closed alerts when the next scans upload. - The weekly DAST scans of getdrydock.com and the demo can now pass. The two public-site ZAP jobs read a new
.zap/rules-public-site.tsv, which adds IGNORE entries for the reviewed false positives (SQL, private IP, timestamp, eval, debug-error and application-error text matched in docs prose, plus proxy, user-agent and public-file CORS notices) and the accepted CORP and COEP choices. The app scan inci-verify.ymlkeeps the stricter.zap/rules.tsv, and a workflow test pins both. - A stable release shows its own version instead of the release candidate it was promoted from. A stable release is the last release candidate's image, promoted unchanged, so the
1.6.1image reported1.6.1-rc.15in the UI, the API and Home Assistant. Drydock now shows the base version (1.6.1) and keeps the full build identity as a separate build field. The build appears in the About dialog, under Config > General and in the agent detail panel when it differs from the version, and the startup banner (shown on a TTY) readsversion 1.6.1 (build 1.6.1-rc.15). Thedrydock is startinglog line carries no version.GET /api/v1/appandGET /api/v1/agentskeepversionas the base version and addbuild, the agentdd:ackevent carries both, the debug dump addsdrydockBuildnext todrydockVersion, and Home Assistant'ssw_versionand the OpenAPI document version use the base version. API clients that parsed a prerelease suffix out ofversionshould readbuildinstead. Values that aren't a semver with a prerelease suffix, such aslocalorci, are reported unchanged. The controller applies the same split to the version an older agent or a Portwing edge agent reports, so those agents show their base version too. Reported in #1284.
Fixed
- The website docs pages
/docs/v1.6/changelog,/docs/v1.6/configuration/ui,/docs/v1.7/changelogand/docs/v1.7/configuration/uino longer return a 500. A remark plugin renders bare{column},{date}and{countdown}in prose as literal text instead of evaluating them as JS expressions. - Stop the dashboard from showing a connected Local Docker host when no local watcher is configured. Agent-only fleets show only their configured agents.
- Apply the configured outbound HTTP timeout to Docker Hub publish-date metadata requests, so a stalled response cannot indefinitely hold up container discovery. Existing bounded retries and publish-date failure handling are unchanged.
- Keep bulk scan counts and scanner results accurate when a post-scan notification fails. Each completed task is counted once, and notification delivery failures no longer replace a completed scan's status with an error.
- Keep bulk scan progress accurate for large fleets and busy event streams while HTTP acceptance is delayed. Fresh request correlation and server-owned cumulative counts replace the 500-entry early-event buffer, with bounded client state and explicit recovery for conflicting or lost progress. Duplicate/replayed events do not overcount, and failed tasks still advance progress.
- Correlate bulk scan progress with the accepted scan cycle, retain early completion events, and ignore duplicate or unrelated scans. The Security page now shows localized request/progress errors and supports an explicit retry without automatically repeating a scan request. Lost progress requires a successful read-only results refresh before starting another scan; refreshing does not establish whether the original scan has finished.
- Release-gated store migrations now compare against the base version, so a future migration gated on a release runs on that release's stable image rather than one release later.
1.7.0-rc.17 — 2026-10-02
Security
- Patch brace-expansion to 5.0.12 and the website to Next.js 16.3.6. Update DOMPurify to 3.4.16 for its in-place sanitization fix.
- Update Undici to 8.10.2, Nodemailer to 10.0.9, gRPC to 1.14.5, Moment to 2.31.0, fast-uri to 4.1.5, and ip-address to 10.7.1 for the newly published dependency advisories. Keep UI and E2E Undici on their patched 7.29.1 line. Nodemailer 10 requires Node.js 20 or newer; Drydock already requires Node.js 24.
- Update Alpine OpenSSL to 3.5.9-r0 after 3.5.8-r0 left the package index and the pinned install stopped resolving.
- Patch Alpine zlib 1.3.2 with the upstream fix for CVE-2026-85091 and update libexpat to 2.8.5-r0 for CVE-2026-93990. The temporary zlib APK retains its upstream version and records a unique local revision; its exact backport is documented in the image scanner's VEX evidence.
Fixed
- #1284: a disabled healthcheck no longer health-gates the update. A container with
healthcheck: disable: true(stored by Docker asTest: ["NONE"]), an empty test, or a healthcheck block with only timing fields was treated as having a healthcheck. Docker never reports a health state for those, so the Docker action waited out the whole rollback window and rolled back an update that had started fine. The gate now applies only when the container has a realCMDorCMD-SHELLprobe or Docker is reporting health. - #1280: update policy set in the UI survives recreation of an agent-managed container. An agent reports a recreated container as a removal of the old id and an addition of the new one. The controller deleted the old row without keeping its policy, so a maturity setting made in the UI was lost every time the container was updated. The policy is now carried to the replacement whichever of the two events arrives first, and also when a reconnecting agent's snapshot drops the old id before listing the new one. It is only ever carried to a container with the same agent, watcher and name. A local scan that catches Drydock's temporary
-old-<timestamp>rename no longer stores that name, which could make the same carry-over miss on the controller host. - #1281: the Docker Hub config blob redirect is no longer logged as a failure. Registry redirects are not followed, so the optional created-date lookup against Docker Hub always ends in a 307. The debug line now says the created date is optional and was skipped, instead of "Unable to fetch image config blob created date", which read like the reason an update had not run.
- Let accepted bulk vulnerability scans finish after the HTTP request completes normally. Previously, inventories larger than the four-scan concurrency limit could stop after the first batch while the UI kept waiting for the remaining results. Prematurely closed, incomplete requests still stop queued scans.
1.7.0-rc.16 — 2026-09-15
Fixed
- Keep Crowdin source uploads and translation PRs on the highest integration branch, including runs triggered by maintenance-line pushes, so the shared translation branch cannot target an older release line with newer product changes.
- Accept newer stable YAML and PostHog pins in dependency guards while enforcing the YAML security floor and manifest/lockfile consistency, including nested YAML installs.
- Update the Docker image's timezone package pin to
tzdata=2026d-r0, available in Alpine 3.24 for amd64 and arm64, after2026c-r0left the package index. - Backport #1139 to publish agent container state on the MQTT topic Home Assistant discovery advertises, fixing a cause of entities staying
Unknownwith v1.7's default agent segmentation. Existing retained state on the old unscoped topics is not removed automatically. - Restore website referral-source reporting while keeping referrer URLs and campaign parameters out of analytics.
1.7.0-rc.15 — 2026-09-10
Fixed
- Stop an edge agent's in-flight component initialization from publishing after disconnect and replacing its reconnected owner. Retired registrations clean up only their own components. Disconnected Docker proxy, log, delete, and exec requests now fail immediately without creating new request state or sending frames.
1.7.0-rc.14 — 2026-09-08
Fixed
- A maintenance cut labeled the shipped image with
main's commit instead of the commit it was built from. Bothdocker/metadata-actionsteps inrelease-cut.ymlleftorg.opencontainers.image.revisionat its default,github.sha, which is the workflow run's own checkout rather than the dev-branch source commit the build used. The v1.6.1-rc.9 staging image carried5ae315227in that label instead of2969675ef. Both steps now set the label from the release source SHA. - Rolling back a container (auto-rollback on an unhealthy update, or a manual restore) now runs the same runtime-config sanitization as an update, so an entrypoint or command the newer image introduced is no longer copied onto the older image and the rolled-back container starts.
- Tag family matching now requires the candidate's variant suffix to match the current tag's exactly, so a container on 1.27.3-alpine is no longer offered 1.28.0-alpine-perl (or 1.28.0-alpine from -alpine-slim) under dd.tag.family=loose or a dd.tag.include filter.
- Precision-only suffix carve-out now requires at least one digit placeholder, so a trailing dot (
1.3.0-alpine.) no longer counts as the same tag family as1.3.0-alpine. - Monthly and longer watcher schedules could expire scans after 1 ms. The scan deadline was twice the cron interval, which overflowed Node's timer limit and cleared the in-flight scan guard almost immediately. Deadlines now stop at the largest supported delay, preserving the existing ten-minute floor and shorter schedule behavior.
1.7.0-rc.13 — 2026-09-08
Fixed
- Containers on a floating tag that drydock first saw before v1.5.0-rc.17 could stay marked Current forever, even when the registry had a newer digest.
image.digest.watchwas written once at first discovery and never revisited, so a row discovered before the digest-watch default changed from!isDockerHubDomain(domain)to "watch when the tag is meaningful" (v1.5.0-rc.17) kept the oldfalsedefault permanently; only recreating the container picked up the new one. It is now re-derived every scan, the same wayisLocalImageanddigest.repoDigestsalready are, and an explicitdd.watch.digestlabel or imgset override still wins. (#1070) - The weekly ZAP full scan of getdrydock.com ran into its 60-minute job timeout on every run, so it never produced a report. The scan step now caps the spider at 10 minutes and the active scan at 35 minutes (5 per rule), leaving room for startup, the passive scan and the report inside the job budget; a workflow test pins the arithmetic.
1.7.0-rc.12 — 2026-09-06
Fixed
- The demo site did not send
Cross-Origin-Opener-Policy, so the weekly DAST scan failed on ZAP rule 90004 every run.apps/demo/vercel.jsonnow sendssame-originnext to the existingCross-Origin-Embedder-Policyheader. - The arm64 pass of the image arch check failed on every multi-platform cut with
docker: cannot overwrite digest sha256:<index>.scripts/check-image-arch.shran once per platform against the same index-digest reference (ghcr.io/codeswhat/drydock:release-staging-N@sha256:<index>), and docker's classic image store cannot hold two platform variants under one digest, so the amd64 pass succeeded and the arm64 pass that followed it always failed. This killed the v1.6.1-rc.9 cut. The script now resolves each platform's own manifest digest out of the index viadocker buildx imagetools inspect --rawand jq before it runs docker, skipping attestation entries, and falls back to running the reference unchanged when it isn't an index at all. - Two more prunes cleared a container's update policy the same way the startup prune used to. The startup prune stashes a departing record's update policy under its Docker id so the record that replaces it inherits it, but the agent-removal prune and the agent's own stale-container prune still called
deleteContainerplainly. An agent removed from config or renamed, or a container handed from one agent to another (or back to the controller's own watcher), lost its snooze, maturity mode and minimum age, and skipped tags or digests the same way the startup case did before that fix. Both prunes now passidentityChangeExpected: trueas well, so the same Docker-id stash carries the policy across either hand-off. - A manual "check now" (the dashboard, the API, a webhook, or the controller polling an agent) could fire the same notification twice if it landed while a scan was already running.
watch()has no re-entrancy guard of its own, soPOST /api/v1/containers/watch,POST /api/v1/webhook/watch, and the agent's own watcher API each started a second, fully independent scan on top of one already in progress from the cron schedule or another manual request, and underonce=trueboth passes could read "not yet notified" before either had finished writing its own history, so a trigger like Slack or Telegram sent the same update twice. All three now route through the same single-flight scan orchestration the cron schedule uses: a call that lands mid-scan is folded into that scan's one follow-up instead of starting an independent scan, and the response reports it (result.coalescedin the JSON body for the first two, anX-Drydock-Watch-Coalescedheader for the agent endpoint, which keeps its existing bare-array body for compatibility across controller/agent version skew). Manual and API scans still run regardless of a configured maintenance window, exactly as before; only automatic installation is deferred by it. Separately,handleMaturityGateClearedEvent's ownonce=truecheck read notification history with a bare, unreserved lookup, so two overlapping evaluations of the same maturity-cleared event could both pass it before either recorded a result; it now takes the same reservation the generic update-available path does before dispatching. - A once-notification reservation that never settled held its dedup key for the process lifetime.
once=truereserves atriggerId::containerId::eventKind::resultHashkey before sending so an overlapping evaluation of the same result cannot send twice, and releases it once the send settles.runHandlerWithTimeoutdetaches a handler that misses its 30-second deadline instead of waiting on it forever, so a provider that never resolves or rejects left its reservation held forever too, and every later scan for that exact result read it as still in flight and silently skipped the send. The reservation now carries its own expiry, four times the handler timeout, that releases it with a warn log naming the key if nothing released it first. - DR-121: the session store and the main store wrote the same
/store/dd.json, and whichever one saved last erased the other's data.express-session'sconnect-lokistore opened its own independent LokiJS instance on the exact file the main store already used, and LokiJS'ssaveDatabase()always serializes the whole in-memory database, so a session autosave (every 5 seconds, and armed by nothing more than an authenticated request touching its session) reverted every container, setting, and audit row the main store had written since boot, while the main store's own autosave (every 5 minutes) deleted theSessionscollection out from under active logins. In practice this meant containers or settings written after startup could vanish fromdd.jsonafter a crash or a hard stop, and a restart could log every user out. The session store now writes to its own sibling file,dd-sessions.jsonby default (derived fromDD_STORE_FILE, so a custom filename still gets a distinct sibling rather than colliding with anything else on the volume), and the main store drops and logs a staleSessionscollection left behind indd.jsonby an older build instead of continuing to re-save it. Backups remain scoped to the main store file; sessions are not included and are expected to be dropped on restore, which just logs everyone out rather than corrupting anything.
Documentation
- The agents page's paired Gitea registry example had the controller talking HTTPS to an agent serving plain HTTP. The controller block set
DD_AGENT_REMOTE1_CAFILE=/certs/agent-ca.pem, but the agent block above it had noDD_SERVER_TLS_*variables or certificate mounts, so the example copied as written could never connect. The agent block now mountsagent.pem/agent-key.pemand setsDD_SERVER_TLS_ENABLED,DD_SERVER_TLS_CERT, andDD_SERVER_TLS_KEY, with a comment noting the certificate must be signed by theagent-ca.pemthe controller mounts and be valid for the host the controller dials.
1.7.0-rc.11 — 2026-09-05
Fixed
- OIDC login on v1.7.0-rc.1 through rc.10 bounced straight back to the login page. The service worker's navigation fallback denied
/api/and nothing else, so every other top-level navigation was answered from the precached app shell. The identity provider redirects the browser to/auth/oidc/<name>/cb?code=..., which is a document navigation, so the callback was servedindex.htmland never reached Express: no code exchange, no session, and the SPA booted and bounced to/login. Basic auth was unaffected because it authenticates overfetch, which the navigation fallback never touches.skipWaitingandclientsClaimre-register the worker on the next load, so clearing site data only helped until the page reloaded. The navigation fallback now skips every server-owned route (/api,/auth/,/health,/metrics) and serves the shell only for the SPA's own paths; the/authsettings view still loads from the shell, because Express matches that mount path too but has no handler for it, so the shell answers instead. v1.6 was never affected, it shipped no service worker. (#939) - A controller running its own default
localwatcher refused every container reported by an agent whose watcher was also namedlocal. The ownership gate added in v1.7.0-rc.6 (#922) decided ownership of a container that had no store row yet from the watcher's name: the report was refused whenever the controller had a watcher registered under that same name. The race it closes is real, an agent can otherwise claim a container id that lives on the controller's own host before the controller's watch cycle writes the record, but a watcher name is not evidence of who owns an id, and it collides by default. A controller with noDD_WATCHER_*registers its default watcher aslocal, and the agents quickstart setsDD_WATCHER_LOCAL_SOCKETon the agent, so both sides are calledlocalwhile watching entirely different hosts. On a fresh store that refused every container the agent reported, on every ingest path, loggingwatcher 'local' belongs to the controller's local watcher namespaceand leaving zero agent rows in the UI and the API, permanently. An install that already had the rows stored kept working until a container was recreated, because the gate also feeds the prune keep-set: the stale row was pruned as a removal rather than a replacement and the new id was refused, losing that container for good. Ownership of a no-record id is now decided by what the controller's own watchers have actually enumerated. Each controller-local Docker watcher records the container ids itslistContainers()call returns, replacing the set every cycle so a container that leaves the controller's host stops being claimed, and clearing it when the watcher is deregistered. An agent report with no store row is refused only when its id is in one of those sets, and the log line names the watcher holding it. A controller-side agent watcher running the Docker transport records nothing, since it enumerates the agent's daemon rather than the controller's. Disabling the controller's local watcher or renaming the watcher on either side were the only workarounds. - A rollback of a compose-managed container redeployed the update it was undoing.
Dockercompose.recreateContainerwrote the image it was handed into the compose file and then handed the runtime refresh no image at all, so the refresh re-derived one from the container's own update candidate. The compose file said the backup and the container that came up ran the update, while the API answeredContainer rolled back successfullyand the audit row recorded a success. Clearing the update result did not help either, because the fast resync that follows an update had already moved the container's tag to the new version by the time anyone rolled back. The recreate now passes the caller's image through to the refresh, so the container, the compose file and the report agree. Automatic rollback on a failed healthcheck never got that far on a compose stack at all: the health monitor called the trigger with only the container's id and name, and the compose action read the registry, the watcher and the compose labels off the container to find the service to rewrite, so it threw a TypeError before it reached the compose file. That failure was logged as an auto-rollback failure and recorded in anauto-rollbackaudit row carrying the error, neither of which stopped the unhealthy container from staying up on the version that had just failed its healthcheck. The monitor now hands over the whole container, carrying the id of the replacement it is rolling back, and the container type it arrives as requires the registry and the watcher, so the stand-in that caused this cannot compile again. Getting that far exposed the next one: the monitor logged that it was pulling the backup image and never pulled it. Neither recreate fetches the image for itself, because the manual rollback inapp/api/backup.tspulls before it calls in and a second pull there would be wasted work, so a backup image no longer on the host failed at create, on the Docker path after the running container had already been stopped and removed and on the compose path after the runtime refresh removed it and then "restored" it onto the failing update the rollback existed to undo. The automatic rollback now pulls the backup reference, digest-pinned when the record carries one, before it touches anything, and the compose refresh refuses an image it cannot find locally before the stop and remove rather than at create, so a rollback that cannot happen leaves the running container where it is. That pull is skipped when the backup image is already on the host, so a rollback on a host with no route to the registry, or one hitting a Docker Hub anonymous rate limit, still restores from the image the prune retained. - The arm64 image published for v1.7.0-rc.4 through rc.10 was an x86-64 image wearing an arm64 label. #881 rolled the
node:24-alpineandalpine:3.24base pins to digests that name a single amd64 manifest instead of the multi-arch image index, and buildx resolves a digest pin the same way for every--platform, so the arm64 stage built on an amd64 rootfs and nothing in the build, the manifest list, or the image scans noticed. On a Raspberry Pi the container died atexec /sbin/tini: exec format error(#1021). Both pins are back on image index digests, CI now rejects anyFROMpinned to a per-platform manifest, and the release reads the ELF machine type of/sbin/tini,/usr/local/bin/nodeand/bin/healthcheckin each platform of the image it built and refuses to sign, tag, or promote one whose binaries do not match the platform they are published under. - A container that moved to an agent stayed stranded when the controller's local watcher was turned off.
DD_LOCAL_WATCHER=falseleaves the controller with no local watcher registered, and the startup prune that clears records naming a watcher that no longer exists skipped that case entirely, so the old controller-owned record survived every restart. The agent's report for the same container was then refused as owned by the controller, the record was never rewritten to name the agent, and the container sat on whatever it last said. The prune now runs when no local watcher was configured in the first place. It still leaves the store alone when watchers were configured and all of them failed to register, which looks identical in the registry but means something transient rather than an operator decision. A snooze, a maturity mode and any skipped tags or digests set from the UI live on that record, and they travel with the container rather than dying with it: the prune stashes them under the container's Docker id and the agent's copy inherits them when it arrives. - A container handed from the controller to an agent came up with its update policy cleared. The startup prune deletes the controller's record so the agent can claim the container, and it deleted it the way it deletes a container that is gone for good. Nothing would have helped if it had asked for a replacement either: the stash that carries an update policy across a delete is keyed on agent plus watcher plus name, and a move to an agent changes all three, so the incoming record looked up a key nothing had ever written. A snooze, a maturity mode and minimum age, and skipped tags or digests were all silently back to defaults, which for a maturity gate means the next update goes out with no soak at all. The prune now stashes the policy under the container's Docker id, which is the one thing the move leaves alone because it is the same physical container on both sides, and a record arriving under a new identity with that id inherits it. The identity-keyed stash is untouched and still covers the opposite move, a recreate, where the id changes and the identity does not. The same fix covers a watcher rename, which loses the policy for the same reason. A container recreated as part of the move mints a new Docker id and matches neither key, so that one still starts from the declarative policy.
- One local watcher failing to register deleted its containers' records when a second watcher registered fine. The startup prune keeps records whose watcher is in the registry, and #1025 only protected the case where every configured watcher failed. With two configured and one of them broken by an unreadable CA file, a socket that is not there, or a value the schema rejects, the surviving watcher keeps the registered set non-empty, so the broken watcher's records were pruned as though the operator had renamed it away, and it came back to an empty slate once its configuration was fixed. The prune now keeps any record whose watcher is still present in
DD_WATCHER_*, whether or not it registered this run, and only prunes a watcher that is neither registered nor configured.registerWatchersalso waits for every registration to settle instead of returning the moment one rejects, so the prune reads the registry the run actually produced rather than whichever watchers happened to have landed by the time another one failed.
Documentation
- The agents page didn't say registries have to be configured on every agent, not just the controller. A traditional agent runs its own watcher and does its own registry matching and update checks, so
DD_REGISTRY_*configured only on the controller left every agent-reported container from that registry stampedunknown, credentials are never pushed from controller to agent. The controller needs the same registry configured too, or the container's registry link in the UI resolves to a registry the controller was never told about. A new "Registries on agents" section spells this out with a worked Gitea example on both sides. Reported in #945. - The agents page's registries example pointed
DD_AGENT_REMOTE1_CAFILEat a CA file the controller's compose service never mounted. The environment variable named/certs/agent-ca.pem, but the example'sservices.drydockhad no matchingvolumes:entry, so a reader following it as written couldn't find the file on the container's filesystem. The example now mounts./certs/agent-ca.pemread-only at that path, and the inline comment points at the controller environment variables reference instead of citing an unrelated plain-HTTP quickstart. - The watchers page said manual updates bypass the maintenance window but didn't say the window gates the entire scheduled scan, not just installing an update. A closed window means new containers stay invisible in the UI until the next window opens, container state shown in the UI goes stale (a container stopped during the last window still shows as stopped, and a start action on it fails because Docker refuses to start a container that's already running), and update notifications are deferred right along with the update itself. A manual scan (
POST /api/v1/containers/watch, or the UI) bypasses the window the same way a manual update does, because it calls the watcher'swatch()directly rather than the cron path that checks it. Discussed in #946.
1.7.0-rc.10 — 2026-09-04
Fixed
- An update could be announced twice, and a torn-down watcher could still warn about a scan deadline it no longer owned. The
once=truereservation added for #972 covered the simple notification path only, so batch and digest eligibility still did a plain read of the notification history, and that history is written after the send resolves. A manual single-container scan overlapping a cron scan therefore passed the check in both evaluations and sent the same candidate twice in batch mode, and in digest mode a report landing while a flush was still sending re-buffered the same result behind the send, so the post-send delete skipped it and the next flush sent it again. Batch and digest eligibility now take the same synchronous reservation the simple path does, the batch releases every reservation it took in a finally, and the digest flush holds one for exactly the results it is sending. Two paths that bypassed the gate entirely are closed with it. The digest flush swaps the current store container in for the buffered one at send time, and when that substitute was a result an earlier flush had already sent it went out again while the post-send delete quietly dropped the newer candidate the buffer was holding; it is now skipped and evicted instead, and a genuinely pending update re-enters the buffer on the next scan. Entries in the batch retry buffer, which every batch re-sends until one succeeds, took no reservation at all, so two overlapping retries both carried the same entry to the trigger. Separately, the cron scan's deadline timer lived only in the closure that raced it, so deregistering a watcher while itswatch()was stalled left the timer running to the full deadline and then logged "exceeded its deadline" against a watcher that no longer existed, while the caller that started the scan and every caller coalesced into it stayed pending. The timer and a cancel handle now live on the watcher state, deregistration clears the timer and settles the race with its own sentinel so every caller resolves to an empty result with no deadline warning, and the per-scan cleanup is identity-guarded so a late settlement cannot clear a newer scan's handle. A scan requested after teardown is now refused outright, which also covers the docker-events debounce:just-debounceexposes no cancel, so its pending timeout still fires up to five seconds after the watcher is gone and used to start a full scan against it.
1.7.0-rc.9 — 2026-09-03
Security
fflateoverride added ine2e/for CVE-2026-45820.@smithy/middleware-compressionpinsfflateat 0.8.1 exactly, so the fix had to come through an npm override;fflatenow resolves to 0.8.3 ine2e/package-lock.json, which is the only workspace that carried the vulnerable range. Lockfile-only change, no runtime code touched.
Fixed
watchFromCron()had no re-entrancy guard, so overlapping scans on a large fleet fired the same trigger multiple times for one update. A full scan can take minutes, and the cron schedule, the docker-events debounce, the discovery-settle timer, and the startup timer could all start one while the previous scan was still running. Each overlapping call ran its ownwatch(), and a tag first seen mid-burst passed theonce=truehistory check in every one of them before any of them recorded it, so a trigger like Telegram fired once per overlapping scan for the same update.watchFromCron()is now single-flight: a request while a scan is running does not start a second one, it records that a rescan was requested, and exactly one follow-up scan runs once the current scan finishes, so a docker event that arrives mid-scan is not lost. A scan that never settles cannot wedge later ticks either: the in-flight scan is raced against a deadline, and when it fires every caller waiting on that scan, the one that started it and any that coalesced into it, resolves to an empty result and the next tick starts a fresh scan. The "Cron started" log line now also names what triggered the scan (schedule, docker-event, discovery-settle, startup, or maintenance-window). Reported by @tarzan77cz in #972.- A
once=truetrigger re-fired hours later for a tag update it had already announced, whenever a registry rate-limited the accompanying digest lookup. The notification-history hash for a tag-kind update included the digest and, when the digest was absent, the image'screatedtimestamp. A transientDigest watch failed (429)dropped the digest for that scan only, socreatedswapped into the hash on the failed scan and back out once the digest lookup next succeeded, and the once history stopped matching an update it had already sent. The hash for a tag-kind update on a container with digest watching configured is now keyed on the tag alone, so a digest lookup failing or succeeding does not change it; a container without digest watching configured keeps the originalcreatedfallback, and digest-kind updates stay keyed on the digest. Concurrent evaluations of the same history key are also now atomic within the process: the check and the reservation happen in the same synchronous step, so two overlapping scans evaluating the same result cannot both pass the check and both send, which is why one trigger could fire four times in the same millisecond while another skipped. After upgrading, a container with digest watching enabled, a tag update already pending, andonce=truewhose stored hash came from a scan where the digest lookup had succeeded re-notifies exactly once, because the stored hash format changed; every scan after that stays stable. Reported by @tarzan77cz in #972. - Two deprecation banner strings in the UI still described the legacy
DD_TRIGGER_*env vars and the curl-based healthcheck override as active with a future removal. Both were removed outright in v1.7.0;legacyConfigBodyandcurlHealthcheckBody(all 17 locales) now say so instead of pointing at a deadline that already passed. The curl banner's{bin}slot also rendered the compose migration snippet with a single$, which compose expands from the host environment before the container starts; doubled it to$${DD_SERVER_PORT:-3000}to match the corrected DEPRECATIONS.md snippet. - The marketing site's Get Started snippets deployed an instance that never became healthy. Neither the
docker runquick-start nor the hardened compose preset configured any authentication, so the instance ended up with zero registered auth strategies, which is a deliberate fail-closed state:/healthstays503forever and there's no way to log in. Both presets also skipped a/storevolume, so container state and audit history didn't survive a restart. The quick preset now setsDD_ANONYMOUS_AUTH_CONFIRM=true, the hardened preset setsDD_AUTH_BASIC_ADMIN_USER/HASH, and both mountdrydock-store:/store.
Documentation
- A docs audit turned up a batch of claims in the README, DEPRECATIONS.md, and the configuration/triggers/registries/API/monitoring/agents docs that no longer matched this tree's code. Compose examples that mangled a pasted argon2id hash and omitted the
/storevolume; translated READMEs with mistranslated provider names and a backwards roadmap pointer; a deprecation entry using the wrong removal-status label plus four missing DEPRECATIONS.md entries; hook, threshold, and rollback documentation describing behavior the code doesn't have (amajorthreshold letting digest-only updates through unfiltered, an allowlist that's exact-match rather than basename, a third rollback path that doesn't exist); trigger docs missing template variables and shared config keys; registry docs describing a routing mechanism and a rate-limit scope that don't exist and missing thepublic.ecr.awsrate row; API docs with a stale audit action list, wrong log-filter semantics, a nonexistent SSE event, and a rate limit bypass that isn't real; and feature docs conflating the docker trigger's health gate withdd.rollback.auto, describing a malformed signing key as a graceful skip when it crashes the controller, and framing a fail-closed/health503 as a transient startup state. All of it corrected against this tree's actual code, not against v1.8's.
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. Contributed by @hakan42 in #948.- 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.
1.7.0-rc.7 — 2026-08-29
Security
- Every cookie-less request authenticating with an
Authorization: Basicheader persisted a 30-day session row that was never reaped.requireAuthenticationcalledpassport.authenticate(getAllIds(), { session: true })unconditionally, and passport'sreq.logInregenerates and saves a session regardless ofsaveUninitialized: false, which only governs the auto-save on response end. Cookie-less Basic polling against/auth/useris the documented pattern for wud-card and Homepage integrations, so a single poller accumulated one session document per request. Worse, connect-loki never receives attl(onlyttlInterval), so LokiJS's own expiry sweep never ran and the store grew/store/dd.jsonwithout bound until it was rebuilt by hand.requireAuthenticationnow passessession: falsewhenever the request carries anAuthorizationheader, so passport still authenticates the request but skips the regenerate/save entirely; cookie-based logins through/auth/loginare unaffected. - A container could point another service's compose
image:at its own repository. The compose action picks which service to rewrite from the container's owncom.docker.compose.servicelabel and never compares the two images, so a container carrying a label for a service it is not an instance of had that service'simage:line rewritten to its own repository, and the operator's nextdocker compose upwould run that image with the victim service's volumes and privileges. The one runtime-versus-compose comparison there was,reconciliationMode, compares full references and so cannot tell a tag drift from a different repository, and in its defaultwarnmode it only logs. The write also lands before the runtime update and is rolled back only if that update throws, which it doesn't, because it is the labelling container's own recreate. Setting a label needs write access to the socket, which is root on plain Docker, so this is only a boundary in the shapes where container-create isn't: Portainer with bind mounts disabled for non-admins, rootless Docker with one watcher per user socket, an authz plugin scoping mounts per client identity. A repository mismatch between the composeimage:and what the container actually runs now throws before anything is written, in everyreconciliationModeincludingoff, because a different repository doesn't mean the file has drifted, it means the container is not that service. The check runs again under the selected compose file's lock against a freshly read effective multi-file chain, so concurrent external edits and images inherited from another chain file cannot bypass it; automatic backups are created only after that validation passes. Tag-only drift keeps its existing warn, block and off behaviour, and the Docker Hub aliases (docker.io/,index.docker.io/,registry-1.docker.io/,library/) plus digest-pinned references still compare equal to the same repository (#938). - The HTTP trigger's bearer credential was written to the log in cleartext on every registration.
Component.register()logs the configuration it was handed, and both layers that are supposed to scrub it missed this one field: the shared trigger redactor knewtoken,passwordandapikeybut notbearer, andHttphad nomaskConfiguration()of its own, so the base implementation handed the configuration back untouched. The credential sits nested underauth, which is also why the flatmaskFields()helper the other providers use would not have reached it. That log line is not only container stdout: it feeds the buffered stream behind the log API, so the credential was readable over HTTP as well as on disk.beareris now an infrastructure key in the shared redactor, which covers both the registration log and the/api/v1/triggersresponse, andHttpmasks the nestedbearerandpassworddirectly so the mask holds for any caller that readsmaskConfiguration(). GET /api/v1/debug/dumpreturned cloud registry credentials, chat bot tokens and agent secrets in the clear. The dump's redactor split a key on non-alphanumerics and required a whole segment to equal one of its known tokens, so every compound single-word field name slipped past it:SECRETACCESSKEY,ACCESSKEYID,CLIENTSECRET,BOTTOKEN,ACCESSTOKENandAGENTSECRETall came back verbatim, for ECR, ACR, Telegram, Matrix and the agent secret. The providers' ownmaskConfiguration()masks those fields correctly andstate.registries/state.triggersgo through it; the leak was the separateenvironment.ddEnvVarsblock, which is a flat copy of everyDD_*variable with__FILEsecrets already resolved to plaintext. The strong tokens now match anywhere in the key, while the short ones (pass,key,pat) stay segment-exact soCOMPASS_MODE,KEYFILEandDISPATCHare still readable. Pushover'suseris its user key rather than an address, so it is resolved by provider rather than by wideninguserfor everyone and hiding the SMTP mailbox with it. Any value carrying URL credentials is redacted regardless of its key, matching whatGET /containersalready did. This is the fourth fix to the same class of bug, which is why the docs now state the actual rule instead of "all sensitive values are redacted".- The debug dump served raw container environment variables, and no audit or rate limit sat on the route. It read containers through
getContainersRaw(), whose own comment says it is "for internal callers that do not return container data to users" — but the dump is downloaded and pasted into support threads.POST /:id/env/revealshows the same values behind a 10-per-minute limit and anenv-revealaudit row; the dump had neither, and it also missed the substring and URL-credential checks that path applies, soMINIO_SECRETKEYandREDIS_AUTHTOKENwere readable through it and not through the API. The dump now reads the same redacted clonesGET /containersserves, records adebug-dumpaudit entry on success, and is limited to 5 requests per minute. - Six registries matched lookalike hostnames and sent the operator's credentials to them. ACR, GHCR, GCR, Quay, LSCR and DHI decided whether an image belonged to them with a regex of the shape
/^.*\.?azurecr.io$/— an optional separator, and in four of the six an unescaped dot — soevilazurecr.io,evilghcr.io,ghcrXioandghcr.io.attacker.comall matched. Provider selection prefers a credentialed instance, and the request keeps the image's own host, so a container simply runningevilghcr.io/victim-org/private:1(a typo-squat, or a compromised compose file) was enough: no label needed. ACR then attaches a staticclientid:clientsecretBasic header unconditionally; the others mint a pull token from the operator's PAT at the real token endpoint and send it to the lookalike host. All six now use a sharedmatchRegistryHosthelper that accepts the base host exactly or as a dot-suffixed subdomain, which is what Hub, DOCR, Mau and Trueforge already did — the loose form was inherited from the upstream project's first commit, not chosen. Regional GCR (eu.gcr.io,asia.gcr.io) and per-tenant ACR (myregistry.azurecr.io) are unaffected.matchUrlPatternhad no callers left and is gone. - The command trigger's env sanitizer stopped shell injection but not argument injection. It replaced the metacharacters
`,$,;,&,|,<,>,(and)with_, and left spaces,-, and the globbing characters alone. The docs' own canonical example expands a variable unquoted, soimage_nameset to--registry evil.example/backdoor *reached an operator'sprintf "[%s]\n" $image_nameas--registry, thenevil.example/backdoor, then one word per file in the working directory — a flag and extra arguments injected into whatever the script does with them. Reachable without touching a container:POST /api/v1/triggers/command/:namedocuments caller-supplied container JSON as simulated test data, so any authenticated caller could set the field. Scalar container fields now also lose whitespace,*,?,[,],{,},~,\,"and'to_, and a leading-is dropped rather than replaced so the value cannot look like an option at all.container_json,containers_json,dd_titleanddd_bodyare exempt and keep their spaces and quotes, because the JSON has to stay pipeable intojqand the digest title and body are message text; the docs now say to double-quote every expansion and the examples do. The lifecycle hook sanitizer, which its own comment says matches this one, moved with it — hook values are all scalars, so nothing there legitimately carries a space. One visible consequence: add.display.namewith spaces now reaches a command script with underscores in their place. - An agent could still change how the controller pruned that agent's own containers, by naming an id it doesn't own. The ownership gate added for the bulk ingestion paths lives in
processAuthoritativeContainer, and all four callers prune before they ingest, so a foreign id in a sync frame still reachedpruneOldContainersunchecked. It could not delete or save a record on its own, because the prune only ever considers rows already owned by the reporting agent. What it could do is the #496 replacement match, which is keyed onagent::watcher::namewith the agent forced to the reporting agent: naming a container owned by another agent or by the controller was enough to turn the removal of one of the reporting agent's own rows into a replacement, which retains the update policy on the incoming record and skips the Home Assistant discovery cleanup for the one going away. The prune now filters its input through the same rule the ingest gate uses, silently, since the ingest pass logs each rejection a moment later.
Fixed
- A self-update could restart the controller while an unrelated container update was between removing the old container and starting its replacement. Self-update bypassed the optional global concurrency cap and shared no lock with a container in another Compose project, so both lifecycles could start together. The new controller then recovered the unrelated operation against its deleted container id, got a 404, and left the replacement in
Created. Self-update now takes a fair process-wide exclusive lifecycle gate: it waits for active updates to finish, later updates queue behind it, and a successful helper handoff keeps the gate closed until the process restarts. A failed or dry-run handoff releases the gate, and infrastructure-mode updates keep their existing global-cap bypass without becoming permanently exclusive. Reported with the operation records that exposed the race by @tarzan77cz in #930, fixed in #942. - A caller-supplied update operation id was inserted without checking whether it already existed.
POST /api/v1/triggers/:type/:nameand its per-agent variant accept anoperationIdin the body so the controller can thread its own id through to an agent-local route. Nothing checked it, and the store's id index is not unique, so reusing an id left two rows sharing it. Lookups return the newest of the two, so every later status write landed on the duplicate and the original stayedqueued, holding its container's active-operation gate for the full 30 minute TTL or until a restart reconciled it, and when the orphaned entry's turn came round it resolved to the other container's row and overwrote it with its own fields. The active-operation gate did not catch this because it only looks up operations for the container being targeted, so an id belonging to a different container passed straight through. Both routes now return 409 for an id that already exists, active or terminal, and nothing is inserted. - Self-update stranded itself on hosts that need the root break-glass pair, leaving nothing running under the container's real name. The transition helper is the drydock image with only its command overridden, so it still runs the entrypoint as uid 0, but its environment was built from scratch and carried only the
DD_SELF_UPDATE_*values. On a host where the Docker socket is owned by GID 0 — Docker Desktop, OrbStack, a root-owned socket, the population the FAQ tells to setDD_RUN_AS_ROOTandDD_ALLOW_INSECURE_ROOT— the entrypoint refused implicit root mode and exited 1, andAutoRemovedeleted the evidence. Nothing noticed: dockerode'sstart()resolves when the container execs, not when it exits, so the code logged "Helper container started" and reported success while the old container sat renamed to-old-<timestamp>, the new one had never been started, and the next attempt failed on the rollback cascade guard. The helper now inherits both variables from the running container's own inspected environment when both aretrue(both, because either one alone still gets refused), and a watchdog inspects the helper a few poll intervals after starting it: if it has already exited, or auto-remove has already reaped it, the rename is rolled back and the failure is reported with the exit code instead of being swallowed. Reading the pair from the container spec rather thanprocess.envkeeps it working for an agent-side spawn. - An update that had already passed its health gate could still be rolled back, and with
AutoRemoveset that could leave nothing running at all. The Docker update strategy renames the old container out of the way, creates and starts the new one, gates on health, and only then removes the old one. That last removal rethrew anything that was not a "no such container", which sent an already-finished update into the rollback path: the health-verified new container was stopped and force-removed, and the old one renamed back and restarted. The errors that reach it are ordinary Docker hiccups, not failures of the update — the 10 second timeout on waiting for anAutoRemovecontainer to disappear, a 409removal of container <id> is already in progress, a 500 from a slow overlay unmount or an anonymous-volume delete under IO pressure. When the old container hadAutoRemoveset the outcome was worse than a rollback, because Docker may already have deleted it by the time the rename back ran, so the rollback failed too and the user was left with no container at all. Cleanup after the health gate can no longer roll anything back: the new container stays running, the operation is markedsucceeded, and the leftover<name>-old-<timestamp>container is logged as a warning and recorded on the operation so it can be pruned. The two tests covering this pinned the old behaviour — they were written for branch coverage rather than as an assertion about what should happen — and now assert the update survives. - Every sanitized log line kept the visible half of an ANSI escape.
sanitizeLogParamstripped control characters first, which deletes the escape byte the ANSI pattern then looks for, so the pattern could never match andhello\x1b[31m worldcame out ashello[31m worldinstead ofhello world. Not an injection hole, since the escape byte itself was always removed and a terminal can't act on the remainder, but the module's own contract said it removed ANSI sequences and it didn't, the residue is in every log line, audit detail and close reason that carries user- or registry-supplied text, and the test asserting the behaviour had the residue written into its expected value. ANSI now goes first, and the pattern covers both ESC-prefixed and C1 CSI sequences across the whole CSI grammar rather than just color sequences ending inm, so a cursor or clear-screen sequence doesn't leave[2Jbehind either; the remaining C1 control range is stripped with the other controls. The scanner-asset error path had grown its own local copy of the ANSI regex to work around this, with a comment explaining the ordering problem; that copy is gone (#938). - Any repository with more than 1000 tags was silently truncated at the first page, and on AWS ECR Public it failed the watch outright. Tag listing captured the
Linkheader the registry returned and then discarded it, rebuilding the next-page cursor by hand aslast=<the previous page's last tag>. Pagination cursors are opaque under the OCI distribution spec, so that only works on registries that happen to accept a literal tag name there. ECR Public does not: it answers the rebuilt cursor with405 Method Not AllowedandInvalid parameter at 'NextToken', which is not in the retryable set and so surfaced asError when processing (Request failed with status code 405)for that container, every cycle. Found by running the published rc.6 image against a real Docker host:public.ecr.aws/supabase/postgreshas 1502 tags, page one returns 1000, and the installed tag existed only on the page that never loaded. Sibling images on the same registry were unaffected because none of them cross 1000 tags, which is why this reads as a per-image fault rather than a registry one. The cursor is now followed as given. A cursor pointing at a different origin is refused rather than followed, because the request carries registry credentials, and tag listing stops after 50 pages so a registry that always reports another page cannot loop forever. Coverage was 100% on this code the whole time: the existing tests drove the pagination loop withlink: 'next'andlink: 'rel="next"', neither of which is aLinkheader, so the loop condition saw a truthy value and the cursor-building path was never given anything real to parse (#927). - AWS ECR Public was rate-limited at the generic default and drew 429s from ordinary use. The per-host token bucket has tuned entries for GHCR, Docker Hub and the GitHub API, but ECR Public fell through to the more permissive 5 requests per second default despite throttling anonymous reads harder than any of them. A compose stack pulled from a single ECR namespace, Supabase self-host being the common case, puts a dozen containers on that host in one cycle and produced widespread
429warnings. It now uses the same 2 per second and burst of 10 the other strict registries get.
Performance
- Turning security scanning on could disconnect every open browser tab once per scan cycle. The
dd:container-addedanddd:container-updatedserver-sent events carried the whole container record, which includes the per-CVE arrays for both the running image and the update candidate (capped at 1000 entries each, around 340 bytes per entry), the SBOM documents and the cosign verification blocks. That is roughly 340 KB for a single container against the 256 KB a client is allowed to have pending while its socket is backed up, so a scan cycle touching two or three replicas of a vulnerable image dropped every listener, and the reconnect replayed the same oversized events out of the ring buffer and dropped them again. The container list endpoint has stripped those fields since it was written; the event path never did, and there was no projection between the store and the wire. Both lifecycle events, and the removal event, now go through that same projection, on the wire and in the replay ring, so they carry exactly what the list carries: scan status, severity summary, blocking count, block severities and scan time. No display loses anything, because the UI never read the arrays off the event — vulnerabilities, SBOMs and signatures are fetched from their own endpoints, and the security detail is refetched ondd:scan-completedregardless.
Documentation
- The public feature comparison was dated March 2026 and measured drydock against a field that no longer reflects what people are choosing. One table ran against WUD, Diun and two archived projects, so the three tools that draw the direct comparisons today, Arcane, Komodo and Dockhand, appeared nowhere in it. It is now two tables, update managers and management platforms, so neither gets wide enough to stop being readable, and every cell traces to the 2026-08-29 competitive audit. Corrections that came out of that audit: WUD ships threshold filtering and has 17 notification providers and 12 registries, not 16 and 13; Diun's semver and Home Assistant support are partial rather than full, and it does have Prometheus metrics; Watchtower's Shoutrrr count is about 20. Drydock's own rows now include the two it loses, RBAC and a real pending-approval queue, rather than only the ones it wins. A note under the first table records Watchtower's December 2025 archival, its v1.7.1 (November 2023) last release, and the unofficial community fork still shipping releases. The same table is mirrored into all six translated READMEs.
- Seventeen rows across the comparison surfaces on the site disagreed with the audit, and two of them undersold drydock or a competitor. Komodo ships maintenance windows, and Dockhand ships Prometheus metrics and Home Assistant MQTT, all three of which were listed as absent. Komodo and Dockhand both have a limited dry-run rather than none, and Dockhand has no rollback on failure where the
/compareindex called it partial. Diun's semver and MQTT rows claimed parity where the audit found partial support. Dockge's stable releases have been stalled since March 2025, so it is no longer listed as plainly maintained, and it ships 29 UI languages, not "30+". Portainer does have a distributed agent architecture, which the homepage teaser denied. And the Dozzle page still called drydock's resource monitoring planned when it shipped in v1.5.0. The WUD and Watchtower provider counts now agree with the README, and Drydock's 23 registries are 11 more than WUD's 12, not 10 (#938). - The README roadmap table listed scoped rotatable API keys under v2.0+. They are v1.8.0 work now, where the SQLite store migration provides the persisted key table they depend on, and the per-update approval queue is listed with them.
Chore
- A compose rollback that itself fails was mapped to a terminal state no test ever drove.
getComposeRollbackTerminalPatchturns arollback-failedcompose outcome into{status: failed, phase: rollback-failed}, but the whole branch sat under av8 ignoreclaiming it was integration-covered through compose recovery, and nothing drove that status throughrunContainerUpdateLifecycle: the one lifecycle test usedrolled-back, and the compose-side test stops at the throw. A regression mapping it torolled-back, or letting it fall through to the duplicate-updateexpiredreclassification, would have shipped at 100% coverage. The second ignore in that path, over the rollback-state persistence, was stale the other way: the rolled-back lifecycle test has been exercising it all along. Seven new lifecycle cases now cover both statuses plus the blank-reason, absent-lastError, unrecognized-status, no-container-id, no-persistence-dependency and watcher-only-identity paths, which retires seven of the file's thirteenv8 ignoreblocks. The terminal patch'slastErroris also typed as always present, which it always was, so its dead fallback is gone. The six ignores left each name a specific unreachable input instead of claiming coverage that lives somewhere else.
1.7.0-rc.6 — 2026-08-29
Added
Security
- An authenticated agent could still pre-empt a controller-local container before the controller wrote it. The ownership check landed for #904 only covers a container id the store already holds; a brand-new id had no check at all, so an agent could insert a record under a watcher name matching the controller's own local Docker watcher, race the controller's own watch cycle, and have
docker-trigger.ts's trigger routing (which selects purely on the storedagentfield) hand that container's future lifecycle actions to the agent instead of the controller. A no-record insert claiming a watcher name the controller registered directly, rather than through an agent, is now rejected. - That #904 fix only covered the two incremental
dd:container-*event handlers, not the bulk ingestion paths. Handshake, the watcher-snapshot fallback, on-demandwatch/watchContainer, and edgehandleContainerSyncall reachbuildContainerReportthroughprocessAuthoritativeContainerwith no ownership check in between, so an agent could still name an id owned by another agent or by the controller's own local watcher in its next routine snapshot and have the store reassign it, repeatedly, at its own cadence.processAuthoritativeContainernow runs the same no-existing-record and different-owner checks before ingesting. It deliberately skips the third checkcanMutateContaineralready has — rejecting awatchermismatch against the stored record — because bulk ingestion is exactly how a legitimate watcher rename propagates (an operator renaming aDD_WATCHER_<NAME>_SOCKETkey), and rejecting on that mismatch here would silently stop every future report for that container id from landing, with nothing left to prune or retry it. - Thirteen registries authenticated for the version check and then pulled anonymously. The pull-credential builder handled a login and password pair and a username and token pair, but had no branch for a configured
authvalue, while the sibling that builds lookup credentials did. So Hub, Custom, DHI, DOCR, Harbor, Gitea, Forgejo, Codeberg, Nexus, Artifactory, Alibaba CR, OCIR and IBM CR detected an update correctly and then attempted the pull with no credentials, which fails outright against a private registry and consumes the anonymous quota against a rate-limited public one. The value is decoded into a credential pair rather than passed through, because the Docker daemon reads the pair and never inspects anauthsub-field, and it is split on the first colon only since a password may contain one. A malformed value now fails closed rather than returning nothing, which would have silently reproduced the same anonymous pull for a different bad input. - A watcher failure on an agent returned the thrown message to the caller. Two agent handlers returned the raw message for anything that was not an
Error, which is the branch a rejected upstream request lands in with its context attached. Both now return the fixed generic message unconditionally; the detail still reaches the server-side log through the existing sanitizer. - A bulk security scan request had no cap on how many containers it could queue.
parseBulkScanBodyvalidated thatcontainerIdswas an array of non-empty strings but accepted it at any length with duplicates intact, and each accepted id drives a vulnerability scan in the handler, so an authenticated caller could send tens of thousands of ids, or the same id repeated, and make the controller do that much scanning work from one request. The siblingparseWatchContainersBodyalready caps and dedupes for the same reason; bulk scan now enforces the same 200-entry limit, checked before the per-element loop so an oversized array is rejected without walking it, and collapses duplicates with the sameSet-based dedup that silently drops a repeat instead of erroring. - Error responses could carry upstream credentials back to the caller. Eight handlers across the trigger, container and agent APIs interpolated a raw thrown message straight into the response body. That matters because of what those messages contain: a trigger failing against an upstream service throws with the request context attached, so an
Authorization: Bearerheader and a credentialed webhook URL both reached the client in a 500 that any authenticated caller could read. All eight now route through thesanitizePreviewErrorReasonscrubber that already existed for exactly this and had only ever been wired into the preview path. The two sites that deliberately tell an empty message apart from a missing one keep that distinction and scrub only a truthy string, so their synthesized fallback still fires. - The preview-error sanitizer missed credentials embedded in a URL path segment, not just in headers or userinfo. Telegram builds its API URL as
https://api.telegram.org/bot<credential>/sendMessageand IFTTT ashttps://maker.ifttt.com/trigger/<event>/with/key/<credential>, so a bot credential or Maker key sits in the path itself rather than behinduser:pass@or anAuthorizationheader. This is a latent gap, not an observed leak: nothing currently puts either provider's request URL into anerror.message, but a future throw that includes request context would carry the credential straight through.sanitizePreviewErrorReasonnow redacts the credential segment for both, plus the same shape in Discord webhook URLs (https://discord.com/api/webhooks/<id>/<credential>) found while checking the other providers, all anchored to each provider's own fixed host and path rather than to "any long opaque string" so an ordinary registry path segment, including a manifest digest, is left alone. - The API end-to-end suite could not detect an unmasked secret. Five assertions in
api-registry.featurechecked masked credential fields against.*, which passes for any value including a completely unredacted one.Component.mask()returns the literal[REDACTED], so all five now assert that, matching what the authentications feature already did.
Fixed
-
Row selection never highlighted anything in seven views. Servers, Registries, Watchers, Audit, Auth, Notifications and Triggers all passed an
active-rowprop to the shared data table, which declaresselectedKey. Vue drops an undeclared prop onto the root element and carries on, so the selection styling was a no-op in every one of them. The test suite was fine with it because each view's table double accepted the invented prop and echoed it back into an attribute the spec then asserted against, so the specs were checking the stub's own invention rather than the real component's contract. Coverage was 100% throughout. The doubles now require the real prop and each row-click test asserts the value actually changes. -
White text on a light accent, as low as 1.37:1. The trigger test button, the config profile avatar and the app-layout user menu avatar hardcoded white over a gradient whose far stop is the info or success color. Flattening to the solid primary token with its computed foreground clears 4.5:1 in all twelve themes, worst case 5.07:1. Keeping the gradient could not be fixed with the correct token, because the foreground token is computed against the primary color and not against the other stop. The contrast gate now also scans the sources for a tag combining white text with a primary-color reference, since token math cannot see inside a template, which is how this survived the previous contrast pass.
-
The notification outbox showed whichever tab's data resolved last rather than whichever tab was selected last, so switching quickly left the other tab's rows on screen. Guarded with the request-id pattern the triggers and notifications views already use.
-
Status was unreadable or untranslated in five places. The icon-only status indicators in Servers, Registries and Watchers had no accessible name, so state was conveyed by color and glyph alone. Auth, Audit, the container log header and the dashboard security widget interpolated raw enum values, so a non-English reader saw
active,success,errorandCRITICALin English regardless of locale. Five new English keys, translated into all 16 locales in the same change rather than left for a sync to fill with verbatim English. -
Removing a container while its full-page detail view was open left a blank screen. Two independent causes, either of which still reproduces alone. Closing the panel nulled the selection without clearing the full-page flag, so the layout stayed in full-page mode with nothing to render. And the watcher meant to notice the removal used a getter source: a watch with a getter source fires on reference change, not on in-place mutation, and the SSE pipeline removes by splice on purpose to avoid per-row invalidation, so the sync never ran and the selection kept pointing at a deleted object.
-
Two dashboard watchers missed every in-place SSE update. The pending-update rows and the operation holds both watched a bare ref, so neither ran while the patch pipeline mutated rows in place, leaving ghost update rows and holds that never released. The first takes a length-augmented source; the second watches a per-row fingerprint, because the fields it reads are patched without touching array length. Neither uses a deep watch, which would re-fire on every unrelated field mutation of every row and defeat the point of patching in place.
-
A malformed
authregistry value could still decode into credentials that looked valid.decodeAuthCredentialsfed the configured value straight toBuffer.from(auth, 'base64'), which silently drops characters outside the base64 alphabet instead of throwing:dXNlcjpwYXNz!still decodes touser:pass, the trailing!just discarded. That defeats the malformed-auth throw this decode exists to enforce, since a corrupted or truncated value can still land on a valid-lookinglogin:passwordsplit and fall through as real credentials instead of being rejected. The value is now re-encoded and compared against the original before decoding; any character the decoder ignored makes the round trip diverge and the throw fires. -
A rare upstream build race could fail a release cut with nothing to recover from. An open BuildKit bug (moby/buildkit#7089) makes a canceled-then-retried operation inside a single multi-architecture build get the QEMU emulator path prepended twice, so the build dies instantly with
Invalid ELF image for this architecturebefore producing any output. It fired once in CI and has passed on every run since, which is what a scheduler race looks like. The multi-architecture smoke build now retries, and the release cut gains a full-build retry for the specific case where the first attempt produced no digest at all. That case matters because the existing manifest retry needs a digest already sitting in a registry and hard errors without one, so before this the race firing during a real cut meant a hard failure discovered live in the release window. -
A long enough error killed the process instead of closing a log stream. The container log stream used the raw thrown message as the WebSocket close reason. The library throws synchronously once that exceeds 123 bytes and never catches it, and the stream is invoked as a floating promise with no rejection handler, so a registry error past the limit took the whole process down. Both remaining sites now truncate through the helper the file already had and one sibling was already using. The second of them was never a crash: it sat inside a
try/catchand so swallowed the error, and because the socket is marked closing before the throw, the close frame was never sent and the socket stayed open. -
Checking for updates again reset the image-maturity clock. Four agent entry points processed the authoritative container list and then pruned, so a container that had just been re-inserted was compared against a store that still held it. Pruning now runs first at all four. The watch path was also missing the zero-container guard the handshake path has, and needs it, because the Docker watcher returns an empty report array both when there genuinely are no containers and when enumeration threw and was swallowed, and the response cannot tell the two apart, so a transient socket error on an agent pruned every container for that watcher. (#565)
-
The docs search returned every archived version at once. A search for a common term returned about 1600 hits spread across five documentation versions, ranked with the oldest changelog first, so a reader on the current docs was served entries describing a configuration prefix this release removes. Each page is now indexed with its version and the search dialog scopes to the version being read, defaulting to the current one elsewhere.
-
Two badges on the homepage were blocked by the site's own content-security policy. The best-practices and coverage image hosts were missing from
img-src, so both rendered broken and logged a violation in any enforcing browser. Exactly those two origins are added, and the policy's full source list is now pinned by a test. -
The weekly DAST scan has never completed. The active scan alone consumed 39 minutes 46 seconds of the job's 40 minute budget, so the job was cancelled the moment the second scanner's steps opened: the second scanner has never run, the report file was never written, and the severity gate and SARIF upload both ran against a file that did not exist. The two scanners now run as separate parallel jobs with their own budgets, so neither can starve the other and a blown budget names the scanner responsible.
-
The action-filter documentation described shipped behaviour as scheduled. The callout still framed the include and exclude filters becoming hard blockers as future work, three release candidates after it shipped, so an operator reading it concluded they still had time to migrate labels while their update button was already being refused.
-
The
PUT /api/settingsdeprecation pointed consumers at a dead endpoint. The deprecation warning logged on every call, and the matching entry in deprecations, both named the unversioned/api/settingspath. v1.6.0 removed the unversioned/apialias and settings is not one of the four wud-card compatibility endpoints exempted from that, so anyone following the migration advice moved off a deprecated method and onto a path that returns 410. Both now name/api/v1/settings, and the deprecations entry says outright that the unversioned path is not an alias for it. -
The UI is properly translated in all 16 non-English locales. Every locale already had every key, but between 17% and 29% of the values in each were still the English source text showing through, so large parts of the container list, the update and rollback dialogs, the search palette and the notification outbox rendered in English regardless of the language selected. 2109 strings are now actually translated. The 61 left alone are the ones with nothing to translate, such as
"{registryHost} — {error}", and category labels whose only word is a product name each locale already keeps in English. -
The image-maturity badge showed no age outside English.
Detected {duration} agohad replaced an older bareNEWlabel in the English source, but all 16 locales still carried a translation of the old wording, so the badge silently dropped its duration for every non-English user. Retranslated in all 16, and a new placeholder-parity gate now fails the build when a translated string loses or invents an interpolation placeholder, which is the class of bug that hid this one: it breaks neither JSON parsing nor key parity. -
A non-numeric
tailorsinceon the log and agent endpoints put NaN into the ring-buffer read instead of rejecting it. Both now validate and return 400, matching what the audit endpoint already did. No clamping, because the log buffer caps at its own maximum regardless of what is asked for, so an oversized value cannot cause unbounded work the way the audit limit could against a growing collection. -
An empty
tailorsince(?tail=) was read as absent rather than invalid, on all three log endpoints. Every one of them guarded withif (!value) return undefined, and an empty string is falsy along withnullandundefined, so a supplied-but-empty value skipped the 400 above it and was silently forwarded downstream asundefinedinstead of being rejected as malformed input. The controller's log and agent endpoints and the agent-mode API's own log endpoint all now treat only a genuinely missing parameter as absent; an empty string proceeds to the integer-shape check like any other supplied value and is rejected. -
A
limitoroffseton the audit endpoint with a numeric prefix, such as?limit=25logs, validated as the leading digits instead of being rejected.Number.parseIntstops at the first non-digit rather than requiring the whole string to be a clean integer, the same bug already fixed on the log and agent endpoints'tail/since. Audit's contract falls back to its default instead of erroring on a bad value, so that stays: an unparseablelimit/offsetnow behaves the same as an absent one and falls back to the default, rather than switching audit over to a 400 like the other three params. A value that overflowsNumber.MAX_SAFE_INTEGERgets the same treatment, closing a related precision-loss gap where a huge digit string used to silently clamp to 200. -
A duplicate Portwing agent key was reported as a server error. The conflict now returns 409 with the conflict message, carried by a typed error rather than by matching on the message text, which is how the update path already does it.
-
The Dutch curl healthcheck banner named the wrong thing. Its title rendered the CLI tool
curlaskrulstatus, which in Dutch is a decorative curl. -
Drydock offered older LinuxServer builds as updates.
v8.9.0-ls101was reported as having an update available tov8.9.0-ls99, and the same for0.5.0b3.dev101-ls250down to0.5.0b3.dev99-ls235and4.0.19.2979-ls322down to4.0.9.2244-ls257. Three separate mechanisms, all landing in the same comparator. The-lsNNNcases were spec-correct semver behaving wrongly for the shape: an alphanumeric prerelease identifier compares lexically, so"ls101" < "ls99"on the first differing character. The four-component case was worse and not truncation:semver.clean('4.0.19.2979-ls322', {loose: true})mis-splits the19and returns the corrupted4.0.1-9.2979-ls322, while the same call on4.0.9.2244-ls257returns null and falls through tocoerce()for a clean4.0.9, so the two sides were never compared through the same parse path at all. Candidates whose version cores tie are now compared on the numeric counters embedded in an otherwise identical suffix template, generalised to any trailing counter rather than special-cased to LinuxServer, and a pair whose ordering cannot be established safely is declined rather than guessed. Note this was visible on default settings too:DD_WATCHER_DOCKER_TAG_FAMILY=loosewas needed for the downgrade to be offered as actionable, but the informational "newer version available" insight shared the same admission logic and showed the older tag without it. (#918) -
The action-policy badge and its tooltips rendered in English in every locale. The
Auto/Manual/Blockedbadge and the three state tooltips behind it, on both the update-status panel and the per-trigger rows, were added to the English catalog only, so eleven strings fell back to English for every non-English user from the release that introduced them. Translated in all 16 locales. The key-parity gate now fails on a key that exists in English and not in a locale, which is what should have caught this: it previously tolerated the gap on the grounds that Crowdin would fill it after the push, and what Crowdin actually fills it with is verbatim English. -
The weekly Crowdin sync no longer reverts the six translated READMEs to English.
README.mdwas registered as a Crowdin source, so every sync round-tripped the translated copies through the project. Turning off content segmentation in #804 to fix an earlier corruption renumbered every segment, which orphaned the stored translations from the strings they belonged to, and Crowdin exports source text for anything it cannot match. The next sync opened as a pull request deleting 1392 lines of translated README, caught only byreadme-translations.test.ts. Crowdin no longer mapsREADME.mdat all: the translated READMEs are hand-authored in-repo and asserted phrase by phrase at every cut, and nobody was translating them in Crowdin. The UI locale catalogs are unaffected, since segmentation never applied to JSON. (#919) -
Restoring the maturity-policy stash at startup no longer ignores the size cap, or evicts the wrong entry afterwards. The durable
updatePolicyRetentionCacheadded in 1.7.0-rc.2 restored every non-expired persisted record straight into memory without checkingDD_UPDATE_POLICY_RETENTION_CACHE_MAX_ENTRIES, so lowering that limit between restarts came back over it and the extra entries stayed live until the next stash trimmed them. Restoring also used the store's document order, which has nothing to do with stash age, and the eviction that runs when the cap is hit reads that order as its LRU, so the first eviction after any restart could drop the newest entry and keep the oldest. Startup now restores oldest first and enforces the cap against both the in-memory cache and the persisted records. (#565)
1.7.0-rc.5 — 2026-08-27
Security
- An unauthenticated Portwing hello could throw outside the callback error boundary. A non-string compatibility value in the hello payload reached
.split()before anything validated it. The payload is now validated before parsing. (#904) - An authenticated agent could write to containers it does not own. Update and removal accepted globally keyed container IDs belonging to another agent or to the controller; ownership is now enforced at the ingestion and removal boundaries. (#904)
- Runtime environment redaction missed
*_PATvalues and credentials embedded in URLs. Both are now detected and redacted in the debug dump. (#904) - Rejected-origin diagnostics could be used to amplify debug logging outside the route limiter. The diagnostic path is bounded and rate-limited. (#904)
- Credentials survived redaction in scheme-relative container URLs. A value like
//user:pass@host/pathhas no scheme to anchor it, so parsing threw and the raw string was returned unredacted. Those values now parse and redact like any other URL. (#904)
Accessibility
- Dark themes now meet the WCAG 2.2 contrast minimum for normal text. Secondary and muted text, the tone colors (warning, caution, danger, success, info) and the two accent colors all failed 4.5:1 against surfaces they are actually painted on, across all six dark themes. Worst measured pairs were muted text on an elevated surface at 1.57:1 and secondary text in a warning dialog at 1.33:1. Token values are raised to clear 4.5:1 against the worst real pairing for each, keeping each palette's hue. Colors used only for borders, focus rings, icons and toggle fills are held to the 3:1 non-text target instead of being raised needlessly. (#850, #865)
- Text in the update-status condition rows is readable again. Each nested condition row painted the same muted tone background its parent section had already painted, compounding two tints into a surface no text color could clear without washing out the whole muted/secondary hierarchy. The rows now use the card surface they already carried, which no value change could have fixed. (#850)
- Toast messages meet the contrast minimum. Toast surfaces mixed their tone color at 25%, leaving the tone-colored text on them between 3.65:1 and 3.94:1 in five of the six dark themes. They now use the same 15% mix as every other tone surface in the app. (#850)
- Primary button labels are no longer white on a light accent. The login and notification-test buttons hardcoded white text on the primary color, measuring as low as 1.45:1. A new
--dd-primary-fgtoken picks black or white per theme, whichever clears 4.5:1 against that theme's primary. (#850)
Fixed
- Startup recovery could mark an untouched container as a successfully updated one. A
prepare-phase operation interrupted before Docker did anything was reconciled as though the replacement had happened, so a container that never changed was recorded as updated. Recovery now compares the persisted old and new container identities: an untouched original fails recovery, a real replacement still succeeds. Startup also stopped expiring destructive in-progress Docker updates before anything had inspected the runtime, and recovered dependency groups rehydrate their completed-upstream context instead of running a restart-only dependent as a full update. (#904) - A healthy agent with a large inventory could never reconnect. When the cached watcher replay exceeded 256 KiB the controller rejected the agent before acknowledgement, permanently, so the biggest deployments were the ones that could not come back. The stream now stays open and sends only the acknowledgement, and the authenticated full-inventory handshake supplies the state. Latest-only coalescing also no longer overwrites a protocol-critical snapshot belonging to a different watcher. (#904)
- Backups could be selected or pruned across unrelated containers that shared a name. Ownership keyed on the container name alone, so the same service name under two agents, two watchers or two compose projects collided. Backups now carry a stable scoped identity, and legacy identity-less backups stay selectable only while a single active identity owns that name. (#904)
- Rollback restored a mutable tag instead of the digest recorded with the backup, so a rollback could land on whatever that tag pointed at by then rather than the image the container was actually running. (#904)
- A slow SSE client could make drydock retain unbounded queued bytes. The main broadcast stream, the container and summary stats streams, and the agent controller fan-out all ignored
res.writereturning false. All three now deliver bounded, drain-aware latest-state updates and clean up on close, and the agent fan-out has a connection cap. (#904) - Concurrent scans could cancel each other. The digest single-flight entry captured only the first caller's cancellation state and transient-retry option, so one caller aborting could kill an unrelated caller's scan, and a later waiter could join an entry whose controller was already aborted. Waiter cancellation is isolated, retry requirements are aggregated across live waiters, and aborted entries are retired before a new waiter is admitted. A cancelled scheduled scan also now propagates cancellation into the scanner instead of rejecting the wrapper and letting later runs accumulate. (#904)
- Docker event chunks were parsed concurrently against a shared buffer, racing both the parser state and the writes that followed. Parsing and application are now serialized. (#904)
- A successful container action returned 500 when the follow-up refresh failed. The best-effort post-action inspect or store refresh was allowed to overwrite the outcome of an action that had already succeeded. Success is now authoritative and refresh degradation is reported separately. (#904)
- Paginated container lists were only sorted within a page. The default name sort ran after the store had already paginated, so ordering was not global across pages. (#904)
- A restart suppressed batch-completion events for updates that were still in flight. Volatile batch membership was cleared on startup, so recovered operations completed silently; membership now rehydrates from the persisted operations. (#904)
- The system-log stream limiter fell back to an empty identity instead of the client IP when no configured key applied. (#904)
- An unsupported agent transport was admitted and then failed asynchronously at watcher lookup during a dependency restart. It is now rejected at admission. (#904)
- A backpressured controller SSE client lost every event but the newest. Only one pending payload was retained while the socket drained, so anything that arrived behind it was silently replaced. Pending payloads are now queued in order and flushed on drain, with the byte cap applied across the queue so a genuinely slow client is still dropped. (#904)
- Tearing a watcher down while it was setting up its Docker event listener could resurrect it. Deregistration landing during the client-recreation or auth preflight, or during the
getEventsrequest itself, left a late callback to fire against already-cleaned-up state: it registered a stream and attached handlers to a watcher that was being removed, or scheduled a reconnect for one that was already gone. Superseded and deregistered listener setups now bail out at each of those points, and a stream that arrives too late is destroyed rather than adopted. (#904) - An update that never started was reported as a failed update. An operation that could not be resolved before the runtime was reachable was marked failed rather than expired, so users saw a failure for an update that had not run. (#904)
- The Crowdin sync workflow failed on every push to a dev branch that was not the newest one. The base resolver always picked the highest
dev/vX.Yon origin regardless of which ref triggered the run, but the Crowdin action had already downloaded translations into the working tree by then, so checking out a different branch over them died withYour local changes to the following files would be overwritten by checkout. A push to adev/vX.Ybranch now targets that branch directly; scheduled and manually dispatched runs keep the highest-wins lookup and its default-branch fallback. (run 33047712284)
Documentation
- The homepage FAQ described
DD_TRIGGER_*as still deprecated, past its actual v1.7.0 removal. It said the prefix was "a deprecated alias scheduled for removal", but v1.7.0 removed it outright: anyDD_TRIGGER_*environment variable now fails startup with an error naming every offending variable and its exactDD_ACTION_*/DD_NOTIFICATION_*replacement (see deprecations). The FAQ answer now says so, and a new test asserts the copy can't drift back to describing an already-removed feature as merely pending removal. - Archived changelog snapshots kept relative
./DEPRECATIONS.mdlinks that don't resolve on the docs site.content/docs/v1.x/changelog/index.mdxfiles are frozen copies of the rootCHANGELOG.md, which uses relative links, and the docs sync script's per-version rewrite only handled absolute/docs/...targets. So the relative-link rewrite that already ran for the freshly generated current-version changelog never reached the frozen archives.rewriteDocsLinksInDirectorynow routes changelog files through the same rewrite, fixing the two broken links in the v1.5 archive, and any future archive with the same pattern, without touching non-changelog content.
Chore
- Two real production modules were excluded from the coverage gate, and a third exclude entry never matched anything.
query-values.tsandsorting.tsare used bymaturity-filter.ts,container.tsandcrud-context.ts, but were carved out of the 100% coverage requirement the repo otherwise enforces. Both are back in:sorting.tswas already fully covered indirectly, andquery-values.tsneeded a new direct test for the repeated-query-param (array) path, which is real Expressqsbehavior nothing exercised before. The third entry namedTrueforge.tswhile the file on disk wastrueforge.ts, so it was a silent no-op on case-sensitive CI, and dropping it surfaced what the wrong case was hiding.registerComponent()resolves a provider's module by testing for aProvider.ts-cased file, and on a case-insensitive filesystem that test also matchedtrueforge.ts, loading it under a second differently-cased specifier the direct unit tests never touch, so full-suite coverage read it as partly uncovered even though the module is fully tested. Every other provider file already follows the PascalCase convention, so the file and its test are renamed to match and the convention check now resolves identically on every filesystem. - Removed the now-stale CVE-2026-14456 (openssl/libssl3/libcrypto3) Grype suppression. It was scoped to the Alpine 3.24
opensslpin at3.5.7-r0and named its own removal trigger: bump the Dockerfile'sopenssl=pin to 3.5.8 or newer. That bump already landed in #881, soopensslis now pinned at3.5.8-r0, and a live Grype scan of that exact package and version confirms zero matches for this CVE or any other finding. The suppression no longer applies.
1.7.0-rc.4 — 2026-08-26
Security
- Base images bumped to clear six HIGH OpenSSL CVEs. The
node:24-alpineandalpine:3.24digest pins are rolled to the current upstream rebuilds, moving the shipped OpenSSL from 3.5.7-r0 (flagged by Grype for CVE-2026-14457, CVE-2026-18798, CVE-2026-54874, CVE-2026-63072, CVE-2026-63075, and CVE-2026-63076) to 3.5.8-r0. (#881) - The demo site sends the full security-header set.
demo.getdrydock.comwas failing the weekly ZAP baseline scan with six WARN-NEW alerts.apps/demo/vercel.jsonnow sendsX-Content-Type-Options,Permissions-Policy, a credentiallessCross-Origin-Embedder-Policy, and a same-originAccess-Control-Allow-Origin; the two cache-control alerts are allowlisted in.zap/rules.tsvunder the same static-SPA rationale as the existing 10049 entry. (#878)
Fixed
- WebSocket log-stream connections behind a TLS-terminating proxy no longer 403 when
X-Forwarded-Protois absent. With trust proxy enabled,isOriginAllowedfell back to the local socket'sencryptedflag whenever the proxy omittedX-Forwarded-Proto, which is plain HTTP on a backend behind TLS termination, so a browser'shttps://Origin never matched and every WebSocket upgrade was rejected while REST traffic worked fine. The protocol is now treated as unknown (and skipped from the comparison) in that case instead of being inferred from the local socket; host validation is unaffected. (#867, #868) - A
ws/wssvalue inX-Forwarded-Protono longer rejects the WebSocket upgrade. Traefik forwards the upgrade's client-facing scheme aswssrather thanhttps(traefik/traefik#6388), which the origin check treated as an unsupported protocol and hard-rejected — so the trust-proxy fix above still 403'd behind a default Traefik setup.wsandwssnow map tohttp:/https:for the Origin comparison; genuinely unknown protocols are still rejected. (#867, #887) - Startup no longer crashes with
EPERMwhen the store volume forbidschmod. The permission tightening added in 1.6.0 now warns and continues onEPERM/EACCES/ENOTSUPinstead of throwing, so mounts that rejectchmod(NFS/CIFS volumes, non-root containers, some volume drivers) no longer take the whole process down at startup; a genuinely read-only volume (EROFS) still fails fast at startup, because nothing could be persisted there anyway. (#874, #886) - Tag suggestion no longer ranks a bare integer build-number tag above a real dotted version. A bare integer tag (e.g.
168) coerces viasemver.coerce()into a fake168.0.0, which previously outranked a real release like1.43.3— forlinuxserver/plex, this meant the suggested-tag badge and, with a permissivedd.tag.includefilter, the actionable update candidate itself could point at a destructive downgrade. Bare integer tags are now only ever ranked among themselves (never against a real dotted version, and never at all when the population contains any other non-integer version signal, such as a prerelease-only or coercion-lossy tag), sorted numerically rather than lexically. The rule is shared between the suggested-tag badge (tag/suggest.ts) and the actionable non-semver/includeTagsrecovery path (watchers/providers/docker/tag-candidates.ts) via a newtag/version-population.tsmodule so the two paths can't drift apart again. (#859, #871) - The debug dump redacted env var names instead of values. Container env vars appear in the dump as
{ key, value }pairs, and the generic redaction walker matched the pair'skeyproperty against thekeysensitive-token rule, so a var likeHF_TOKENshowed up as"key": "[REDACTED]", "value": "xyz"— the name was hidden and the actual secret was left in plain text. Pair objects are now handled explicitly: the name always stays visible, and the value is redacted only when the name itself matches the sensitive-key rules. (#875, #885) - Containers that drop out of watch scope are now pruned from the store and UI. A container excluded by
watchbydefaultbeing off, or by itsdd.watchlabel being removed, previously kept its stale store record forever as long as it still inspected successfully in Docker — inspect success alone was being read as "still tracked" instead of "still in scope," so the only way to clear it was deletingdd.json. Stopped-but-still-watched containers are unaffected and keep their start-button visibility in the UI. (#869, #888)
1.7.0-rc.3 — 2026-08-23
Added
- Portwing edge tunnels now carry non-JSON Docker response bodies. A Docker endpoint that answers with a non-JSON body — for example
GET /_ping's plain-textOK— previously couldn't cross the edge tunnel intact. The controller's welcome frame now advertises anedge-response-body-b64capability and decodes base64-negotiated response bodies from agents that support it, falling back to the existing path otherwise. Additive and capability-gated, no protocol-version bump; pairs with the portwing-side change (portwing #206). (#852)
Changed
- README badges read live instead of being hand-bumped. The version, license, pull-count, and stars badges in the English README and all six translations now render from live shields.io endpoints (
github/v/releasewith prereleases,github/license, Docker Hub pulls,github/stars) instead of static images. The typed "GHCR 150K+ pulls" figure is replaced by the live Docker Hub count, and the release tooling no longer bumps or validates a static version badge per cut — there is nothing left to drift. (#851) - The Star History chart ships as a light/dark pair and refreshes at the release cut. The README's star chart is now a
<picture>block with theme-matched light and dark SVGs, so it follows GitHub's theme toggle instead of the OS preference, and the committed chart regenerates from the release-cut workflow rather than a cron — it can't mutate underneath a tag. (#844, #847) - DAST and workflow-lint gates fail closed. The ZAP scans (baseline and full) no longer pass
-I, which had told ZAP to ignore every warning, so findings now actually fail the gate (targeted suppressions belong in.zap/rules.tsv); the pre-push zizmor step errors with an install hint when the binary is missing instead of silently skipping. (#842) - A daily monitor asserts
maincarries a release tag. Drydock is the first caller of the org's main-is-released invariant monitor: a scheduled read-only workflow that goes red whenmain's HEAD is untagged, deliberately not a required PR context (a promotion merge is untagged by definition until the cut lands). (#846)
Fixed
- The release pipeline no longer trips over its own test infrastructure. The rc.2 cut failed on
main's push-triggered CI: a global js-yaml v5 override broke Artillery's load-test jobs (reverted to the v3 range Artillery actually supports, #829), and two Playwright waits were budgeted tighter than the backend operations they cover, so promotion runs raced the app's own state machine (ceilings raised past the app's budgets, fixes #832, #836).
1.7.0-rc.2 — 2026-08-20
Added
- Action-policy resolution is surfaced on the API and UI (spec-6.0.1-action-policy.md, slice 5).
GET /api/v1/containersand SSE container payloads now include anactionPolicyobject (state:blocked/manual/auto, plustriggerIdand, when blocked,reason) onupdateEligibility, reflecting the winning action trigger's resolved verdict for that container independent of whether it produced a blocker.GET /api/v1/containers/{id}/triggersgains a per-triggerresolvedStatefield with the same three values, so every candidate action trigger's individual verdict is visible, not just the winner. The container list/detail UI shows a new Auto badge (Update Status panel, full-page Actions tab, side panel trigger rows) wherever a container's or trigger's resolved state isauto, with tooltips explaining the blocked/manual/auto states. Newdd.action.autocontainer label andonautoAUTOmode: underAUTO=onauto,dd.action.includekeeps granting manual access but automatic dispatch additionally requires add.action.automatch, letting a container opt into manual-only access without picking up automatic execution. Drydock now logs a startupWARNfor any action trigger left onAUTO=onincludethat hasdd.action.include-matching containers with no correspondingdd.action.autolabel (would silently drop to manual-only if switched toonauto), and a secondWARNwhen a trigger is onAUTO=nonebut a container carries an inertdd.action.autolabel for it. Docs: newdd.action.autolabel andonautovalue are documented on the labels reference, triggers configuration, and update-eligibility pages, including a callout on all three clarifying thatAUTO=none/AUTO=allalso set the baseline access default (not just automatic execution) —AUTO=nonestill opens manual access to every container,AUTO=allopens both. - Greptile second-opinion review, summoned behind a label. A new thin caller workflow,
.github/workflows/greptile.yml, fires only onpull_request: labeledevents for thesecond-opinionlabel and calls the org's reusableCodesWhat/.githubgreptile-summon.ymlworkflow (pinned to a frozen commit SHA, no floating ref) to post one@greptileaireview request per exact PR head. CodeRabbit stays the automatic review lane; Greptile is opt-in only, for security-sensitive diffs, large refactors, or a tiebreaker when CodeRabbit and the author disagree..coderabbit.yamlgains alabeling_instructionsentry so CodeRabbit can auto-apply thesecond-opinionlabel itself when those criteria hit, in addition to a human applying it manually. (#751) release-cut.ymlcan now cut a maintenance patch from a retired dev branch without touching the cosign signing identity. A new optionalsource_refinput (e.g.dev/v1.6) builds and releases from that branch's tip instead ofmainHEAD, for a patch on a linemainhas already moved past (v1.6.1aftermainis on v1.7). The workflow itself still only ever dispatches fromrefs/heads/main, so the published cosign OIDC identity is unaffected.source_refis bounded hard — it must be an exactdev/vX.Ybranch that exists on origin, sharerelease_tag's line, not be the active (highest) dev line, and pair with a nonzero patch version — and the main-sync drift guard is skipped rather than failed, since there's no drift claim to make against a line main has already superseded. Left empty, every normal cut is unchanged. SeeRELEASING.md's new "Maintenance cuts" section for the full precondition list, including the manualci-verify.yml/e2e-playwright.ymldispatch required before cutting.
Changed
- Documented the container SBOM attestation in the release-verification guide.
release-cut.ymlhas generated and attested a signed SPDX 2.3 SBOM for every release container image since the v1.5 hardening batch, butcontent/docs/current/guides/verifying-releasesnever told readers how to check it. Added a "Verify the container SBOM attestation" section with thegh attestation verify --predicate-type https://spdx.dev/Document/v2.3command the workflow itself uses, and a note on the human-readabledrydock-${TAG}.image.spdx.jsoncopy attached to each release. No workflow change — the SBOM and build-provenance attestations, and their in-workflow verification, already met the house SLSA Build L2 / SBOM-attestation standard; only the user-facing documentation was missing. (#759) - Dropped the trivy qlty plugin in favor of Grype. Grype already owns the vuln-scanning surface (
.github/workflows/security-grype.yml); trivy's qlty-pluginconfigdriver only ever ran its DS*/KSV* Dockerfile misconfiguration checks, which checkov already covers (thecheckov:skip=CKV_DOCKER_3comment onDockerfileline 1, with matching rationale in.trivyignore.yamlfor the standalonetrivybinary the image ships at runtime — a separate concern, untouched here). Removed the[[plugin]] name = "trivy"block and thetrivy:DS002/trivy:DS-0002triage entry from.qlty/qlty.toml; trufflehog stays as-is. (#753) - Faster initial load on the login screen. The app's logo asset had drifted 4x oversized and was still being base64-inlined into the parse-blocking entry chunk (~640KB, reachable before authentication). It's now correctly sized and served as a separately cacheable file, cutting the entry chunk's gzipped size roughly in half and no longer re-downloading on every release. (#805)
- BREAKING: the Home Assistant MQTT topic layout now includes an
agent/<name>segment by default for agent-owned containers. Announced in v1.5.x and scheduled for v1.7.0. Multi-agent deployments previously collided on shared watcher names; the corrected layout scopes topics, sensor counts, and discovery cleanup per agent. Home Assistant references to the old topic paths must be re-pointed. SetDD_NOTIFICATION_MQTT_<name>_HASS_AGENTTOPICSEGMENT=falseto keep the old layout temporarily. Drydock does not retroactively clean up the old, pre-upgrade discovery entities. They remain retained on the broker and show up in Home Assistant as orphaned, frozen-at-last-state duplicates until manually removed.
Fixed
- Dependency-group admission no longer loses restart context when an upstream member is rejected. A restart-only dependent whose upstream was rejected during admission was previously misclassified as a normal update in wave 0 instead of being restarted after its dependency finished (or skipped if it never ran). The original admission batch is now carried through dispatch and API annotation so rejected members no longer corrupt the graph for their accepted dependents. (#718)
- Docker Compose updates no longer carry forward stale image-inherited environment defaults. A compose refresh could leave an old image's default environment value in place after updating to a new image with a different default, even though explicit runtime overrides on the container were always respected correctly. (#736, closes #734)
- Stashed update-policy overrides now survive drydock's own self-update. The
updatePolicyRetentionCachethat lets a recreated container inherit its predecessor's policy overrides lived only in process memory and was silently lost on the exact restart caused by a self-update. It now persists write-through to the store and rehydrates at startup, mirroring the same fix already applied to the maturity-clock cache. (#743, fixes #565)
Fixed
- Drydock no longer reports "Up to date" when an update check failed. A registry error while verifying a candidate's digest discarded the newer tag that had already been found, and the UI presented the result as a confirmed "no update available." A check that could not complete now surfaces as an explicitly unknown status instead of a negative answer. This also covers transient registry failures reported separately. (#814, #808)
- A single malformed container no longer zeroes out an entire agent inventory sync.
AgentClient.processAuthoritativeContainers()— the loop driving both the standard-mode handshake (_doHandshake(), every initial load and reconnect) and edge-modehandleContainerSync()(everydd:container_syncframe) — had no per-container error isolation, unlike the sibling SSE watcher-snapshot path. One container that threw while building its report (for example, during controller-side SBOM document offload) aborted the whole batch before any container reached the store, so a single bad container could make an otherwise-healthy fleet's inventory sync silently report zero containers. Each container in the batch is now processed independently, matching the existing isolation pattern inhandleWatcherSnapshotEvent(): a failure is logged with the container id and the batch continues with the rest. A batch where every container fails now also logs a single proportionate warning, since a silently empty result was what let this go unnoticed for a release cycle. Found while investigating #802; this closes the isolation gap but does not confirm the SBOM path as that issue's trigger — Portwing does not currently sendsecurity.sbomon its container wire format, so #802 remains open pending its actual root cause. - Nested OCI image indexes now resolve to the real image manifest. An image whose per-platform entry is itself an index, which is what Buildx produces when SBOM or provenance attestations are enabled, failed with
Unexpected error; no manifest foundand left the container unable to complete a digest check. Drydock now follows the nested index to the platform's actual manifest, bounded to three levels, and correctly ignores the attestation manifest alongside it. (#814)
Removed
- Retired the self-hosted star-history provider and the Warpchart embed that replaced it, in favor of a committed SVG chart. The self-hosted
/api/star-historyroute readprocess.env.GITHUB_TOKENat request time, and since that variable was always unset in production, it served a placeholder "check back shortly" SVG at HTTP 200 instead of erroring. Nothing ever went red, so the chart silently never rendered for as long as the route existed. Deleted the route handler, its SVG-rendering and request-resolution libraries, and the homepageStarHistorycard. Warpchart was adopted next as its replacement, then retired again days later once that decision reversed. Both now give way todocs/assets/star-history.svg, a chart built from GitHub's own stargazer timestamps and committed straight into the repo: it needs no runtime secret and cannot return a placeholder at HTTP 200, so a stale chart is visible and a missing one is a broken image, not a silent failure. Regeneration is handled by a scheduled shared workflow landing separately. All seven README language variants now point at the committed SVG instead of an external chart service. - BREAKING:
trigger-excludedandtrigger-not-includedare now hard blockers, ending the v1.5.0–v1.6.x soft-severity grace period (spec-6.0.1-action-policy.md, slice 6 — see deprecations). Previously, a container filtered out bydd.action.exclude/dd.action.include(or the legacydd.trigger.*labels) showed a Trigger filtered/Trigger excluded pill but still let a manual Update click through after a confirm-modal warning. As of this release both reasons are hard: the Update button is locked, and the API rejects a manual update request with the blocker's409message instead of queuing it — matching every other hard eligibility gate. This ships together with, and not before, the per-action execution policy (dd.action.autolabel,AUTO=onautotrigger mode) landed across slices 1-5, so the manual-only escape hatch isn't removed without a replacement path: an operator who wants a container to stay manually updatable while excluded from automatic dispatch now expresses that withAUTO=onautoplusdd.action.include(ordd.action.auto), rather than relying on the soft bypass.AUTO=oninclude's existing meaning is unchanged — a matchingdd.action.includelabel still grants both manual and automatic access under that mode. - Removed the unversioned
GET /api/auth/methodsalias. Deprecated since v1.6.0 and scheduled for removal in v1.7.0, it now returns 410 like the rest of the unversioned/api/*surface. UseGET /api/v1/auth/statusinstead.
Removed
- Removed
curlfrom the Docker image. Deprecated since v1.5.x with removal scheduled for v1.7.0. The image's own healthcheck has used the compiled/bin/healthcheckbinary since v1.5.0; user-defined HEALTHCHECK overrides that shell out to curl will stop working and should switch totest: /bin/healthcheck $${DD_SERVER_PORT:-3000}(the doubled$is compose escaping) or drop the override. Drydock now also logs a startup warning naming the container when it detects that its own healthcheck override still shells out to curl (in addition to the existing UI banner).
Security
- Scoped the Grype image gate around CVE-2026-14456 (openssl/libssl3/libcrypto3, Alpine 3.24) pending an upstream fix.
ci-verify.yml'sgrype-imagejob started failing--fail-on highafter this CVE (CWE-770, unbounded memory growth in OpenSSL's QUIC server incoming-channel-queue handling) entered the Grype DB; the packages are pinned at3.5.7-r0and no Alpine branch — 3.24-stable or edge — has repackaged the upstream 3.5.8 fix yet. Drydock links these libs only as a TLS client (theopensslCLI and Node's own TLS stack) for registry queries and the healthcheck, never as a QUIC/HTTP3 server, so the vulnerable path isn't reachable; OpenSSL's own advisory rates the flaw Low, versus the generic CVSS 7.5 the gate scores. Added a CVE-scoped (not location-scoped).grype.yamlignore entry for exactly this CVE on these three package names, so a different HIGH/CRITICAL finding in openssl still fails the gate. Two removal triggers: bump the Dockerfile's pinnedopenssl=version once Alpine ships >= 3.5.8 for 3.24, or 2026-11-17 (90 days out) regardless. - Closed a remote-property-injection path in the container list's URL query sync.
ContainersView's query-normalization helper wrote URL-controlled keys onto a plain object with bracket notation, which could in principle let a crafted__proto__query key reach the object's prototype chain. It now accumulates entries in aMapand materializes the result withObject.fromEntries, closing the vulnerability class rather than just suppressing the scanner finding. (#750)
Documentation
- Retroactively documented the
WUD_AGENT_SECRET/WUD_AGENT_SECRET_FILEremoval. v1.6.0-rc.1 silently dropped agent mode'sDD_AGENT_SECRET ?? WUD_AGENT_SECRETfallback (and the_FILEequivalent) with no deprecation period, and it was never recorded in the v1.6.0 changelog entry or deprecations at the time. An agent configured with only the legacyWUD_AGENT_SECRETwent from authenticating to a startup failure whose message doesn't name the variable actually set. See the new deprecations entry.
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 in deprecations). 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.
1.6.0 — 2026-08-11
Consolidates the 1.6.0-rc.1 … 1.6.0-rc.13 prereleases. Users upgrading from
1.5.2 get everything below; users already on 1.6.0-rc.13 receive no additional
runtime changes.
Added
- Portwing edge and agent integration matures into a first-class transport. Portwing 0.9.0+ agents work over either inbound Standard HTTP or dial-out Edge WebSocket transport, and drydock 1.6.0-rc.11+ recognizes Portwing's own Docker watcher marker to run native registry checks plus single/batch Docker updates controller-side (#632, #637, Portwing #76). Standard-mode agents can sign every request with Ed25519 (
DD_AGENT_{name}_AUTHMODE=ed25519, upgraded mid-series to signature version 2) instead of sending a shared secret. Edge agents stream live container logs and process deletes over the existing WS tunnel with correlated per-request IDs, pick their own display name (bound to their signing key so it can't be squatted), and are proactively disconnected when they stop answering pings. Theportwing/1.0edge endpoint is enabled by default (DD_EXPERIMENTAL_PORTWING=falseremains an emergency disable), backed by a real fleet-soak workflow that runs signed Portwing processes against production infrastructure on every PR and on a weekly schedule. - Declarative, three-tier update policy with a maturity stabilization gate (Discussion #307, Discussion #406, #320). Containers can declare
dd.updatePolicy.*labels (maturity mode/min-age, skip tags, skip digests) with watcher-level defaults and UI/API overrides, resolving through label → watcher default → persistent override precedence and surviving agent refresh, container recreation, and error-state rebuilds. A candidate held back bymaturityMode: matureis visible immediately with a live minute-by-minute countdown to its unlock time, and a newmaturity-clearednotification (backed by a 5-minute background sweep) announces the moment it clears instead of waiting for the next scan. Update Now remains available as an explicit soft-policy override. - Per-rule notification templates, bell preferences, and a new health-status event (Discussion #205, Discussion #198, Discussion #210). The Notifications view can override title/body/batch-title text per rule and provider with a live preview, and independently control which event categories reach the in-app bell — including a new
container-unhealthyrule (disabled by default) that fires off Docker health-check transitions. Bidirectional MQTT lets Home Assistant's Install button trigger a real drydock update through the normal eligibility/dispatch path, rate-limited per container and audited.dd.action.*/dd.notification.*labels are now strictly scoped to their own trigger category instead of one silently gating the other (#494). - Global update mode and an actionable Update Status panel (Discussion #325). Settings → General now selects one server-wide mode —
notify,manual, orauto— and the container side panel and detail view replace the old eligibility-badge stack with plain-language status plus a structured, deep-linking condition list covering all 16 eligibility reasons. Existing installs migrate toautoon upgrade so configured automatic updates keep working; fresh installs default tomanual(see Upgrade Notes). - Cross-device preference sync and a zero-dependency dashboard grid (Discussion #220, #281). An opt-in Sync across devices toggle stores the full UI preference set server-side per user and propagates changes to a user's other signed-in sessions in real time over SSE. The dashboard grid drops its
grid-layout-plusdependency for a deterministic CSS Grid implementation with edit-mode drag/reorder, bounded resizing, touch support, and per-breakpoint layout persistence. Audit, Security, Servers, and Watchers gain the same per-view column show/hide picker Containers and Agents already had. - More signal, less noise in update visibility. A pinned tag now surfaces the best newer same-family tag as a purely informational badge without making it an actionable update candidate (#498); a new Version Update filter isolates real semver bumps from digest-only churn on fleets that rebuild images daily (#538); and a startup warning flags minute-precise maintenance-window crons that only open their window for one minute per matching hour instead of the intended full range (Discussion #639).
- Opt-in wud-card/Homepage compatibility endpoints. With the unversioned
/api/*alias removed (see Removed, below),DD_COMPAT_WUDCARD=true(defaultfalse) mounts a narrow compatibility layer covering exactly the four endpoints the Home Assistant wud-card integration and Homepage'swhatsupdockerwidget call, reshaped into the bare-array response those integrations expect. (Discussion #469)
Changed
- Every major list view is responsive and consistent. Containers, Agents, Notifications, Security, Triggers, Watchers, Servers, Registries, Audit, and Auth now share one
DataTablewith a persisted per-view table⇄card toggle that auto-reflows to cards below ~640px; card views gained a sort control; and Source/Release-notes/Registry resource shortcuts render through one consistent 44px toolbar everywhere they appear (#498, Discussion #295). The Dashboard and Containers views no longer overflow horizontally on narrow phone screens. Update-status vocabulary was overhauled ("Digest update", "Security hold", neutral Major/Minor/Patch badges for informational-only updates), pinned reverted to being a tag property rather than an update status, and the maturity panel now shows exactly one countdown clock instead of two that could disagree with the gate (#556). Table polish across the series fixed pinned-column overlay clipping, a registry error overwriting the visible tag, sticky-column/auto-hide width miscalculation, and a clipped icon column. - Trigger taxonomy migration reaches its final warning stage. Every
DD_TRIGGER_*variable and the deprecateddd.trigger.include/dd.trigger.excludelabels still work in v1.6 but now log aterrorlevel ahead of removal in v1.7; useDD_ACTION_*/DD_NOTIFICATION_*and category-scoped labels, or runconfig migrate --source trigger. - Less duplicate work on the hot paths. Registry tag-list requests are shared across containers within a poll instead of being repeated per container; scheduled security scans and the shared log viewer avoid redundant store writes and re-renders; and transient registry network errors (timeouts, connection resets, DNS blips) now retry with backoff before a watch error is recorded.
- Base image bumped from Alpine 3.21 to 3.24, later rebased onto
node:24-alpineat Node 24.19.0 with a matching Trivy build-stage bump to 0.73.0, closing several HIGH/MEDIUM CVEs in the underlying image (#682).
Deprecated
GET /api/auth/methodsand the legacyGET /auth/strategiesresponse shape are deprecated in v1.6.0 (removal in v1.7.0 and v1.8.0 respectively). Both now log on every request and returnDeprecation/Sunsetheaders pointing callers at canonicalGET /api/v1/auth/status. See DEPRECATIONS.md.
Removed
- Unversioned
/api/*andWS /api/log/streamaliases are gone, returning410 Gone(or a rejected upgrade) instead of serving the request. This is a breaking change for integrations that hardcode the unversioned base path, notably the Home Assistant wud-card integration and Homepage'swhatsupdockerwidget — use theDD_COMPAT_WUDCARDshim above, or migrate to/api/v1/*directly. - Legacy v1.4-era authentication compatibility. Basic auth now accepts only argon2id hashes (
{SHA}, APR1/MD5, crypt, and plain-text hashes fail validation), and OIDC discovery now requireshttps://with no insecure HTTP workaround. - Legacy WUD configuration aliases, obsolete watcher switches, and legacy trigger-template variables.
WUD_*environment variables andwud.*labels are ignored (the migration CLI still recognizes them so existing files can be rewritten);DD_WATCHER_<name>_WATCHDIGEST/WATCHATSTARTare no longer configuration keys (usedd.watch.digest=true; startup watches always run); and$id/$name/$watcher/$kind/$semver/$local/$remote/$link/$countno longer populate in trigger templates. - Kafka
clientIdand token-only public-registry compatibility handling. Kafka validation accepts only lowercaseclientid, and malformed public-registry configurations (a barePUBLIC_TOKENwith noPUBLIC_LOGIN) now fail closed instead of silently falling back to anonymous pulls.
Fixed
- The maturity soak clock, update-policy overrides, and digest identity now survive the paths that used to reset them. Container recreation, agent report ingestion, watch errors, concurrent security scans, and manual rechecks each previously had at least one path that could silently restart a maturity countdown, drop a stored policy override, or roll back freshly detected update state — every one of those is closed, including the store's digest anchor now self-healing from a stale, multi-entry
RepoDigestsordering instead of trusting whichever entry happened to land first (#669). Notification dedup no longer fires a duplicate one-shot notification when a manual recheck returns drifted metadata for the same candidate, and containers with no available update are no longer ranked or filtered as though they had one. - Agent- and edge-specific reliability fixes. The container list no longer flashes a false "Agent Mismatch" during an agent's brief re-registration window; start/stop/restart/rollback return an explicit
501instead of an ambiguous404when an agent lacks lifecycle transport (#637); WebSocket log streams accept anonymous-auth sessions; and edgememoryGbnow reports binary GiB instead of decimal GB. - Operational fixes. The Star History chart is now self-hosted after the third-party chart services' outage (#671); the icon bundle no longer silently drops icons that became iconify aliases mid-series;
GET /api/v1/containers/backupsis reachable again (it was shadowed by the container router's own root handler); infrastructure-mode self-updates no longer fail to spawn their helper container on Docker Hub installs (#644); unchanged pending updates stop bloating the audit log on a timer; and the app and demo favicons match the v1.5.1 brand refresh (#439).
Security
- Anonymous access fails closed on upgrade, not just on fresh installs. An unconfigured or unconfirmed-anonymous instance now rejects protected API requests with
401instead of showing an open dashboard; setDD_ANONYMOUS_AUTH_CONFIRM=trueto keep an intentionally open instance working. - Transport and session hardening. HTTP notification triggers are hardened against SSRF (blocked cloud-metadata/link-local targets, contained redirects); WebSocket upgrades validate the complete origin instead of a host substring; the session cookie is renamed to
drydock.sid, signing every existing user out once;/storeand its contents are created owner-only (0700/0600); and icon CDN sources are pinned to exact upstream revisions instead of floating tags. - Ongoing dependency and base-image CVE sweeps ran throughout the series — notably Next.js 16.2.11 and React 19.2.8 on the documentation site,
fast-uri/ip-address/brace-expansionoverride advances, and the Node 24.19.0 + Trivy 0.73.0 bump in the final release candidate. See the individualrc.*entries below for full CVE detail.
Upgrade Notes
- Existing installs keep 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 record with noupdateModemigrates toauto, preserving pre-v1.6 behavior. 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. If you relied on the pre-v1.6 cross-category leak (#494), previously-suppressed notification (or action) triggers may fire once on the first scan after upgrading. drydock logs a one-time warning per affected container naming the missing label; set the matching label to the same value to restore the previous filtering.
Documentation
- Podman/Docker socket security docs refreshed, and a full configuration/env-var and API/OpenAPI parity audit brought the docs and generated spec in line with the shipped
/api/v1surface, including the authentication component, registry webhook signature auth, notification outbox actions, bulk container updates, backup listing, and container summary/update-response details.
1.6.0-rc.13 — 2026-08-08
Security
- Pinned
js-yamlto 3.15.1 in the e2e workspace (override; transitive dependency) for GHSA-5p4m-2wfm-xmqj (CVE-2026-59870 backport gap: quadratic CPU consumption in!!omapresolution). - Pinned
nanoidto 3.3.18 (override; transitive dependency ofpostcssin the root, app, apps/demo, apps/web, and ui workspaces, and ofartilleryin the e2e workspace) for GHSA-2v37-7h3g-55p8 (CVE-2026-67213) and, in the e2e workspace which was still on 3.3.12, also GHSA-28wg-ghj8-5hjv (CVE-2026-67214). node:24-alpinebase image bumped to Node 24.19.0, replacing the Node 24.18.0 image shipped in rc.12. Picks up Node's July 29 2026 security release, fixing 3 HIGH (CVE-2026-56846, CVE-2026-56848, CVE-2026-58043) + 5 MEDIUM CVEs that landed in 24.18.1 (#682).- Vendored
aquasec/trivybuild-stage pin bumped from 0.72.0 to 0.73.0, resolving 4 HIGH / 6 MEDIUM CVEs in its vendored Go dependencies: go-git (CVE-2026-71556),x/text(CVE-2026-56852), grpc (GHSA-hrxh-6v49-42gf), oras-go (CVE-2026-50151, CVE-2026-50163), and the Go stdlib (CVE-2026-39822) (#682).
Fixed
- Icon bundle no longer silently drops referenced icons at image build time (#683). The bundle is regenerated from the locked
@iconify-jsonpackages during every Docker image build, but the extractor only looked up plain icon entries —lucide:history(the Audit navigation icon in the Lucide icon theme) became an alias in lucide 1.2.121 and vanished from shipped images, rendering blank. The extractor now resolves alias chains, and references that never existed in the locked collections are fixed:iconoir:history→iconoir:clock-rotate-right,iconoir:gitlab→iconoir:gitlab-full,iconoir:stack→iconoir:multiple-pages, and the Font Awesome brand glyphs (GitHub/GitLab/Google/Microsoft registry icons) gained the previously missing@iconify-json/fa6-brandspackage. A new test asserts every icon referenced inicons.tsexists in the committed bundle. - Star History chart is now self-hosted (#671). The homepage card and README embed rendered a broken image after api.star-history.com's global outage (their GitHub tokens rate-limited; starchart.cc also failing). A new
/api/star-historyroute on the website fetches stargazer timestamps from the GitHub API server-side (optionalGITHUB_TOKEN, edge-cached six hours with stale-while-revalidate, short-lived fallback SVG on fetch failure) and renders the chart in the site's own palette for both themes; the README uses a<picture>element with theme-matched variants. No third-party chart service remains in the path, andapi.star-history.comis dropped from the site's CSPimg-src. - Digest-update comparison no longer anchors on an arbitrary
RepoDigests[0]entry (#669). A local Docker image can carry multiplerepo@digestentries for one Image ID (pull/retag accumulation, no ordering guarantee);getRepoDigestblindly took index 0, so a stale or foreign-repo entry landing first anchored the whole digest-update pipeline to the wrong manifest and produced a persistent digest-update false positive that survived applying the update.getOrderedRepoDigests(app/watchers/providers/docker/docker-helpers.ts) now returns every RepoDigests entry whose repo component matches the container's own image reference, ordered, falling back to the full list only when nothing matches; the container model gained an optionalimage.digest.repoDigestsfield carrying that ordered list, re-derived from the live Docker image inspect on every discovery/refresh cycle.handleDigestWatch(app/watchers/providers/docker/image-comparison.ts) now walks that candidate list — a cheap raw-value check first, then a normalize-and-compare registry call per remaining candidate, skipping anchors whose manifest lookup fails — and re-anchorsdigest.repoto whichever candidate actually matched, so a store already poisoned with a staledigest.repoself-heals on its own. A genuine same-tag republish (no candidate matches) still flags an update exactly as before; if every candidate fails to normalize, the failure now propagates instead of being silently coerced into a false "no update".
1.6.0-rc.12 — 2026-08-04
Changed
- Routine dependency maintenance across the root tooling, demo, UI, and website workspaces (#653, #654, #655, #656). The dependency-version guard's Next.js check became a 16.x floor instead of an exact pin so routine patch bumps stop tripping it (#664).
- Crowdin translation sync (#665): one French container-component string corrected.
Fixed
- "Agent Mismatch" no longer appears in the container list/SSE display during the brief window an agent's docker/dockercompose trigger is still (re)registering (#605). Eligibility is recomputed live on every read, and
AgentClient._doHandshake()deregisters the agent's components before awaiting the/api/triggersfetch and re-register. A read in that window found zero triggers for the agent andcomputeUpdateEligibilityraised a hardagent-mismatchblocker, disabling the Update button, even though nothing was actually misconfigured — the condition self-corrected once registration finished.agent-mismatchnow downgrades to a soft blocker (button stays enabled) on display surfaces whenever the container's own agent is mid-registration, per the newAgentClient.isRegisteringComponentsflag (true only for the deregister→re-register span, not the whole reconnect backoff). Update admission (app/updates/request-update.ts) is unaffected and stays hard/fail-closed throughout, so an update can never be enqueued through a wrong-agent trigger during that window. - WebSocket log streams no longer reject anonymous-auth sessions (#636). Both WS upgrade paths — the system log stream and the container log stream — gated on
isAuthenticatedSession()requiringsession.passport.user, whichpassport-anonymousnever sets, so underDD_ANONYMOUS_AUTH_CONFIRM=truethe log stream WebSocket always rejected the upgrade even though every REST endpoint worked.isAuthenticatedSessionnow also accepts the session when anonymous authentication is the registered mode. - Maturity clock: swallowed auth errors surfaced, per-container threshold respected (#604).
getImagePublishedAtfailures — including GHCR/LSCR 401/403 auth errors — now log atwarninstead ofdebug, so the maturity gate's silent fallback from the registrypublishedAttoupdateDetectedAtis no longer invisible.getRawUpdateMaturityLevel(app/model/container.ts) andgetContainerMaturityLevel(app/api/container/maturity-filter.ts) now resolve each container's ownupdatePolicy.maturityMinAgeDaysbefore falling back to the globalDD_UI_MATURITY_THRESHOLD_DAYS, matching the gate's ownisUpdateSuppressed/isMaturityGatePendinglogic so the hot/mature badge can no longer disagree with the gate in the same API response. - Container start/stop/restart/rollback return an explicit 501 instead of an ambiguous 404 for agent containers without lifecycle transport (#637).
POST /:id/start|stop|restartandPOST /:id/rollbackreturned a bare 404No docker trigger found for this containerwhenever the lookup missed, indistinguishable from "container not found" — for agent-owned containers this was the only signal the UI got. That lookup miss now returns 501 naming the likely cause (the agent's connection typically hasn't advertisedusesControllerDockerTransport) whencontainer.agentis set; non-agent containers still get the existing 404. This complements the native-transport support that shipped in rc.11 via #651, which closed #637's core gap — this is the remaining explicit-error half.
Security
brace-expansion,ip-address, andfast-urioverrides advanced to patched releases.brace-expansionmoved to 5.0.9 inapp/,ui/, ande2e/(CVE-2026-69152, GHSA-rgw5-rvv9-x895);ip-addressmoved to 10.3.1 inapp/(CVE-2026-54272, CVE-2026-69192, CVE-2026-69198), pulled in transitively viaexpress-rate-limitandmqtt→socks;fast-uriadvanced from 4.1.1 to 4.1.2 inapp/andui/(host confusion via backslash authority introducer, CVE-2026-18446, GHSA-7p8r-x3mc-p8w7, superseding #658).
1.6.0-rc.11 — 2026-08-01
Added
- Portwing controller-owned Docker watcher, update, and lifecycle transport (#632, #637, Portwing #76). Drydock 1.6.0-rc.11+ recognizes Portwing 0.9.0's exact Docker watcher marker (
transport=docker-api,execution=controller,events=portwing) and runs its native registry checks plus single/batch Docker updates controller-side. Container start, stop, restart, update preview, and backup rollback actions use that same native Docker capability surface instead of failing because Portwing intentionally advertises no remote trigger. A loopback-only bearer-authenticated bridge carries Docker API calls through Portwing over Standard HTTP or Edge correlatedrequest/response/streammessages, while Portwing remains the lifecycle-event source. Later raw Portwing inventory (updateAvailable=false,updateKind=unknown) preserves rather than erases the controller-enriched update state.
Changed
- Standard Portwing Ed25519 requests now use signature version 2. The controller sends five authentication headers, including
X-Portwing-Signature-Version: 2, and signs the exact origin-form request target (escaped path plus the unmodified raw query) instead of a decoded path without its query.
1.6.0-rc.10 — 2026-07-31
Added
- Startup warning for minute-precise maintenance-window crons.
DD_WATCHER_{name}_MAINTENANCE_WINDOWis matched minute-by-minute, so a fixed minute field like0 2-6 * * *only opens the window for one minute per matching hour instead of the whole hour range — a common copy-paste trap reported in Discussion #639. The Docker watcher now logs a one-time warning at init when the configured window's minute field doesn't contain*, pointing at the fix (* 2-3 * * *). Step values like*/5are intentional and are not flagged. - "Learn more" link in the update confirm dialog for policy-blocked overrides. When overriding a soft-blocked update, the confirm dialog now links to the update-eligibility reasons reference, matching the link already shown in the Update Status panel. (Discussion #639)
Changed
- Translations resynced from Crowdin (#620), refreshing the container-component and list-view catalogs across all 15 non-English locales, plus a fuller French pass over the agents, app-shell, common, and config catalogs.
- Routine dependency maintenance across the app and UI workspaces (#614, #615, #617, #646): undici 8.9.0 (app) and the 7.29.0 UI override, express-rate-limit 8.6.1, @aws-sdk/client-ecr 3.1096.0, vue-i18n 11.4.8, knip 6.29.0, postcss 8.5.24, and the
fast-urisecurity override advanced from 3.1.4 to 4.1.1 (both retain the CVE-2026-16221 fix; the dependency-version guard now rejects the still-vulnerable 4.0.0–4.1.0 range). Renovate no longer proposes@babel/coremajors while Stryker's instrumenter requires Babel 7.
Fixed
- Unchanged
update-availableaudit entries are no longer re-recorded on a timer. Previously an unchanged pending update was re-written to the audit log every time the 1-hour dedupe window lapsed, bloating the audit log for fleets with long-pending updates (a fleet with 10 persistent pending updates wrote 140 rows in 24h). Audit rows are now recorded only on first detection or when the update target/kind changes. The RC-onlyDD_AUDIT_UPDATE_AVAILABLE_DEDUPE_MSvariable is removed. - Maintenance-window documentation corrected and made findable (Discussion #639). Every documented example used a minute-precise cron (
0 2-6 * * *) while describing it as an hourly range ("2am–6am daily").MAINTENANCE_WINDOWis matched minute by minute, so that expression opens the window for one minute at the top of each listed hour and reports "closed" the rest of the day. Examples now use* 2-6 * * *, with a table of correct forms in Watchers. The update confirm dialog's own wording ("This update is currently policy-blocked", "Outside maintenance window — auto update deferred until the window opens", "Update anyway") appeared nowhere in the docs, so searching for the on-screen message returned nothing; Update Eligibility & Blockers now documents that dialog verbatim and states plainly that a soft blocker never gates a manual update. - Infrastructure-mode self-updates no longer fail to spawn the helper container on Docker Hub installs (#644). For containers labeled
dd.update.mode=infrastructure, the self-update helper is spawned from Drydock's own image, but the reference was rebuilt asregistry-1.docker.io/codeswhat/drydock:<tag>— the Docker daemon stores Hub images under their short name (codeswhat/drydock:<tag>), the helper image is never pulled since it must already exist locally, and container creation failed with404 No such image, rolling the update back. The helper image reference is now resolved through the registry provider'sgetImageFullNamenormalization (the Hub provider stripsregistry-1.docker.io/,docker.io/,library/) so it matches the daemon-local image name; the previous raw construction remains only as a fallback when no registry manager resolves.
1.6.0-rc.9 — 2026-07-28
Added
- Continuous Portwing edge log streams. The authenticated container-log WebSocket now bridges correlated
dd:container_log_chunk,dd:container_log_end, anddd:container_log_errorframes, cancels the agent stream when the viewer closes, preserves stdout/stderr and timestamp decoding, and caps each downstream viewer at 1 MiB of buffered data. Older Portwing agents degrade to their one-shot response without breaking the viewer. - Real Portwing fleet-soak workflow. Pull requests and the weekly quality schedule run actual signed Portwing processes against Drydock's production edge gateway through concurrent exec, sustained logs, forced controller backpressure, and reconnect storms. Machine-readable evidence records RSS/heap budgets and is retained for 90 days.
Changed
- The
portwing/1.0edge endpoint is enabled by default.DD_EXPERIMENTAL_PORTWING=falseremains as an emergency disable for new edge connections. OpenAPI, operator docs, and translated feature summaries now describe the stable/default-on behavior.
Fixed
- The row "Updating/Queued/Scanning" overlay chip is no longer clipped at the pinned-column edge. The table's pinned identity-cluster cells (
sticky z-10) painted over the full-width row overlay hosted in the first cell and cut the centered chip in half at the cluster boundary. While an overlay is active the host cell now sits above its z-10 row siblings (and still below the sticky header). (#631) - Registry errors no longer replace the container's tag in the tag column. A rate-limit/auth/not-found registry error rendered a danger pill instead of the current tag (table mode) or appended one (card mode), duplicating the warning glyph + tooltip the registry column already shows. The tag cell now always shows the actual tag; the registry column glyph remains the single error flag. (#631)
1.6.0-rc.8 — 2026-07-28
Fixed
- Auto-update no longer stops when the update-available notification rule is scoped to specific channels (#623). Action triggers (
docker,dockercompose,command) were gated by the same trigger allow-list on theupdate-availablenotification rule that routes messages to notification channels — but action-trigger ids are deliberately barred from that list by the API validator, the UI picker, and the documented rule model, so the moment any notification trigger was assigned to the rule, every action trigger (local and agent-hosted alike) silently failed the membership check withexcluded-from-allow-listand auto-update stopped fleet-wide, with only a debug log as evidence. Action-category triggers are now exempt from the allow-list membership check ingetUpdateAvailableAutoTriggerDispatchDecision(app/triggers/providers/Trigger.ts), mirroring the exemption the lifecycle-notification path has always had; disabling the rule itself still acts as the global kill switch. Present since the rule allow-list landed in v1.6.0-rc.1. - Controller-set update policy overrides no longer vanish from agent-managed containers (#565). Remote agents resolve their own declarative (env/label) policy but never learn controller-side runtime overrides, so every container report they send carries an explicit empty override layer. The controller persisted that layer verbatim, and
updateContainer(app/store/container.ts) treated any presentupdatePolicyOverrideskey as authoritative — clearing maturity mode/min-age days, skip lists, and snoozes on every periodic agent sync or manual recheck. This was the settings-deletion mechanism behind #565, distinct from the soak-clock resets fixed in #568 and rc.7. The controller now reapplies its stored overrides when ingesting agent reports, and the store itself encodes the rule the recreate path has had since #497: an empty incoming override layer carries no controller intent and only clears stored overrides when the update-policy PATCH handler marks the write as authoritative, so deliberate clears from the UI still stick.
1.6.0-rc.7 — 2026-07-26
Changed
- Regenerated
ui/src/boot/icon-bundle.jsonagainst the locked iconify versions (#608), fixing drift since the tabler 1.2.35 → 1.2.37 bump landed in #594.
Fixed
- The maturity soak clock no longer resets when a container is recreated.
getResultSignature(app/store/container.ts) decided whether a recreated container inherits its stashedupdateDetectedAt/firstSeenAt/maturityGatePendingSince, and it includedcreatedeven when a digest was present. Registries don't all derivecreatedfrom the digest, so it could differ between the pre-delete snapshot and the post-recreate rescan without the update candidate changing — discarding the stash and silently restarting the soak right after an update landed, which is exactly when clock continuity matters. Same defect class as #565, surviving in the recreate path that #568 never touched. - Notification dedup no longer fires a duplicate
once: truenotification when a manual recheck bypasses the registry poll cache.computeResultHash(app/store/notification-history.ts) hashedsuggestedTagandcreatedalongsidetag/digest/updateKind, so a recheck that returned a driftedcreated/suggestedTagfor the same candidate produced a different hash, madehasAlreadyNotifiedForResultreport "not yet notified", and re-fired notifications that were supposed to be one-shot. The hash now derives from the same candidate-identity definition PR #568 established for the maturity soak (tag+digest, withcreatedparticipating only when no digest is available), via a single shared helper (getCandidateIdentityFieldsinapp/model/container.ts) now used byhasCandidateIdentityChanged,getResultSignature, andcomputeResultHashalike — previously three independently hand-rolled comparators that disagreed with each other. Upgrading may produce one extra notification for updates already in flight, since history persisted under the old hash formula won't match the new one; this is a one-time false positive, not a regression. - A security scan finishing no longer reverts an update the watcher detected while it was running.
runBulkScan(app/api/container/bulk-security.ts) andpersistAndBroadcast(app/api/container/security.ts) both captured a container snapshot before starting the scan, then wrote that whole snapshot back with onlysecuritychanged. Vulnerability scans run for seconds to minutes, so a watcher poll during that window could legitimately detect a new update and haveresult,updateAvailable,updateDetectedAt, andmaturityGatePendingSincesilently rolled back by the scan's write-back. The next poll re-detected the same update and stamped a fresh timestamp, discarding the elapsed soak time. Both paths now re-fetch the live record at write-back time and merge onlysecurityonto it, matching what the scan scheduler already did, and skip the write entirely if the container is gone. - Containers with no available update are no longer ranked or filtered as if they had one.
getContainerUpdateAge(app/api/container/update-age.ts) fell back tofirstSeenAt/publishedAt/updateDetectedAtwithout checkingupdateAvailable, so a container gated bymaturityModecould still appear under?maturity=mature,?maturity=hot, and?sort=agewhile its ownupdateEligibilityblockers in the same response said it was blocked.
1.6.0-rc.6 — 2026-07-26
Added
- Maturity-cleared notification (Discussion #587). When an update previously withheld by the maturity gate (
maturityMode: mature) becomes applicable, drydock now fires a dedicatedmaturity-clearednotification instead of waiting for the next scheduled scan's genericupdate-availablenotification. A new background sweep (DD_MATURITY_SWEEP_CRON, default every 5 minutes) re-checks containers currently withheld by the gate and fires the moment each one's soak window elapses; the same check also runs at every watch-cycle scan. Delivery respects the sameTHRESHOLD, include/exclude, andONCEdedup rules asupdate-available, and is deduplicated against it so the same update is never announced twice. Action triggers (Docker, Docker Compose, Command) are unaffected and keep applying updates on their normal scan cadence.
Changed
- Routine dependency maintenance across every workspace. Minor and patch updates for the app, UI, demo, documentation website, and end-to-end suites, including
undici8.8.0,helmet8.3.0,express-rate-limit8.6.0,node-cron4.6.0,re2js2.8.6, and@aws-sdk/client-ecr3.1091.0. - Translations resynced from Crowdin (#545), refreshing the container, dashboard, list, and shared-component catalogs across the supported locales.
Fixed
- Empty-result fallbacks in CI scripts are reachable again (#599). Steps that run under
bash -eo pipefailpiped agreporfindstraight into a variable assignment, so an empty result — no matching branch, a load-test artifact directory that a failed run never created — exited non-zero and aborted the step before the deliberate "nothing found" handler beneath it could run. The Crowdin sync hit this for real on 2026-07-24 and retargeted its translation PR at the default branch. The fallible command is now guarded on its own so a genuine failure (network, auth, bad syntax) still fails loudly, and regression tests pin every affected site. - Renovate no longer opens dependency PRs that bump
package.jsonwithout the lockfile (#594), which previously landed manifests and lockfiles out of step. Alockfile-synctest now enforces the pairing. - The gitignored
.planningdirectory is no longer tracked (#593), with a test guarding against re-adding files that.gitignorealready excludes.
1.6.0-rc.5 — 2026-07-23
Changed
- The Containers table's Resources column is now optional (#498). It remains visible by default, but the column picker can hide it and preserves that choice. Source, release-note, and registry shortcuts move into each row's More menu while hidden and remain available in card and detail views.
- The Playwright release gate now waits for watcher-complete QA fixtures. Browser scenarios start only after the authenticated setup sees the full local and remote fixture set from the startup scans, while later cron and Docker-event scans are parked in the browser-only compose stack. This prevents grouping, update-status, and targeted-refresh assertions from racing partially enriched container state without adding whole-suite retries or changing production watcher defaults.
Fixed
- The Dashboard and Containers views no longer overflow horizontally on narrow phone screens (#498). Crossing into a single-column breakpoint now re-syncs the dashboard widget layout so widgets stop spilling past the viewport, single-column card lists no longer leave an empty band below reflowed cards, and long stack names in the Containers group header truncate instead of pushing the update button off-screen.
Security
- The documentation website now runs Next.js 16.2.11 and React 19.2.8. The Next.js patch closes nine upstream advisories disclosed against 16.2.9, including proxy bypass, Server Actions denial of service and SSRF, rewrite SSRF, response-cache confusion, image-optimization denial of service, and Server Function endpoint disclosure (GHSA-6gpp-xcg3-4w24, GHSA-m99w-x7hq-7vfj, GHSA-89xv-2m56-2m9x, GHSA-p9j2-gv94-2wf4, GHSA-68g3-v927-f742, GHSA-4633-3j49-mh5q, GHSA-4c39-4ccg-62r3, GHSA-q8wf-6r8g-63ch, GHSA-955p-x3mx-jcvp).
- The documentation website's
postcssbuild dependency is upgraded to 8.5.22, closing CVE-2026-45623 (GHSA-6g55-p6wh-862q), an arbitrary-file-read via an attacker-controlledsourceMappingURL(fixed upstream in 8.5.12).
1.6.0-rc.4 — 2026-07-22
Added
- Container groups can be edited directly in the browser (#498). A container's More menu can set, change, or clear a local group override; clearing it falls back to the durable
dd.group/ Compose project / Swarm namespace group. Overrides are regular UI preferences, so they remain browser-local unless cross-device preference sync is enabled.
Changed
- Same-tag rebuilds read “Image update” instead of “Digest update.” The update tooltip now explains that the visible tag has not changed but points to a different image build, and that redeploying pulls the new image. The dashboard uses the same vocabulary; literal image hashes are still correctly labeled digests.
- The Containers table distinguishes Tag, Software Version, and Update more clearly. The secondary metadata column is now named Software Version, explains its source in a header tooltip, and folds before Host at constrained laptop widths so host identity stays visible.
- The release-gated E2E lanes now fail with attributable evidence instead of blanket retries. Cucumber reuses the build-gated QA image, waits for the exact active fixture manifest, restores scenario-mutated state, publishes structured reports and diagnostics, and leaves browser rendering to Playwright. Playwright keeps first-failure media without retrying the whole test, gives its short-lived QA process explicit API and icon-proxy traffic budgets, waits for Docker's own healthcheck to acknowledge its health-transition fixture before scanning it, and fails setup if bounded runner-daemon pulls and image transfers cannot seed its required remote Docker fixtures.
DD_SERVER_RATELIMIT_MAXcan override the outer API's default 1,000-request/15-minute budget per rate-limit key; deployed defaults remain unchanged.
Fixed
- Pinned containers with a visible newer version no longer say “Current.” A non-actionable
updateInsightnow renders as an informational Major/Minor/Patch state in table, card, and Update Status views while remaining ineligible for automatic or manual update actions. - A targeted container recheck no longer overwrites fresh Docker health.
POST /api/v1/containers/:id/watchnow scans and returns the live container object refreshed by the watcher instead of the stale store snapshot captured before refresh, preserving health-only transitions and their notification events.
1.6.0-rc.3 — 2026-07-21
Changed
- Update-status vocabulary overhaul across the containers UI (#498, #556). "Digest" reads as "Digest update"; the NEW/MATURE freshness badges are gone (the detection age lives in the update-kind badge's tooltip instead); an unrecognized update kind now renders a neutral "Unknown" badge instead of nothing; and the bouncer's "Blocked" label is now "Security hold".
- Pinned is a tag property again, not an update status. Pinned containers show a persistent pin glyph on the Tag cell whether or not an update exists, with the
dd.tag.familyremedy in its tooltip; up-to-date pinned rows read "Current" like everything else instead of a bare "Pinned" chip that only appeared when a newer family version existed. The container-detail Update Status panel follows the same rule: an insight-only container reads up to date there too, with the held-back newer tag relegated to an informational detail row. - The maturity panel keeps exactly one clock. The policy sentence names the clock the gate actually measures against ("Candidate published {date} — n more days until the minimum", with a detection-date fallback), the duplicate countdown collapsed into one "{countdown} · unlocks {date}" line, and the backend now exposes the resolved clock (
clockSource/clockStartAt) on the maturity blocker so the UI reads the gate's own verdict instead of re-deriving it from the detection time. - The column picker tells the truth about auto-hidden columns. Columns hidden to fit the viewport stay checked with a muted "hidden to fit" note, and the "+N" toolbar badge's tooltip names them.
Fixed
-
The pin glyph marks only tags the pin gate actually governs. The glyph is driven by a new backend
tagPinGatedverdict — a specific-version tag with nodd.tag.includefilter under a non-loosedd.tag.familypolicy — instead of bare tag shape, which had put a "won't climb to newer versions" pin (clipped to near-invisibility) on every exact-version container, including ones with actionable updates. It now renders inline beside the pinned tag at a legible size. -
Container-detail registry links land on the registry they name (#556). The
/registries?q=<type>.<name>deep-links emitted by container detail matched neither the bare registry name nor the bare type, so they always rendered an empty filtered list; the registries filter now also matches the dotted instance ID. -
Removed-API-path banner grammar and tooltip freshness. The banner title pluralizes correctly ("1 request" vs "n requests"), and tooltip-bound native
titleattributes now follow reactive value changes (previously the column picker's "+N" badge kept naming whichever columns were hidden when the page first rendered). -
Manual recheck no longer restarts the maturity countdown on display-only metadata drift (#565). A per-container recheck bypasses the registry poll cache, so the suggested tag and image created date can wobble between scans even when the update candidate itself hasn't changed, falsely resetting the soak. The lifecycle clock now restarts only when the candidate's actual identity — tag or digest — changes; suggested tag and created date no longer factor into the restart decision.
Security
-
Release dependency security refresh. Updated
fast-urito 3.1.4 for CVE-2026-16221,fast-xml-parserto 5.10.1 for GHSA-8r6m-32jq-jx6q, and the website's transitivesharpdependency to 0.35.3 for GHSA-f88m-g3jw-g9cj. -
Anonymous access now fails closed on upgrades, not just fresh installs. An instance with no authentication configured — or with anonymous auth enabled but unconfirmed — starts and fails closed: protected API requests are rejected with
401, auth discovery/status remains public,/healthreports503, and the SPA shell may load without access to protected application data. This replaces the previous warning plus open dashboard. If you run an intentionally open instance, setDD_ANONYMOUS_AUTH_CONFIRM=true; otherwise configureDD_AUTH_BASIC_<name>_USER/_HASHbefore upgrading. -
HTTP notification trigger hardened against SSRF. Hostnames are re-resolved on every request through a guarded DNS lookup that blocks cloud metadata and link-local targets (override with
allowmetadata=true), and redirects can no longer escape into blocked address space via cross-host or DNS-rebinding hops. -
WebSocket upgrades validate the complete origin. Origin checks now compare full scheme/host/port origins — honoring
X-Forwarded-Proto/X-Forwarded-Hostonly when trust proxy is enabled — instead of host-substring matching, closing WebSocket CSRF gaps behind reverse proxies. -
Session cookie renamed to
drydock.sid. Replaces Express's defaultconnect.sid, so drydock sessions no longer collide with (or are fingerprintable as) other Express apps behind the same host. Everyone is signed out once on upgrade. -
Persistent store files are owner-only.
/storeis created0700,dd.jsonis kept0600, and the container entrypoint and store process set a077umask so LokiJS autosave replacement files never widen permissions. -
Icon CDN sources are pinned to exact upstream revisions (dashboard-icons, selfh.st icons, simple-icons) instead of floating tags, so an upstream push can't silently change what the dashboard serves.
1.6.0-rc.2 — 2026-07-18
Added
- "Version update" container filter (#538). The Containers filter bar gains a Version Update kind option that shows only real semver upgrades (major, minor, patch) and hides digest-only churn — the noise that dominates the dashboard on fleets that rebuild images daily in CI. Like every other container filter, it round-trips through the URL (
?filterKind=version), so the filtered view can be saved as a bookmark for one-click access.
Fixed
-
Repair rebuilds no longer reset the registry-reconciled digest (#541). When the fast refresh path repairs a stored container's broken image reference, the reconciled
digest.value— the security scan-group key — is now preserved unless the repo digest genuinely changed (image re-pulled), the same stickiness rule the slow-path rebuild gained for #536. -
Containers with a stored watch error refresh on the fast path (#542). An error from the previous cycle no longer routes a known container through the full first-discovery enumeration — image/label/tag re-resolution plus an O(n) stale-entry scan of the store — on every cron. Errored-but-known containers reuse the same cheap refresh as healthy ones, and broken image references still self-heal via the unconditional repair check.
-
Last-known-good update state survives watch errors (#543). A failed watch cycle — registry timeout, rate limit, transient network error — no longer erases the previous successful comparison. The stored
result,updateAvailable,updateKind, and current release notes are preserved alongside the recorded error until a later cycle succeeds, so the UI keeps showing the last known update state with the error annotated next to it instead of a blank. -
Portwing WS Welcome frames now report the actual server version — the endpoint had hard-coded
1.5.0since v1.5.0, so v1.5.1/v1.5.2/v1.6.0-rc.1 servers all identified as1.5.0to agents (#550 review finding)
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. (Discussion #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) -
Column show/hide picker now covers every major data view. Audit, Security, Servers, and Watchers gain the same per-view column show/hide picker (
DataTableColumnPicker, backed by theuseViewColumnVisibilitycomposable) that Containers and Agents already had, so every one of those six views can hide low-priority columns and have the choice persist per view.
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. 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. -
Card view gained a sort control. Containers, Agents, and Security now render a sort-field and ascending/descending control (
DataSortControl) in the filter bar when in card mode — the pre-v1.6 card grid (DataCardGrid, since removed) had no sorting of its own, so cards showed whatever order the table view happened to produce. -
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. -
Authenticated session checks no longer trip the shared auth rate limiter.
GET /auth/usercalls from an already-authenticated session are now exempt from the shared public auth limiter (100 requests/15 min); normal signed-in navigation could exhaust that budget through repeated re-reads and start returning spurious429s even though the backend was healthy. Every other auth route — including unauthenticated/auth/userchecks, status discovery, login, remember, logout, and mutations — remains rate-limited as before. (PR #526) -
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.
1.5.2 — 2026-07-13
Consolidates the 1.5.2-rc.1 … 1.5.2-rc.5 prereleases. Users upgrading from
1.5.1 get everything below; users already on 1.5.2-rc.5 receive no additional
runtime changes.
Added
- 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. What's new is exposing this comparison at all for pinned tags, since the pin gate itself already blocks acting on any of it. 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, so nothing new fires because of it. On by default; opt out per watcher withDD_WATCHER_{name}_TAG_PIN_INFO=false. Surfaced in the UI as an informational "Newer available" badge next to the existing update-state badges.
Changed
- Docs: documented the socket-proxy requirements for recreating containers with a static IP or MAC address (macvlan, custom networks). Tecnativa/docker-socket-proxy needs both
POST=1andNETWORKS=1for containers on more than one network (the extraPOST /networks/{id}/connectcalls); sockguard needsrequest_body.network.allow_endpoint_config: trueto permit endpoint configs that specify a static IP or MAC address — and as of sockguard v1.5.0 and later (forthcoming at the time of this release), that requirement also applies toPOST /containers/create, so single-network macvlan/static-IP containers need the policy set too, not just multi-network ones (older sockguard versions enforce the policy only at network-connect time). Added a matching FAQ entry for containers left renamed-old-<timestamp>after a failed update, including a note for operators who intentionally name a container in a way that collides with drydock's own rollback naming convention.
Fixed
- 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.
- Pinned semver tags (e.g.
nginx:1.25.3) 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, leaving them with no update detection at all and a misleading "compared by digest only" notice on every pinned container (#498). Version climbing for pinned tags remains opt-in viadd.tag.includeordd.tag.family=loose. Note: in agent deployments, agents perform the registry checks — update agents alongside the controller to restore digest detection for agent-watched containers. - The "no update detection" notice shown when
dd.watch.digest=falseis explicitly set no longer claims digest comparison is happening. - Removed the unreachable flat
tagFamilyimgset config key (env-derived keys are lowercased, so it could never match); the documentedDD_WATCHER_{watcher}_IMGSET_{name}_TAG_FAMILYform is unaffected. 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. - Tooltip text now wraps inside a bounded popup instead of rendering one screen-wide line — the shared tooltip directive had
white-space: nowrapand nomax-width, so the pinned-tag "Newer available" insight tooltip (and any other long tooltip) rendered as a single ~130-character line off the edge of the viewport (#498). - The pinned-tag insight badge no longer clips in the containers list. Centered with no
max-width, it could overflow its narrow table column and get hard-clipped on both sides; it now truncates gracefully with a trailing ellipsis instead (#498). - Shortened the pinned-tag insight tooltip copy so it reads cleanly at the tooltip's new bounded width (#498).
- Orphaned replacement container after a failed post-create network connect.
createContainercreated the replacement container and then connected it to any additional networks in the sametry; if a network connect failed (for example a static-IP endpoint config rejected by a socket proxy), the error was rethrown without exposing the created container, so nothing could clean it up. The renamed original (<name>-old-<timestamp>) stayed parked while the orphanedCreated-state replacement squatted the real name. Every rollback consumer — the regular Docker update executor, self-update, health-monitor auto-rollback, the backup-restore API, and Docker Compose rollback restore — now recovers the created container from the error and best-effort stops + force-removes it before restoring the original container's name, tolerating cleanup failures with a warning rather than masking the original error. The same recovery applies when the replacement is created successfully but then fails to start. - Repeated failed updates could cascade into a second rename. If a container was already left renamed
-old-<timestamp>by a prior failed update, a subsequent update attempt against it now fails immediately with a clear "needs manual cleanup" error naming the true canonical name (even through nested renames) instead of renaming and recreating again on top of the unresolved failure. The same guard protects drydock's own self-update. - Replacement containers no longer inherit the previous container's auto-assigned MAC address. The recreate path copied each network endpoint's operational
MacAddressstraight fromdocker inspect, permanently re-pinning a MAC the daemon had generated (stale once the endpoint's IP changes, and rejected outright by MAC-denying socket proxies such as sockguard's default policy). A MAC is now carried over only when it was configured container-wide (docker run --mac-address/ compose's service-levelmac_address:, i.e.Config.MacAddress), and only onto the container's primary network; daemon-assigned MACs are left for the daemon to regenerate. - Locally-built images no longer spam nightly error counts. Containers running an image with no registry-hosted digest (built locally or
docker loaded) were still queried against the registry on every watch cycle, producing a 401 that got counted as a watch error. Drydock now detects images with noRepoDigestsat discovery/refresh time and skips the registry lookup for them entirely. - The digest-watch throttling warning no longer prints
with domain undefinedfor unprefixed Docker Hub image references (e.g.nginx); it now showsdocker.io.
Known limitations
- If the rollback itself fails (for example the backup image can't be pulled, or the replacement container never becomes healthy), that failure is recorded in the update-operations store and audit log but does not yet trigger a push notification — check the container's operation history or the audit log after a failed update to confirm whether the rollback actually succeeded.
- A per-network
mac_address(composenetworks.<net>.mac_address) set on a non-primary network cannot be distinguished from a daemon-assigned MAC via the Docker API today — the daemon persists the desired MAC internally but never exposes it indocker inspectoutput — so it is not preserved across recreates; the daemon assigns a fresh MAC for that network instead.
1.5.2-rc.5 — 2026-07-13
Fixed
- Orphaned replacement container after a failed post-create network connect.
createContainercreated the replacement container and then connected it to any additional networks in the sametry; if a network connect failed (for example a static-IP endpoint config rejected by a socket proxy), the error was rethrown without exposing the created container, so nothing could clean it up. The renamed original (<name>-old-<timestamp>) stayed parked while the orphanedCreated-state replacement squatted the real name. Every rollback consumer — the regular Docker update executor, self-update, health-monitor auto-rollback, the backup-restore API, and Docker Compose rollback restore — now recovers the created container from the error and best-effort stops + force-removes it before restoring the original container's name, tolerating cleanup failures with a warning rather than masking the original error. The same recovery applies when the replacement is created successfully but then fails to start. - Repeated failed updates could cascade into a second rename. If a container was already left renamed
-old-<timestamp>by a prior failed update, a subsequent update attempt against it now fails immediately with a clear "needs manual cleanup" error naming the true canonical name (even through nested renames) instead of renaming and recreating again on top of the unresolved failure. The same guard protects drydock's own self-update. - Replacement containers no longer inherit the previous container's auto-assigned MAC address. The recreate path copied each network endpoint's operational
MacAddressstraight fromdocker inspect, permanently re-pinning a MAC the daemon had generated (stale once the endpoint's IP changes, and rejected outright by MAC-denying socket proxies such as sockguard's default policy). A MAC is now carried over only when it was configured container-wide (docker run --mac-address/ compose's service-levelmac_address:, i.e.Config.MacAddress), and only onto the container's primary network; daemon-assigned MACs are left for the daemon to regenerate. - Locally-built images no longer spam nightly error counts. Containers running an image with no registry-hosted digest (built locally or
docker loaded) were still queried against the registry on every watch cycle, producing a 401 that got counted as a watch error. Drydock now detects images with noRepoDigestsat discovery/refresh time and skips the registry lookup for them entirely. - The digest-watch throttling warning no longer prints
with domain undefinedfor unprefixed Docker Hub image references (e.g.nginx); it now showsdocker.io.
Changed
- Docs: documented the socket-proxy requirements for recreating containers with a static IP or MAC address (macvlan, custom networks). Tecnativa/docker-socket-proxy needs both
POST=1andNETWORKS=1for containers on more than one network (the extraPOST /networks/{id}/connectcalls); sockguard needsrequest_body.network.allow_endpoint_config: trueto permit endpoint configs that specify a static IP or MAC address — and as of sockguard v1.5.0 and later (forthcoming at the time of this release), that requirement also applies toPOST /containers/create, so single-network macvlan/static-IP containers need the policy set too, not just multi-network ones (older sockguard versions enforce the policy only at network-connect time). Added a matching FAQ entry for containers left renamed-old-<timestamp>after a failed update, including a note for operators who intentionally name a container in a way that collides with drydock's own rollback naming convention.
Known limitations
- If the rollback itself fails (for example the backup image can't be pulled, or the replacement container never becomes healthy), that failure is recorded in the update-operations store and audit log but does not yet trigger a push notification — check the container's operation history or the audit log after a failed update to confirm whether the rollback actually succeeded.
- A per-network
mac_address(composenetworks.<net>.mac_address) set on a non-primary network cannot be distinguished from a daemon-assigned MAC via the Docker API today — the daemon persists the desired MAC internally but never exposes it indocker inspectoutput — so it is not preserved across recreates; the daemon assigns a fresh MAC for that network instead.
1.5.2-rc.4 — 2026-07-12
Fixed
- Tooltip text now wraps inside a bounded popup instead of rendering one screen-wide line — the shared tooltip directive had
white-space: nowrapand nomax-width, so the pinned-tag "Newer available" insight tooltip (and any other long tooltip) rendered as a single ~130-character line off the edge of the viewport (#498). - The pinned-tag insight badge no longer clips in the containers list. Centered with no
max-width, it could overflow its narrow table column and get hard-clipped on both sides; it now truncates gracefully with a trailing ellipsis instead (#498). - Shortened the pinned-tag insight tooltip copy so it reads cleanly at the tooltip's new bounded width (#498).
1.5.2-rc.3 — 2026-07-11
Added
- 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. What's new is exposing this comparison at all for pinned tags, since the pin gate itself already blocks acting on any of it. 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, so nothing new fires because of it. On by default; opt out per watcher withDD_WATCHER_{name}_TAG_PIN_INFO=false. Surfaced in the UI as an informational "Newer available" badge next to the existing update-state badges.
Fixed
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.
1.5.2-rc.2 — 2026-07-10
Fixed
- Pinned semver tags (e.g.
nginx:1.25.3) 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, leaving them with no update detection at all and a misleading "compared by digest only" notice on every pinned container (#498). Version climbing for pinned tags remains opt-in viadd.tag.includeordd.tag.family=loose. Note: in agent deployments, agents perform the registry checks — update agents alongside the controller to restore digest detection for agent-watched containers. - The "no update detection" notice shown when
dd.watch.digest=falseis explicitly set no longer claims digest comparison is happening. - Removed the unreachable flat
tagFamilyimgset config key (env-derived keys are lowercased, so it could never match); the documentedDD_WATCHER_{watcher}_IMGSET_{name}_TAG_FAMILYform is unaffected.
1.5.2-rc.1 — 2026-07-10
Fixed
- 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.
1.5.1 — 2026-07-09
Consolidates the 1.5.1-rc.1 … 1.5.1-rc.6 prereleases. Users upgrading from 1.5.0
get everything below; users already on a 1.5.1 release candidate get the rc.6 fixes.
Added
-
Container software version in the detail panels and a new Version column in the containers table. Drydock now surfaces the application version baked into an image — read from the
org.opencontainers.image.versionOCI label, falling back to the running container's inspect metadata — asimage.softwareVersion. It appears in the container side panel, the full-page detail view, and a new Version column in the containers table. The existing Tag column (column keyversion, preserved so saved column preferences keep working) continues to show the image tag; the new Version column showsimage.softwareVersion, falling back to the tag when no software version is available.dd.inspect.tag.pathnow dual-writes the extracted value intoimage.softwareVersionas well as overwriting the image tag, so the Version column is populated for inspect-path containers with no label change needed. The Version column is visible by default for new installs; existing users have it inserted into their saved column list automatically on first load after upgrading. (#209) -
dd.inspect.tag.version-onlyopt-in label. Whendd.inspect.tag.pathis set, the extracted value normally overwrites the image tag (enabling update detection against the semver embedded in the running container). Settingdd.inspect.tag.version-only=trueroutes the extracted value toimage.softwareVersiononly, leaving the real image tag intact for update detection. This is useful when the inspect path carries a displayable application version that differs in format from the registry tag — the Version column shows it without disrupting how drydock matches updates. The default (tag overwrite) is unchanged when the label is absent. (#209) -
Intermediate release notes between the running and target version. When a container is several versions behind, drydock now fetches the releases between the running tag (exclusive) and the update target (inclusive) and shows them in the release-notes popover. Best-effort and semver-only — date tags and rolling tags (
latest,stable) fall back to the standard two-panel view. Cap the range withDD_RELEASE_NOTES_MAX_INTERMEDIATE(default20; set to0to disable). When the range exceeds the cap, the popover shows a non-silent "N older releases not shown" notice. Supports the__FILEsecret-file convention (DD_RELEASE_NOTES_MAX_INTERMEDIATE__FILE). (#453) -
New
GET /api/containers/{id}/intermediate-release-notesendpoint. Lazy-loads the intermediate release list on demand when the release-notes popover opens; not embedded in the container model or agent snapshot, so it adds no ongoing payload weight. Acceptsfrom(required) andto(defaults to the container's pending update tag) query parameters. (#453) -
Optional mount-prefix fallback for Docker Compose path matching. When a watched container's resolved compose file path differs from the trigger's configured compose file only by a mount prefix (common with Portainer and bind-mounted compose files), drydock can now match on the trailing
<project-dir>/<file>tail instead of skipping the container. Off by default — enable it per trigger withDD_ACTION_DOCKERCOMPOSE_<name>_MOUNT_PREFIX_FALLBACK=true. It stays opt-in because tail matching cannot distinguish two stacks that share a project-directory name across environments (e.g./prod/myappvs/staging/myapp). (#365) -
$currentReleaseNotestrigger template variable. Trigger templates (notification bodies, command arguments, and the like) can now reference$currentReleaseNotesto include the release notes for the container's currently running version, alongside the existing variable for the update target's notes. (#295) -
Container uptime. The side panel and full-page detail view now show how long a container has been running (from the Docker
State.StartedAttimestamp), and a new opt-in Uptime column can be enabled in the containers table via the column picker. The value updates live and falls back to an em-dash when the start time is unknown. -
Warn log when a
dd.source.repocontainer label shadows a trusted OCI image source label. Adding add.source.repolabel to a running container when the image already carries a trustedorg.opencontainers.image.source(ororg.opencontainers.image.url) OCI label silently downgrades source resolution from trusted to untrusted, which drops the GHCR token fallback for release-notes lookups. Drydock now logs awarn-level line each watch cycle when it detects this conflict, naming both repos. (#452) -
Remote agents now report their log level and watcher schedule. The agent handshake (
dd:ack) includeslogLevelandpollInterval(the watcher's cron), so theGET /api/v1/agentsresponse and the Agents view populate these fields for connected agents instead of leaving them blank.
Changed
-
The entire UI is now translatable. The last hardcoded English strings (dashboard widgets, security view, detail panels, host-status labels, the log viewer's invalid-regex notice, and SSE update-failed fallbacks) were extracted into the vue-i18n catalogs, so every surface now resolves through the translation system. Combined with the newly opened community translation project on Crowdin, contributors can translate any part of the interface. The 16 community locales ship with this release, synced from Crowdin.
-
Maturity gate counts from the registry publish date when trustworthy. For Docker Hub and GHCR (including lscr.io), the gate now measures elapsed time from the real image push date (
last_updated/updated_at) instead of from when drydock first detected the update. An image that has been public longer thanmaturityMinAgeDaysclears the gate immediately on the first scan that finds it. All other registries expose only the OCI image build date, which is not a reliable push signal, so drydock falls back to its own first-detection timestamp (updateDetectedAt) for those. A trusted publish date is skipped if it fails to parse or is in the future (clock-skew protection). -
DD_RELEASE_NOTES_GITHUB_TOKENis now forwarded to release-notes lookups for repos resolved from add.source.repocontainer label or a persistedcontainer.sourceRepovalue. Previously these sources were always fetched anonymously. The GHCR token fallback stays restricted to trusted sources (OCI image labels and GHCR image paths) and is never sent to a container-label source. Because the dedicated token can be sent to a repo named by a container label, scope it narrowly: a classic PAT withpublic_reposcope only, or a fine-grained PAT with read-only Contents permission limited to public repositories and no write or account permissions. (#452) -
Container validation now tolerates fields written by newer drydock versions. The store validator no longer rejects unknown keys, so a
dd.jsonwritten by a newer release stays readable after a downgrade. Note: this protects downgrades from v1.5.1 onward — rolling back from v1.5.1 to v1.5.0 (which predates this change) still requires removing the newdetails.startedAtandimage.softwareVersionfields fromdd.json, since v1.5.0 rejects them. -
Coverage reporting moved from Codecov to Qlty Cloud. Part of the org-wide consolidation onto Qlty (one vendor for code quality and coverage). CI now publishes the normalized app/ui lcov reports to Qlty Cloud via GitHub OIDC — no stored coverage token — replacing the Codecov upload and
codecov.yml. The vitest 100% coverage thresholds in the app/ and ui/ test suites remain the enforced gate; the README coverage badge now points at Qlty.
Fixed
-
Maturity gate (
maturityMode: 'mature') never triggered; the first-detection timestamp was never computed. The function that stampsupdateDetectedAtreturnedundefinedimmediately wheneverupdateAvailablewasfalse, which is always the case while maturity suppression is active, so the timestamp was never computed and the maturity clock never started. Containers blocked by the maturity gate remained permanently "maturing" and never became update-available. -
Maturity clock reset on every container recreation. When a container was stopped and recreated (Portainer stack redeploy,
docker compose down && up), its new Docker container ID caused drydock to treat it as a fresh container, resettingupdateDetectedAtto zero. Containers that are frequently redeployed could never accumulate enough age to clear the gate. The clock is now keyed on a stable container identity and survives recreation as long as the same update remains pending, including the common case where a slow image pull means the replacement container isn't yet visible when the old one is pruned. -
A changed update candidate now restarts the maturity soak. When a new image digest or tag is published while an earlier update is still inside the maturity window, the gate restarts the clock for the new candidate instead of letting it inherit the previous candidate's elapsed time. A freshly pushed image always soaks for the full
maturityMinAgeDaysrather than being treated as already mature. (Local watches previously kept the original detection time here; remote-agent containers already behaved this way.) -
In-memory container cache key collision. The cache key joined watcher name and container name with
_, somy_prod+nginxandmy+prod_nginxproduced the same key. A wrong cache hit could apply a stale security scan result or maturity timestamp from one container to a different container, most visibly after a recreation event. The separator is now::. -
Docker and Docker Compose actions can pull private GCR and Google Artifact Registry images again.
getAuthPull()for the GCR and GAR providers returned the raw service-account email as the username and the private key as the password, whichdocker loginrejects, so any action trigger targeting a privategcr.ioor*-docker.pkg.devimage failed to authenticate and could not apply the update. It now returns the_json_keyusername with the service-account JSON as the password, the format Google's registry auth expects (and the same one the token-exchange path already used). -
Quay tag pagination no longer breaks on standard
Linkheaders. Thenext_pagecursor was matched with a greedy pattern that swallowed the trailing>; rel="next"from an RFC 5988Linkheader and corrupted the cursor, so repositories with more than one page of tags could silently stop paginating. The parser now readsnext_pageandlastcursors correctly from both bare-URL and RFC 5988 header forms, and still URL-encodes them to block scope injection. The inherited TrueForge provider gets the same fix. -
Auto-apply update triggers now honor the maintenance window on every detection path. A container update detected through certain code paths could be auto-applied outside the configured maintenance window because the window check was missing on those paths. The gate is now enforced uniformly, so updates only auto-apply inside the window regardless of how the update was detected. (#321)
-
Home Assistant-style PEP 440 nightly tags (and other lossy version formats) no longer masquerade as a stable "suggested pin". Tag suggestions for
latest/untagged containers relied onsemver.coerce()as a last-resort parser, which silently drops any suffix it doesn't understand — a PEP 440 dev/post release (2026.8.0.dev202607050315,1.2.3.post1), an OS-variant suffix (3.11-bullseye), or a hyphenated CalVer date (2024-01-15) all coerced down to a baremajor.minor.patchand got offered as "stable". Suggestions now reject any candidate tag that required this lossy coercion unless the raw tag is itself a bare numeric version (optionallyv-prefixed, 1-3 dot-separated groups) that provably lost nothing. The containers table also now renders the suggested-tag hint through the existingSuggestedTagBadgecomponent (labeled "Suggested" with a tooltip) instead of an unlabeled raw string next to the Digest/NEW badges. (#473) -
"Click to copy" did nothing and logged a TypeError on deployments served over plain HTTP (the common self-hosted LAN setup), because the browser Clipboard API only exists in secure contexts. Copying now falls back to the legacy execCommand technique when the API is missing or rejects, covers the log viewer's Copy button too, and shows a "Copy failed" state instead of failing silently when no copy mechanism works at all. (#472)
-
An open tooltip whose text changed — like the copy button's "Copied" confirmation — stayed stuck on the old text until you moved the mouse away and back. Clicking to copy hid the tooltip outright, so the "Copied"/"Copy failed" state never appeared until a re-hover. Tooltips now update their text in place while open, reposition themselves for the new content, and reappear immediately if the pointer never left. Three call sites that stacked a second tooltip directly onto a copyable tag's root element (the containers table's digest-delta tooltip and two spots in the dashboard's recent-updates widget) now feed their text into the tag's own tooltip through a new
idleTooltipprop instead of clobbering it. (#472) -
The security view now shows release notes for the running image even when no update is pending. The detail panel, table, and card surfaces previously gated the release-notes link behind "an update is available," so a container with no pending update showed nothing even though its current release notes were known. The running-tag notes now appear whenever they exist, and a "View project" link (the source repository) was added alongside them, matching the containers view. (Discussion #295)
-
The dashboard "Recent Updates" widget now uses the shared release-notes and project-link components. It previously rendered a bare release-notes anchor with no project link and no structured current/available notes. It now renders the same icon links as every other surface, fed from the container's
sourceRepo,releaseNotes, andcurrentReleaseNotes. (Discussion #295) -
Completed i18n coverage for the last untranslated UI surfaces. A code-level audit found several strings that still rendered in English for non-English users; they now resolve through the translation catalog: the trigger status badge (
active/inactive), the running/writes-composeyes/nopreview values, the "container actions disabled by server configuration" tooltip, the update-maturity "Available for N days" tooltip (the translate function is now threaded through the container mapper, which previously left the existing catalog keys unused), the grouped "Update All" success toast (which appended a raw Englishin <group>— it now interpolates the group name through a translatable key), the security-view severity tooltips (CRITICAL/HIGH/MEDIUM/LOW), the backup operationunknownfallback label, and the search-bar hint footer connectors. (#329) -
Grouped "Update All" buttons could scroll out of view at moderate desktop widths once the new Version column widened the containers table: columns were only ever laid out at their preferred widths, so the per-stack Update All button — positioned at the far end of the group header row — ended up past the visible edge while everything else looked normal. Tables now shrink columns proportionally toward their minimum widths when space is tight, and the group header's Update All button is additionally pinned to the visible edge, so it stays reachable even when a table legitimately overflows. (Affected the
1.5.1-rc.4/rc.5prereleases only.) (#467)
Security
-
Base image refreshed to clear 24 container-scan CVEs. Bumped the pinned
node:24-alpinebase from a stale digest (Node 24.16.0, Alpine 3.21) to the current digest (Node 24.18.0, Alpine 3.24) and addedlibexpatto the targetedapk upgradeset. This resolves all 11 Node binary CVEs reported by the image scan — including the one critical (CVE-2026-48930) and four high — plus 13 mediumlibexpatCVEs (now2.8.2-r0). A rebuild + rescan confirms zero critical/high/Node/libexpat findings remain. The threebusybox/ssl_clientfindings (CVE-2025-60876, medium) have no upstream fix in Alpine yet and are tracked for a later base bump. All previously pinned Alpine package versions still resolve on 3.24, so the build is otherwise unchanged. -
Custom registry TLS settings now apply to every registry request.
DD_REGISTRY_*_CAFILE,_INSECURE, and_CLIENTCERTwere honored only on the credential handshake for the GAR, GitLab, Mau, DHI, ACR, and ECR providers; the follow-up tag-list, manifest, and blob calls fell back to the system CA bundle. The custom TLS agent is now propagated to those calls, including the anonymous (no-credential) code paths in GAR, GCR, and Quay, so a private CA or aninsecuresetting is enforced end to end rather than only while fetching the token. -
Hook command environment values are sanitized against shell injection. Lifecycle hook commands run through
/bin/sh -c, and registry-controlled values (image name, tag, update digest) flowed into the hook environment unsanitized, so a crafted tag could inject shell commands into a hook script that expanded those variables unquoted. The values are now scrubbed of shell metacharacters, matching the sanitization thecommandaction already applies. -
DD_SESSION_SECRET__FILEis now honored. The session secret was read straight fromprocess.env, bypassing the__FILEsecret-file resolution every other secret uses, so the documented file form was silently ignored and the instance fell back to a generated secret on every restart. It now reads the resolved value, so a secret supplied viaDD_SESSION_SECRET__FILEis used (and, as with every other secret, the file form wins when both the file and the bare variable are set). -
Secret files are checked for unsafe permissions and trailing newlines. When a
DD_*__FILEsecret is readable by group or others, drydock logs a non-fatal warning recommendingchmod 600(skipped on Windows, where the mode bits are not meaningful). File-sourced secret values are also trimmed of a trailing newline so an editor- orecho-added\ncan't corrupt a credential, matching the common Docker*_FILEconvention. -
Debug dump and container environment no longer leak credentials. The debug dump's redaction missed SMTP passwords (
*_PASSkeys) and webhook URLs with embedded secrets, both of which appeared in the dumped environment for any authenticated user. They are now redacted, and the same*_PASSgap is closed for the container runtime environment shown via/api/containers. Registry usernames and service hostnames stay visible by design, since they aren't secrets and aid debugging. -
Suppressed a ZAP DAST false positive (rule 10049, Storable and Cacheable Content). The baseline scan flagged cacheable static responses that are not sensitive; the rule is now downgraded in
.zap/rules.tsvand the JSON-to-SARIF converter handles the suppression so the security workflow stays green without masking real findings. (#374)
Upgrade Notes
- One-time notification burst on first scan after upgrade (Docker Hub / GHCR containers only). Containers on Docker Hub or GHCR whose pending update is already older than
maturityMinAgeDayswill clear the maturity gate immediately on the first poll after upgrading. Notification triggers inalwaysmode will fire once for each such container. Action triggers (docker,docker-compose,command) will also fire, so containers previously held by the gate may be updated automatically on that first poll. Review your active action-trigger configuration before upgrading if you want to control the timing. This is expected behavior: those images were already mature; the gate was simply unaware of it.
Known Issues
- Home Assistant
update.dd_containerentities can show "Unknown" via the MQTT + HASS trigger. Thelatest_versiontemplate renders empty when a container has no pending update, blanking the entity instead of reporting that it is up to date. Pre-existing; the fix ships in v1.6. (#491)
1.5.1-rc.6 — 2026-07-05
Fixed
- Home Assistant-style PEP 440 nightly tags (and other lossy version formats) no longer masquerade as a stable "suggested pin". Tag suggestions for
latest/untagged containers relied onsemver.coerce()as a last-resort parser, which silently drops any suffix it doesn't understand — a PEP 440 dev/post release (2026.8.0.dev202607050315,1.2.3.post1), an OS-variant suffix (3.11-bullseye), or a hyphenated CalVer date (2024-01-15) all coerced down to a baremajor.minor.patchand got offered as "stable". Suggestions now reject any candidate tag that required this lossy coercion unless the raw tag is itself a bare numeric version (optionallyv-prefixed, 1-3 dot-separated groups) that provably lost nothing. The containers table also now renders the suggested-tag hint through the existingSuggestedTagBadgecomponent (labeled "Suggested" with a tooltip) instead of an unlabeled raw string next to the Digest/NEW badges. (#473) - "Click to copy" did nothing and logged a TypeError on deployments served over plain HTTP (the common self-hosted LAN setup), because the browser Clipboard API only exists in secure contexts. Copying now falls back to the legacy execCommand technique when the API is missing or rejects, covers the log viewer's Copy button too, and shows a "Copy failed" state instead of failing silently when no copy mechanism works at all. (#472)
- An open tooltip whose text changed — like the copy button's "Copied" confirmation — stayed stuck on the old text until you moved the mouse away and back. Clicking to copy hid the tooltip outright, so the "Copied"/"Copy failed" state never appeared until a re-hover. Tooltips now update their text in place while open, reposition themselves for the new content, and reappear immediately if the pointer never left. (#472)
- Three call sites bound a second tooltip directly onto a copyable tag's root element (the containers table's digest-delta tooltip and two spots in the dashboard's recent-updates widget), so it silently clobbered the tag's own copy-state tooltip — duplicate listeners and, after the content-change re-show above, occasional spurious re-shows while hovering. The informative text now flows into the tag's own tooltip via a new
idleTooltipprop instead of stacking a second directive on the same element. (#472)
1.5.1-rc.5 — 2026-07-02
Fixed
- Grouped "Update All" buttons could scroll out of view after upgrading to rc.4. The new Version column added in rc.4 widened the containers table for existing users, and at moderate desktop widths the table overflowed horizontally in a way its responsive column-hiding never accounted for: columns were only ever laid out at their preferred widths, so the per-stack Update All button — positioned at the far end of the group header row — ended up past the visible edge while everything else looked normal. Tables now shrink columns proportionally toward their minimum widths when space is tight (matching the widths the responsive logic already budgets for), and the group header's Update All button is additionally pinned to the visible edge, so it stays reachable even when a table legitimately overflows. (#467)
1.5.1-rc.4 — 2026-06-29
Added
-
Optional mount-prefix fallback for Docker Compose path matching. When a watched container's resolved compose file path differs from the trigger's configured compose file only by a mount prefix (common with Portainer and bind-mounted compose files), drydock can now match on the trailing
<project-dir>/<file>tail instead of skipping the container. Off by default — enable it per trigger withDD_ACTION_DOCKERCOMPOSE_<name>_MOUNTPREFIXFALLBACK=true. It stays opt-in because tail matching cannot distinguish two stacks that share a project-directory name across environments (e.g./prod/myappvs/staging/myapp). (#365) -
${currentReleaseNotes}trigger template variable. Trigger templates (notification bodies, command arguments, and the like) can now reference${currentReleaseNotes}to include the release notes for the container's currently running version, alongside the existing variable for the update target's notes. (#295) -
Container software version in the detail panels and a new Version column in the containers table. Drydock now surfaces the application version baked into an image — read from the
org.opencontainers.image.versionOCI label, falling back to the running container's inspect metadata — asimage.softwareVersion. It appears in the container side panel, the full-page detail view, and a new Version column in the containers table. The existing Tag column (column keyversion, preserved so saved column preferences keep working) continues to show the image tag; the new Version column showsimage.softwareVersion, falling back to the tag when no software version is available.dd.inspect.tag.pathnow dual-writes the extracted value intoimage.softwareVersionas well as overwriting the image tag, so the Version column is populated for inspect-path containers with no label change needed. The Version column is visible by default for new installs; existing users have it inserted into their saved column list automatically on first load after upgrading. (#209) -
dd.inspect.tag.version-onlyopt-in label. Whendd.inspect.tag.pathis set, the extracted value normally overwrites the image tag (enabling update detection against the semver embedded in the running container). Settingdd.inspect.tag.version-only=trueroutes the extracted value toimage.softwareVersiononly, leaving the real image tag intact for update detection. This is useful when the inspect path carries a displayable application version that differs in format from the registry tag — the Version column shows it without disrupting how drydock matches updates. The default (tag overwrite) is unchanged when the label is absent. (#209) -
Container uptime. The side panel and full-page detail view now show how long a container has been running (from the Docker
State.StartedAttimestamp), and a new opt-in Uptime column can be enabled in the containers table via the column picker. The value updates live and falls back to an em-dash when the start time is unknown.
Changed
- Container validation now tolerates fields written by newer drydock versions. The store validator no longer rejects unknown keys, so a
dd.jsonwritten by a newer release stays readable after a downgrade. Note: this protects downgrades from v1.5.1 onward — rolling back from v1.5.1 to v1.5.0 (which predates this change) still requires removing the newdetails.startedAtandimage.softwareVersionfields fromdd.json, since v1.5.0 rejects them.
Fixed
- Completed i18n coverage for the last untranslated UI surfaces. A code-level audit found several strings that still rendered in English for non-English users; they now resolve through the translation catalog: the trigger status badge (
active/inactive), the running/writes-composeyes/nopreview values, the "container actions disabled by server configuration" tooltip, the update-maturity "Available for N days" tooltip (the translate function is now threaded through the container mapper, which previously left the existing catalog keys unused), the grouped "Update All" success toast (which appended a raw Englishin <group>— it now interpolates the group name through a translatable key), the security-view severity tooltips (CRITICAL/HIGH/MEDIUM/LOW), the backup operationunknownfallback label, and the search-bar hint footer connectors. The new English catalog keys ship now; the 16 community locales fill in through the normal Crowdin sync after release. (#329)
Security
- Base image refreshed to clear 24 container-scan CVEs. Bumped the pinned
node:24-alpinebase from a stale digest (Node 24.16.0, Alpine 3.21) to the current digest (Node 24.18.0, Alpine 3.24) and addedlibexpatto the targetedapk upgradeset. This resolves all 11 Node binary CVEs reported by the image scan — including the one critical (CVE-2026-48930) and four high — plus 13 mediumlibexpatCVEs (now2.8.2-r0). A rebuild + rescan confirms zero critical/high/Node/libexpat findings remain. The threebusybox/ssl_clientfindings (CVE-2025-60876, medium) have no upstream fix in Alpine yet and are tracked for a later base bump. All previously pinned Alpine package versions still resolve on 3.24, so the build is otherwise unchanged.
1.5.1-rc.3 — 2026-06-28
Added
-
Intermediate release notes between the running and target version. When a container is several versions behind, drydock now fetches the releases between the running tag (exclusive) and the update target (inclusive) and shows them in the release-notes popover. Best-effort and semver-only — date tags and rolling tags (
latest,stable) fall back to the standard two-panel view. Cap the range withDD_RELEASE_NOTES_MAX_INTERMEDIATE(default20; set to0to disable). When the range exceeds the cap, the popover shows a non-silent "N older releases not shown" notice. Supports the__FILEsecret-file convention (DD_RELEASE_NOTES_MAX_INTERMEDIATE__FILE). (#453) -
New
GET /api/containers/{id}/intermediate-release-notesendpoint. Lazy-loads the intermediate release list on demand when the release-notes popover opens; not embedded in the container model or agent snapshot, so it adds no ongoing payload weight. Acceptsfrom(required) andto(defaults to the container's pending update tag) query parameters. (#453) -
Warn log when a
dd.source.repocontainer label shadows a trusted OCI image source label. Adding add.source.repolabel to a running container when the image already carries a trustedorg.opencontainers.image.source(ororg.opencontainers.image.url) OCI label silently downgrades source resolution from trusted to untrusted, which drops the GHCR token fallback for release-notes lookups. Drydock now logs awarn-level line each watch cycle when it detects this conflict, naming both repos. (#452)
Changed
-
DD_RELEASE_NOTES_GITHUB_TOKENis now forwarded to release-notes lookups for repos resolved from add.source.repocontainer label or a persistedcontainer.sourceRepovalue. Previously these sources were always fetched anonymously. The GHCR token fallback stays restricted to trusted sources (OCI image labels and GHCR image paths) and is never sent to a container-label source. Because the dedicated token can be sent to a repo named by a container label, scope it narrowly: a classic PAT withpublic_reposcope only, or a fine-grained PAT with read-only Contents permission limited to public repositories and no write or account permissions. (#452) -
Re-synced the UI translation catalogs from Crowdin. The 16 target-locale
containerComponents.jsonfiles were regenerated from the Crowdin project so their key order tracks the English source catalog, keeping the on-disk catalogs and the translation platform in lockstep as community translations land.
1.5.1-rc.2 — 2026-06-28
Changed
- The entire UI is now translatable. The last hardcoded English strings (dashboard widgets, security view, detail panels, host-status labels, the log viewer's invalid-regex notice, and SSE update-failed fallbacks) were extracted into the vue-i18n catalogs, so every surface now resolves through the translation system. Combined with the newly opened community translation project on Crowdin, contributors can translate any part of the interface.
Fixed
-
The security view now shows release notes for the running image even when no update is pending. The detail panel, table, and card surfaces previously gated the release-notes link behind "an update is available," so a container with no pending update showed nothing even though its current release notes were known. The running-tag notes now appear whenever they exist, and a "View project" link (the source repository) was added alongside them, matching the containers view. (Discussion #295)
-
The dashboard "Recent Updates" widget now uses the shared release-notes and project-link components. It previously rendered a bare release-notes anchor with no project link and no structured current/available notes. It now renders the same icon links as every other surface, fed from the container's
sourceRepo,releaseNotes, andcurrentReleaseNotes. (Discussion #295) -
Auto-apply update triggers now honor the maintenance window on every detection path. A container update detected through certain code paths could be auto-applied outside the configured maintenance window because the window check was missing on those paths. The gate is now enforced uniformly, so updates only auto-apply inside the window regardless of how the update was detected. (#321)
Security
- Suppressed a ZAP DAST false positive (rule 10049, Storable and Cacheable Content). The baseline scan flagged cacheable static responses that are not sensitive; the rule is now downgraded in
.zap/rules.tsvand the JSON-to-SARIF converter handles the suppression so the security workflow stays green without masking real findings. (#374)
1.5.1-rc.1 — 2026-06-26
Added
- Remote agents now report their log level and watcher schedule. The agent handshake (
dd:ack) includeslogLevelandpollInterval(the watcher's cron), so theGET /api/v1/agentsresponse and the Agents view populate these fields for connected agents instead of leaving them blank.
Changed
-
Coverage reporting moved from Codecov to Qlty Cloud. Part of the org-wide consolidation onto Qlty (one vendor for code quality and coverage). CI now publishes the normalized app/ui lcov reports to Qlty Cloud via GitHub OIDC — no stored coverage token — replacing the Codecov upload and
codecov.yml. The vitest 100% coverage thresholds in the app/ and ui/ test suites remain the enforced gate; the README coverage badge now points at Qlty. -
Maturity gate counts from the registry publish date when trustworthy. For Docker Hub and GHCR (including lscr.io), the gate now measures elapsed time from the real image push date (
last_updated/updated_at) instead of from when drydock first detected the update. An image that has been public longer thanmaturityMinAgeDaysclears the gate immediately on the first scan that finds it. All other registries expose only the OCI image build date, which is not a reliable push signal, so drydock falls back to its own first-detection timestamp (updateDetectedAt) for those. A trusted publish date is skipped if it fails to parse or is in the future (clock-skew protection).
Fixed
-
Maturity gate (
maturityMode: 'mature') never triggered; the first-detection timestamp was never computed. The function that stampsupdateDetectedAtreturnedundefinedimmediately wheneverupdateAvailablewasfalse, which is always the case while maturity suppression is active, so the timestamp was never computed and the maturity clock never started. Containers blocked by the maturity gate remained permanently "maturing" and never became update-available. -
Maturity clock reset on every container recreation. When a container was stopped and recreated (Portainer stack redeploy,
docker compose down && up), its new Docker container ID caused drydock to treat it as a fresh container, resettingupdateDetectedAtto zero. Containers that are frequently redeployed could never accumulate enough age to clear the gate. The clock is now keyed on a stable container identity and survives recreation as long as the same update remains pending, including the common case where a slow image pull means the replacement container isn't yet visible when the old one is pruned. -
In-memory container cache key collision. The cache key joined watcher name and container name with
_, somy_prod+nginxandmy+prod_nginxproduced the same key. A wrong cache hit could apply a stale security scan result or maturity timestamp from one container to a different container, most visibly after a recreation event. The separator is now::. -
A changed update candidate now restarts the maturity soak. When a new image digest or tag is published while an earlier update is still inside the maturity window, the gate restarts the clock for the new candidate instead of letting it inherit the previous candidate's elapsed time. A freshly pushed image always soaks for the full
maturityMinAgeDaysrather than being treated as already mature. (Local watches previously kept the original detection time here; remote-agent containers already behaved this way.) -
Docker and Docker Compose actions can pull private GCR and Google Artifact Registry images again.
getAuthPull()for the GCR and GAR providers returned the raw service-account email as the username and the private key as the password, whichdocker loginrejects, so any action trigger targeting a privategcr.ioor*-docker.pkg.devimage failed to authenticate and could not apply the update. It now returns the_json_keyusername with the service-account JSON as the password, the format Google's registry auth expects (and the same one the token-exchange path already used). -
Quay tag pagination no longer breaks on standard
Linkheaders. Thenext_pagecursor was matched with a greedy pattern that swallowed the trailing>; rel="next"from an RFC 5988Linkheader and corrupted the cursor, so repositories with more than one page of tags could silently stop paginating. The parser now readsnext_pageandlastcursors correctly from both bare-URL and RFC 5988 header forms, and still URL-encodes them to block scope injection. The inherited TrueForge provider gets the same fix.
Security
-
Custom registry TLS settings now apply to every registry request.
DD_REGISTRY_*_CAFILE,_INSECURE, and_CLIENTCERTwere honored only on the credential handshake for the GAR, GitLab, Mau, DHI, ACR, and ECR providers; the follow-up tag-list, manifest, and blob calls fell back to the system CA bundle. The custom TLS agent is now propagated to those calls, including the anonymous (no-credential) code paths in GAR, GCR, and Quay, so a private CA or aninsecuresetting is enforced end to end rather than only while fetching the token. -
Hook command environment values are sanitized against shell injection. Lifecycle hook commands run through
/bin/sh -c, and registry-controlled values (image name, tag, update digest) flowed into the hook environment unsanitized, so a crafted tag could inject shell commands into a hook script that expanded those variables unquoted. The values are now scrubbed of shell metacharacters, matching the sanitization thecommandaction already applies. -
DD_SESSION_SECRET__FILEis now honored. The session secret was read straight fromprocess.env, bypassing the__FILEsecret-file resolution every other secret uses, so the documented file form was silently ignored and the instance fell back to a generated secret on every restart. It now reads the resolved value, so a secret supplied viaDD_SESSION_SECRET__FILEis used (and, as with every other secret, the file form wins when both the file and the bare variable are set). -
Secret files are checked for unsafe permissions and trailing newlines. When a
DD_*__FILEsecret is readable by group or others, drydock logs a non-fatal warning recommendingchmod 600(skipped on Windows, where the mode bits are not meaningful). File-sourced secret values are also trimmed of a trailing newline so an editor- orecho-added\ncan't corrupt a credential, matching the common Docker*_FILEconvention. -
Debug dump and container environment no longer leak credentials. The debug dump's redaction missed SMTP passwords (
*_PASSkeys) and webhook URLs with embedded secrets, both of which appeared in the dumped environment for any authenticated user. They are now redacted, and the same*_PASSgap is closed for the container runtime environment shown via/api/containers. Registry usernames and service hostnames stay visible by design, since they aren't secrets and aid debugging.
Upgrade Notes
- One-time notification burst on first scan after upgrade (Docker Hub / GHCR containers only). Containers on Docker Hub or GHCR whose pending update is already older than
maturityMinAgeDayswill clear the maturity gate immediately on the first poll after upgrading. Notification triggers inalwaysmode will fire once for each such container. Action triggers (docker,docker-compose,command) will also fire, so containers previously held by the gate may be updated automatically on that first poll. Review your active action-trigger configuration before upgrading if you want to control the timing. This is expected behavior: those images were already mature; the gate was simply unaware of it.
1.5.0 — 2026-06-22
Added
-
Trigger taxonomy split —
DD_ACTION_*andDD_NOTIFICATION_*prefixes. Action triggers (Docker, Docker Compose, Command) now useDD_ACTION_*/dd.action.*; messaging triggers (Slack, SMTP, Discord, Telegram, ntfy, Pushover, and all others) useDD_NOTIFICATION_*/dd.notification.*. All three families remain interchangeable at runtime through v1.7.0. A migration CLI (drydock config migrate --source trigger) rewrites existing configs automatically. The legacyDD_TRIGGER_*/dd.trigger.*aliases are deprecated (removal targeted v1.7.0). -
Experimental Portwing edge-agent mode. Agents behind NAT or firewalls can dial out to the controller over a persistent
wss://WebSocket instead of requiring an inbound connection. Enable withDD_EXPERIMENTAL_PORTWING=true. Uses Ed25519 public-key challenge-response auth; operator key management is exposed at/api/v1/portwing/keys. -
Real-time container log viewer. WebSocket-based live log streaming from Docker containers in the UI — ANSI color rendering, automatic JSON pretty-printing, free-text/regex search, stdout/stderr and log-level filtering, copy to clipboard, and gzip download. Available in the container detail panel and at
/containers/:id/logs. -
Notification outbox with retry and dead-letter queue. Failed notification deliveries are persisted and retried with exponential backoff + jitter. After 5 attempts (configurable), entries move to a dead-letter queue. New
/api/notifications/outboxREST surface and a dedicated UI page let operators list, retry, or discard entries. -
Durable update queue with restart recovery. Container updates are queued server-side with per-trigger concurrency limits. Queued and mid-pull operations survive controller restarts and are recovered automatically. New cancel endpoint (
POST /api/operations/:id/cancel) accepts both queued and in-flight operations. A global concurrent-update cap (DD_UPDATE_MAX_CONCURRENT) is available. -
Customizable dashboard. Drag-to-reorder, resize, and per-widget visibility toggles. New Resource Usage widget shows fleet-wide CPU and memory with top-N consumers, fed by a live fleet-stats SSE stream (
GET /api/v1/stats/summary/stream). -
Diagnostic debug dump. One-click export of redacted system state from Configuration > Diagnostics — runtime metadata, component state, Docker diagnostics, recent events, and
DD_*env vars, with sensitive values auto-redacted. Available atGET /api/v1/debug/dump. -
SSE Last-Event-ID replay. Every broadcast event carries a monotonic
<bootId>:<counter>id; clients reconnecting withLast-Event-IDreceive missed events from a 5-minute ring buffer. Clients that fall behind the buffer receive add:resync-requiredevent. -
Update-eligibility blockers on container rows. Sixteen structured blocker reasons are surfaced inline on the Containers list, so users see why a container isn't updating without opening the detail drawer. Hard blockers lock the Update button; soft blockers show a warn-and-confirm modal.
-
Per-agent Home Assistant MQTT topic segmentation (
DD_NOTIFICATION_MQTT_<name>_HASS_AGENTTOPICSEGMENT). Prevents two agents sharing the default watcher name from overwriting each other's topics. Opt-in for v1.5.x; targets default-on in v1.7.0. -
Full i18n coverage across the UI. All hardcoded strings migrated to per-namespace JSON catalogs with 17 locale options (de, es, fr, it, nl, pl, pt-BR, tr, zh-CN, zh-TW, ar, ja, ko, ru, uk, vi, plus English). Human translations synced from Crowdin.
-
Colored startup banner. Renders the whale logo as a truecolor half-block art print on interactive terminals; auto-suppressed when not a TTY or when
NO_COLORis set.
Changed
-
Action trigger default mode changed to
AUTO=oninclude. Action triggers no longer auto-update every container by default; an explicitdd.action.includelabel is required. Notification triggers are unaffected. -
Default watcher cron relaxed from hourly to every 6 hours. Reduces registry pressure for the common case; override with
DD_WATCHER_{name}_CRON. -
Consolidated security scanning on Grype; dropped Snyk. Grype now scans both the built container image and all six npm lockfiles. Snyk's GitHub SCM integration,
.snykpolicy file,security-snyk-weekly.yml, and related scripts are removed. Free gates (CodeQL, dependency-review, OpenSSF Scorecard, zizmor) continue to run on every PR. -
Healthcheck binary replaces curl. Default HEALTHCHECK now uses a 65 KB static C binary (
/bin/healthcheck). curl is retained for user-defined overrides through v1.6.0 (removal in v1.7.0). -
DD_SESSION_SECRETauto-generated and persisted when unset. On first boot without the variable set, drydock generates 64 random bytes and persists them in the store so sessions survive restarts. The env var still takes precedence. -
Tag-last release pipeline. The git tag is pushed only after the Docker image is built, pushed, signed (cosign), and attested (SLSA). If the tag exists, the image exists.
Fixed
-
Containers pinned to a fully-specified semver tag no longer climb to newer versions by default. Tags classified as
specificprecision now track digest changes only. Opt in to semver climbing withdd.tag.includeordd.tag.family=loose. -
Multi-agent deployments no longer produce spurious 409 conflicts, cross-agent container contamination, or persistent "0 running containers" in the controller UI. Root causes: the controller's local watcher was pruning remote-agent rows; agent watcher snapshots were lost when the SSE stream was half-open mid-reconnect. Fixed by scoping store reads and key derivation to
{ agent, watcher }at every call site, and having agents replay the latest snapshot to each new SSE client immediately afterdd:ack. -
Self-update overlay holds until the swap is complete. Three compounding bugs (premature overlay dismissal, unreachable finalize callback, helper container appearing in the watcher list) are fixed. A new unauthenticated
/api/v1/self-update/{operationId}/statusendpoint lets the UI poll during the restart window. -
Spurious "update failed" and "update available" notifications eliminated. False notifications arising from concurrent duplicate requests (409 race), post-update stale watcher scans, timed-out operation TTLs, and digest/batch path suppress-lifecycle gaps are all closed.
-
Live log viewer and WebSocket upgrades now work behind TLS-terminating reverse proxies. WebSocket upgrades and the log-stream origin check now honor
X-Forwarded-Host/X-Forwarded-ProtowhenDD_SERVER_TRUSTPROXYis set. -
Docker Compose update no longer destroys the running container on failure. A pre-flight architecture-compatibility check runs before removing the old container; if the recreate still fails, the original container is restored from its captured spec.
-
Registry 429/503 handling with Retry-After and per-host token bucket. Every registry HTTP call retries up to 3 times with exponential backoff honoring upstream
Retry-After; a per-host token bucket prevents self-inflicted rate limiting during large cron cycles. -
Standard Bearer token exchange now works for Chainguard, Codeberg/Forgejo, and other v2 registries that issue a WWW-Authenticate challenge.
callRegistryimplements the spec-compliant challenge-response flow with realm-host validation.
Security
-
Command trigger no longer inherits the full process environment. Child processes receive a fixed allowlist of system variables plus drydock-provided container vars. Additional variables can be whitelisted with
DD_ACTION_COMMAND_{name}_ENV. -
HTTP trigger blocks cloud metadata endpoints. Requests resolving to
169.254.0.0/16,fe80::/10, and related link-local ranges (including IPv4-mapped and IPv4-compatible spellings) are rejected before sending. Opt-out available viaDD_NOTIFICATION_HTTP_{name}_ALLOWMETADATA=true. -
CSRF same-origin enforcement hardened. Forwarded-host headers are now trusted only when Express
trust proxyis enabled;/authmutations (logout, remember) are now covered by the same-origin check; andDD_SERVER_TRUSTPROXY=true(all hops) now emits a startup warning recommending a hop count. -
Security digest templates no longer evaluated as JavaScript.
SECURITYDIGESTTITLE/SECURITYDIGESTBODYpreviously passed throughnew Function()— arbitrary code execution. The renderer now uses the same sandboxed${…}interpolation engine as all other trigger templates. -
Bearer token-endpoint requests now set
maxRedirects: 0to prevent credential exfiltration if a token endpoint returns a redirect. Applied toBaseRegistryand all providers that build their own credentialed token-fetch requests. -
Container image CVE surface cleared. Bumped
node:24-alpinebase (24.14.0 → 24.16.0),cosign(2.6.3 → 3.0.6), musl, curl, and git, clearing all HIGH/CRITICAL findings in drydock-controlled packages. Residual HIGHs inside vendored Go binaries (cosign,trivy) are scoped out of the fail-on-HIGH gate via.grype.yaml. -
LokiJS query-filter values constrained to primitives.
?key[$regex]=…-style operator injection (ReDoS via native RegExp,$ne/$gtoperator injection) is neutralized at the store choke point for every caller. -
Multiple CVE patches:
undicibumped to 8.5.0 (8 advisories including CVE-2026-9675),nodemailerbumped to 9.0.1 (GHSA-p6gq-j5cr-w38f, CVSS 7.1),protobufjsto 7.6.3 (GHSA-xq3m-2v4x-88gg, critical),viteto 8.0.16,axiosto 1.16.1 (CVE-2025-62718), and additional transitive patches across all six lockfiles.
1.5.0-rc.38 — 2026-06-19
Added
- Colored startup banner. When drydock starts on an interactive terminal it now renders the whale logo as a compact truecolor half-block banner followed by a
drydock v<version> · <mode>identity line. The art is baked from the master logo (drydock.png) at build time byscripts/gen-banner.mjs, so startup decodes no image. The banner is written to stderr and suppressed automatically when stdout/stderr is not a TTY orNO_COLORis set, so logs and piped output stay clean.
Changed
- Consolidated dependency/CVE scanning on Grype; dropped Snyk. Snyk's GitHub SCM integration scans the full dependency requirement graph across every
package.json/package-lock.jsonin the repo rather than the resolved, shipped dependency set, so it over-reports advisories in transitive packages the lockfile never actually resolves to — noise on top of a redundant paid integration. Grype replaces it on both axes: it scans the built container image (the image's package catalog is the dependency set actually shipped) and the six npm lockfiles (root,app,ui,e2e,apps/demo,apps/web), matching the lockfile-resolved versions instead of the manifest graph, so it does not emit the requirement-graph false positives. The free gates already in CI cover the rest — CodeQL (SAST),dependency-review(new-dependency CVEs on PRs), OpenSSF Scorecard, and zizmor — so nothing else was needed (Trivy intentionally not added; drydock is TypeScript/Node, so the Go call-graph scanner govulncheck used on sibling repos does not apply here). The newsecurity-grype.ymlruns the dependency scan on pull requests (path-filtered to dependency/Dockerfile/workflow changes) plus a weekly cron and manual dispatch, builds and scans the container image on scheduled/manual runs, and uploads distinct-category SARIF to the GitHub Security tab. Removed the.snykpolicy file, thesecurity-snyk-weekly.ymlworkflow, thesetup-snykcomposite action, and thescripts/snyk-*gate/quota scripts. - Refreshed the drydock whale logo across the app, website, demo, and docs. A new master render replaces the brand mark everywhere — the in-app logo and favicons, the website/demo favicons, PWA icons, and OpenGraph cards, and the README/docs logos (including the dark-mode variant). All brand assets are now regenerated from a single master (
drydock.png) viascripts/regenerate-brand-assets.sh. Filenames are unchanged, so the Home Assistantentity_pictureURL contract is preserved.
Security
-
Documentation site (
apps/web) js-yaml pinned to 4.2.0 (GHSA-h67p-54hq-rp68).fumadocs-mdxpulled js-yaml 4.1.1 transitively; an override forces the patched 4.2.0. Build-time dependency of the website only — not part of the shipped drydock image. -
E2E load-test harness
@opentelemetry/corepinned to 2.8.0 (CVE-2026-54285). artillery pulled@opentelemetry/core2.7.1 transitively, vulnerable to unbounded memory allocation in W3C Baggage propagation; an override forces the patched 2.8.0. Test-only dependency — not part of the shipped drydock image. -
Patched the container image's HIGH/CRITICAL CVE surface and scoped the Grype image gate. The first
grype-imagescan onmainflagged a pre-existing CVE backlog that nothing had been scanning (Snyk Container never ran — no token was configured). Bumped thenode:24-alpinebase (node 24.14.0 → 24.16.0 clearing CVE-2026-21710, musl 1.2.5 → 1.2.6, curl 8.19.0 → 8.20.0, git 2.52.0 → 2.54.0) andcosign2.6.3 → 3.0.6, which clears every HIGH/CRITICAL in the Node runtime and Alpine OS packages. The only residual HIGH/CRITICAL findings live inside the vendored Go module graphs compiled into the bundledcosignandtrivyCLI binaries (drydock shells out to them for signature verification and container scanning) — those clear only when Alpine rebuilds the packages, so a documented.grype.yamlscopes the fail-on-HIGH image gate to the dependencies drydock controls (Node, OS packages, the app npm graph) and excludes the two tool-binary locations. cosign 3.0.6 keeps theverify --output json/--certificate-identity/--certificate-oidc-issuer/--keyflags drydock's signature path uses. -
Patched a batch of newly-disclosed
undiciCVEs across the runtime and tooling workspaces. osv-scanner flagged eight undici advisories disclosed in 2026 — CVE-2026-6733, CVE-2026-6734, CVE-2026-9675, CVE-2026-9678, CVE-2026-9679, CVE-2026-9697, CVE-2026-11525, and CVE-2026-12151. The shipped backend (app) carries undici as a direct dependency and was on8.3.0, vulnerable to all eight — bumped to8.5.0, the only release clearing the full set (CVE-2026-9675 is fixed solely in 8.5.0), and pinned inoverridesas well. The dashboard build (ui) and the e2e load-test harness pulled undici7.25.0/7.26.0transitively; anoverridesentry forces7.28.0(the patched 7.x line) in each — build- and test-only, not part of the shipped image. -
Patched
nodemailerto 9.0.1 (GHSA-p6gq-j5cr-w38f, CVSS 7.1). A message-levelrawoption bypassed nodemailer'sdisableFileAccess/disableUrlAccessguards, allowing arbitrary file read and full-response SSRF in the delivered message. drydock's SMTP trigger only callscreateTransport/sendMailwith plainfrom/to/subject/textfields and never passesraw, so the sink isn't reachable here — but the advisory affects every release through 9.0.0 with the fix landing only in 9.0.1, so the direct dependency inappis bumped from8.0.10. The 8→9 major jump doesn't touch the stablecreateTransport/sendMailcore drydock relies on.
1.5.0-rc.37 — 2026-06-15
Security
- Patched a batch of newly-disclosed transitive CVEs across every workspace. osv-scanner flagged advisories disclosed 2026-06-15 in build- and test-time dependencies:
vite(CVE-2026-53571, CVE-2026-53632),@babel/core(CVE-2026-49356),form-data(CVE-2026-12143),protobufjs(CVE-2026-54269), andws(CVE-2026-48779). Each is pinned to a fixed version via an override (or a direct bump where the dependency is direct).js-yaml@3.14.2, reachable only through artillery's test-only load-test harness, is triaged as unreachable: its sole fix removes thesafeLoad()API artillery still calls, and it parses only trusted in-repo configs.
Changed
-
Registry rate-limiter burst raised from 5 to 10 for ghcr.io and Docker Hub. The conservative burst allowance was tripping the limiter during legitimate request spikes (enumerating tags across many containers at once); the sustained rate (2 req/s) is unchanged.
-
Hardened the E2E/CI suite against transient flakes. Crash-prone real-application e2e fixtures (Home Assistant, Radarr) now run a keep-alive entrypoint so the watcher consistently discovers the full container set instead of intermittently seeing one short; the test-bootstrap readiness count is now exact and strict; and the Playwright container-detail helpers wait on real conditions rather than fixed timeouts. No shipped runtime behavior changes from this item.
1.5.0-rc.36 — 2026-06-15
Added
-
Experimental Portwing edge-agent mode — agents behind NAT or firewalls can now dial OUT to drydock over a persistent
wss://WebSocket instead of waiting for an inbound controller connection (PR #429, M5). The feature is experimental and opt-in: setDD_EXPERIMENTAL_PORTWING=trueto enable it. When disabled, the endpoint is not mounted and the feature has zero runtime footprint. Once enabled, agents connect toWS /api/v1/portwing/wsusing theportwing/1.0subprotocol. Authentication is Ed25519 public-key challenge-response with timestamp + nonce replay protection (±60 s clock-skew window, 16 MB maximum frame size). Operator key management is exposed through a REST registry at/api/v1/portwing/keys(list, register, revoke). Because the feature is experimental the protocol and API surface may change in a future release without a deprecation notice. -
Remote-agent runtime info now carries
logLevelandpollIntervalin the acknowledgement payload (PR #430, M4). Drydock threads these fields throughbuildRuntimeInfoFromAckand surfaces them in the Agents view alongside the existing runtime metadata.
Fixed
-
Containers pinned to a fully-specified semver tag (e.g.
image:v1.13.3, 3+ numeric segments) no longer climb to newer versions by default (#321). Tags classified asspecificprecision are now treated as digest-only by default —getTagCandidatesreturns an empty tag list so updates track digest changes only, not semver version bumps. Opt in to semver climbing by settingdd.tag.include(restricts climbing to matching tags) ordd.tag.family=loose(unrestricted climbing as before). Floating tags (latest,16-alpine, etc.) and 1–2-segment partial versions are unaffected. -
Maintenance window now gates when auto-updates are applied, not just when update checks run (#321). Previously
watchFromCronrespected the window, butmaybeFastResyncAfterUpdate(the post-update fast resync) calledwatchContainerunconditionally — allowing a triggered resync to detect a new image and dispatch an update outside the window. The fast resync now mirrors the same maintenance-window guard used bywatchFromCron. Additionally,computeUpdateEligibilitygains amaintenanceWindowOpencontext field: whenfalse, a softmaintenance-window-closedblocker is recorded in the eligibility result. Manual UI/API-triggered updates passundefinedand remain ungated. -
Self-update overlay no longer flickers — the UI holds the "Applying Update" screen until the swap is actually complete. Three compounding defects made the self-update experience look broken: (1) the UI's connectivity probe treated any successful
/auth/userresponse as "update finished", but the old server keeps answering during the image pull — so the overlay dismissed almost immediately, then the page died again when the old container actually stopped; (2) the in-progress self-update operation could never be recorded as completed: the finalize callback secret was regenerated per-process (the helper's POST always failed with 403 after the restart) and startup reconciliation expired the operation before the helper could finalize it; and (3) the transientdrydock-self-update-<timestamp>helper container carried no watch-exclusion label, so it flashed into the container list with watch-by-default enabled. The finalize secret is now per-operation, with its SHA-256 hash persisted on the operation row so the restarted process can validate the helper's callback; startup reconciliation grants fresh in-progress self-update operations a 10-minute grace window (with expiry as the bounded fallback if the helper dies); a new unauthenticatedGET /api/v1/self-update/{operationId}/statusendpoint reports the operation state while the session is unavailable mid-restart; the UI polls that endpoint during a self-update and only reloads once the operation reaches a terminal state (succeeded,rolled-back,failed, orexpired); and the helper container is labeleddd.watch=falseso it never appears in the watcher's container list. The UI also closes its SSE connection when self-update mode begins (after the ack is delivered) — the browser's built-in EventSource retry would otherwise reconnect to the new server, clear the overlay before the swap was committed, and leave the SPA running stale pre-update assets. Dry-run mode no longer shows the overlay at all: the UI notification is skipped and the no-op operation is marked terminal immediately instead of lingering in-progress.
1.5.0-rc.35 — 2026-06-10
Fixed
-
False "update failed" notification when concurrent update requests race (#421). When a duplicate update request was rejected with HTTP 409 ("update already in progress") while the winning update was still in flight, the controller classified the conflict as a genuine failure — firing an "update failed" notification immediately followed by "updated successfully". The duplicate classifier now recognizes three benign signals instead of one: a recently-succeeded operation (as before), a 409 response whose body explicitly carries the active-update lock message ("Container update already queued/in progress" — authoritative even before the winner's state has propagated from a remote agent over SSE), and another active (queued or in-progress) operation for the same container and agent+watcher identity. The same reclassification now also covers the Docker-native rename path in
ContainerUpdateExecutor, which previously marked the duplicate failed before the outer classifier could run. -
Update operations could hang in-progress forever when deferred rollback reconciliation failed. The deferred reconciliation callback only logged a warning on error, leaving the operation permanently active and blocking all future updates for that container until restart. The operation is now terminalized as failed (self-update and already-terminal operations excluded, as elsewhere).
-
Spurious "update available" notifications around just-updated containers (#408 hardening). Three escape hatches in the post-update suppression mechanism are closed: suppression now keys on both the container ID and the watcher-scoped name, so the recreated container's new Docker ID can no longer dodge the check; the batch retry buffer consults suppression before re-queuing; and on startup the suppression set is re-seeded from update operations that succeeded within the last hour, so a controller restart between an update and the watcher's confirming scan no longer re-fires a stale notification. Suppression entries now also expire after one hour and are cleared on trigger deregistration, so containers deleted outright (or agents that never reconnect) can no longer leak entries for the life of the process.
-
Live log viewer returned 403 behind TLS-terminating reverse proxies even with
DD_SERVER_TRUSTPROXYset. WebSocket upgrades bypass Express, so the log-stream origin check never honored trust-proxy. It now compares the browser origin againstX-Forwarded-Host/X-Forwarded-Proto(first hop) when trust proxy is enabled — and remains byte-for-byte strict when it is not. -
Containers of permanently removed agents lingered in the store forever. Startup now prunes container rows whose
agentno longer matches any registered agent component. Rows of registered-but-currently-disconnected agents are untouched. -
"Updated successfully" toast fired while the new container was still starting (#290 follow-up). The toast settled on the old container's removal event during a recreate; it now waits for the replacement container's arrival (
replacementExpectedremovals are skipped, and the new container's ID rides thedd:update-appliedpayload asnewContainerId), closing the status gap between the last update activity and the success notification. The toast dedup TTL also gained a 20% margin over the server's SSE replay buffer to prevent a boundary duplicate on reconnect. -
Watcher enrichment failures that threw non-Error values leaked malformed entries into the container snapshot. Thrown non-Error values are now wrapped, counted as enrichment errors, and excluded.
-
GCR registry reported anonymous configurations as authenticated.
Gcr.getAuthPull()now returnsundefinedwithout credentials, matching the other providers. GHCR 404 detection now checks the axios response status instead of matching error-message strings.
Security
-
Command trigger no longer inherits the full process environment. User-authored command scripts previously received every
DD_*secret (registry tokens, notification tokens, agent secrets) viaprocess.env. The child environment is now built from a fixed allowlist (PATH,HOME,SHELL,USER,LANG,LC_ALL,TZ,TMPDIR,TMP,TEMP) plus the drydock-provided container variables. Scripts that legitimately need more can name additional variables with the newDD_ACTION_COMMAND_{name}_ENVoption (comma-separated). -
Hook commands can be restricted to an allowlist of binaries (
DD_HOOKS_ALLOWED_COMMANDS). With hooks enabled,dd.hook.pre/dd.hook.postlabels could invoke any binary on the image. The new comma-separated allowlist matches the hook command's first token (basename, or exact path for entries containing/); when unset, behavior is unchanged and a one-time warning recommends configuring it. -
HTTP trigger blocks cloud metadata endpoints. Requests resolving to link-local ranges (
169.254.0.0/16including169.254.169.254,fe80::/10,fd00:ec2::254) are rejected before sending — including IPv4-mapped and IPv4-compatible IPv6 spellings of those ranges (::ffff:169.254.169.254,::ffff:a9fe:a9fe,::169.254.169.254), which would otherwise slip past the literal-IP check. Private-network and localhost targets remain fully supported — they are the normal self-hosted case. The rare legitimate link-local target can opt out viaDD_NOTIFICATION_HTTP_{name}_ALLOWMETADATA=true.
Performance
-
Tag transform patterns are no longer recompiled in the sort hot path. Compiled RE2 transform patterns are cached per formula and tag candidates are transformed once before sorting instead of twice per comparison — previously ~3,000 compilations per 300-tag container per watch cycle.
-
Container normalization no longer deep-clones the entire container per registry call. The watcher now copies only the image/registry fields it mutates instead of
structuredCloneof labels and environment for every container every cycle. -
Update-operation retention pruning uses indexed status queries instead of materializing the whole collection every 100th mutation, and the default rejected-credential pattern in
BaseRegistryis compiled once at module load. An unusedupdatedAtcollection index no longer taxes every mutation.
Changed
- Trigger providers must implement
trigger()/triggerBatch(). The base implementations now throw instead of silently doing nothing, so a provider that forgets to override fails loudly. All 22 bundled providers already comply; this only affects out-of-tree forks.
1.5.0-rc.34 — 2026-06-07
Added
-
Trigger environment variable taxonomy split —
DD_ACTION_*andDD_NOTIFICATION_*prefixes. Action triggers (Docker, Docker Compose, Command) are now configured withDD_ACTION_*anddd.action.*labels; notification/messaging triggers (Slack, SMTP, Discord, Telegram, ntfy, Pushover, and all others) are configured withDD_NOTIFICATION_*anddd.notification.*labels. All three prefix families (DD_ACTION_*,DD_NOTIFICATION_*,DD_TRIGGER_*) are interchangeable at runtime — merge priority isDD_NOTIFICATION_*>DD_ACTION_*>DD_TRIGGER_*. A migration CLI (drydock config migrate --source trigger) rewritesDD_TRIGGER_*,dd.trigger.include, anddd.trigger.excludeto action-prefixed aliases automatically; use--dry-runto preview changes before applying. -
Per-agent Home Assistant MQTT topic segmentation (
DD_NOTIFICATION_MQTT_<name>_HASS_AGENTTOPICSEGMENT, defaultfalse). When enabled, Drydock inserts anagent/<name>segment into every Home Assistant MQTT topic — per-container state topics, watcher-level count/update sensors, and watcher running-status sensors — for containers owned by a remote agent, so two agents that both use the default watcher namelocalno longer publish to (and overwrite) the same topics. Enabling it also scopes the watcher-level sensor counts and the discovery-entity cleanup per agent, fixing the Home Assistant facet of #386. Controller-local container topics are unchanged. Because it changes the Home Assistant entity IDs for agent-owned containers, it is opt-in for the v1.5.x line and targeted to become the default in v1.7.0 — see Deprecated. -
Up-to-date and pinned badges in Kind column — Containers table now shows a green check-circle badge ("Up to date") for containers at their latest version, and a green pin badge ("Pinned") for containers with skipped updates, replacing the previous dash placeholder.
-
Show/hide toggle on the login password field (commit
e086c5bc). The sign-in password input now has an eye / eye-slash button to reveal or mask what was typed, with an accessible label andtype="button"so it never submits the form. -
Real-time container log viewer — WebSocket-based live log streaming from Docker containers directly in the UI. Features ANSI color rendering, automatic JSON log detection with syntax-highlighted pretty-printing, free-text and regex search with match navigation, stdout/stderr stream filtering, log level filtering for structured logs, copy to clipboard, and gzip-compressed download. Available in both the container detail panel and a dedicated full-page view at
/containers/:id/logs. (Phase 4.2) -
Diagnostic debug dump — One-click export of redacted system state from Configuration > Diagnostics. Collects runtime metadata, component state (watchers, registries, triggers, agents), Docker API diagnostics, MQTT Home Assistant sensors, recent Docker events, store stats, and
DD_*environment variables. Sensitive values matchingpassword|token|secret|key|hashare automatically redacted. Configurable time window (1–1440 minutes). (Phase 4.14) -
Container log streaming API —
WS /api/v1/containers/:id/logs/streamendpoint with Docker binary stream demultiplexing, session-based authentication on WebSocket upgrade, and fixed-window rate limiting (1,000 connections per 15 minutes). -
Container log download API —
GET /api/v1/containers/:id/logsendpoint with gzip compression support, stdout/stderr filtering, configurable tail size, and timestamp-basedsincefiltering. -
Debug dump API —
GET /api/v1/debug/dumpendpoint with configurableminutesquery parameter for time-windowed event collection. -
Dashboard customization — Customizable grid layout with drag-to-reorder, resize, and per-widget visibility toggles using
grid-layout-plus. Edit mode via pencil icon in breadcrumb header. Customize panel with checkboxes and S/M/L size badges. All widgets progressively collapse content based on container height. -
Resource usage dashboard widget — CPU and memory usage bars with top-N resource consumers, progressive detail at different widget sizes.
-
Fleet-aggregate stats subsystem (commits
feature/v1.5-rc17). NewContainerStatsAggregatorpolls watched local and agent-owned containers once per tick (default 10 s) and computes a fleet-wideContainerStatsSummary(total CPU%, total memory, top-N rows). Two new endpoints —GET /api/v1/stats/summaryandGET /api/v1/stats/summary/stream— expose the current snapshot and a live SSE feed; the dashboard Resource Usage widget now consumes the SSE stream directly, fixing the regression (introduced in rc.13 by the?touch=falseworkaround) where the widget showed zeros because the per-container cache was never warmed. The legacyGET /api/v1/containers/statsendpoint and the client-sidesummarizeContainerResourceUsagerollup have been removed. -
Per-container update locks (commit
761fb834). New keyedLockManagerprimitive inapp/updates/lock-primitives.tsreplaces the module-levelpLimit(1)that was serialising every container update across the entire process. Lock keys are derived per container (and per compose project forDockercompose), so two unrelated containers can now pull and recreate concurrently while two services in the same compose project still serialise correctly. -
Restart recovery for queued and pulling updates (commit
00788b13). Startup reconciliation inapp/store/update-operation.tsis now selective:status=queuedoperations stay queued for the recovery dispatcher to pick up, andphase=pullingrows are reset toqueued(pull is idempotent). A newapp/updates/recovery.tsmodule runs once afterregistry.init(), re-resolves trigger and container for each queued operation, and dispatches them through the existing fire-and-forget pipeline. -
Notification outbox with retry and dead-letter queue (commits
a9561d93,7d2ef6eb,b215d295,ce26bece). NewnotificationOutboxLokiJS collection andapp/notifications/outbox-worker.tsbackground worker provide durable retry semantics for notification dispatch. On failure, the delivery intent is persisted to the outbox and the worker retries on a periodic drain with exponential backoff + jitter. After a configurable number of failed attempts (default 5) entries transition to the dead-letter queue; delivered and dead-letter entries are auto-purged past TTL (default 30 days). New/api/notifications/outboxREST surface lets operators list entries, retry from the DLQ, or discard. -
Notification outbox UI (commit
feature/v1.5-rc17). NewNotification outboxpage (route/notifications/outbox, nav under Settings) with status tabs (Dead-letter / Pending / Delivered), retry and discard actions. -
Cancel queued or in-flight updates (commits
4b79e3ac,79487115).POST /api/operations/:id/cancelnow accepts both queued and in-progress operations. Queued ops are marked failed immediately; in-progress ops are flagged via acancelRequestedfield and the lifecycle observes the flag at three safe checkpoints. -
Global concurrent-update cap (
DD_UPDATE_MAX_CONCURRENT). New counting semaphore provides a configurable global gate on how many update lifecycles run simultaneously. Default0= unlimited. Positive integerNmeans at most N updates run concurrently. Self-update operations bypass the global cap. -
Health-gate SSE heartbeat (
DD_UPDATE_HEALTH_GATE_HEARTBEAT_MS). While drydock waits for a new container to pass its health gate, a periodic heartbeat re-emitsphase: 'health-gate'at a configurable interval (default 10 s).DD_UPDATE_HEALTH_GATE_HEARTBEAT_MS=0disables heartbeats; values below 1000 ms or non-integers fail fast at startup. -
Post-start liveness grace window (
DD_UPDATE_POST_START_LIVENESS_GRACE_MS). After Dockerstart()returns, Drydock waits this many milliseconds and then re-inspects the new container. If the container has already exited, the lifecycle throws and the existing rollback machinery takes over — catching containers that exit immediately after an update (bad command, broken entrypoint, missing dependency) that would otherwise be recorded as a successful update. Default2000ms. Set to0to disable the check entirely. Values between 1 and 99 ms are rejected at startup; the minimum non-zero value is 100 ms. -
Recovery-boot concurrency cap (
DD_UPDATE_RECOVERY_BOOT_CONCURRENCY). When Drydock restarts after a crash it finds queued update operations left from the previous run and resumes them. This variable bounds how many are dispatched in parallel during that recovery sweep. Default4. Values of0are rejected at startup (minimum is 1). -
Self-update now works when Drydock reaches the Docker daemon over a TCP host, not only through a bind-mounted
/var/run/docker.sock(commitfc34ffb9).resolveHelperDockerConnectionnow inspects the watcher's Dockerode connection: a TCP host produces a TCP helper attached to Drydock's own Docker network. The bind-mounted-socket path is unchanged. -
The per-container Update button is locked with a
Self-update unavailableindicator when Drydock cannot update itself in the current deployment (commitcf777280). A new hardself-update-unavailableupdate-eligibility blocker is raised when self-update cannot run over either a bind-mounted socket or a TCP host. -
i18n coverage extended to the remaining hardcoded UI strings across 28 components (discussion #329). All 16 non-English locales now have full key parity with the English source. 17 locales ship in the picker: de, es, fr, it, nl, pl, pt-BR, tr, zh-CN, zh-TW, ar, ja, ko, ru, uk, vi, plus English.
-
DD_AGENT_ALLOW_INSECURE_SECRETescape hatch for closed-LAN deployments. rc.20 tightened the agent-secret-over-HTTP check to a hard error. rc.21 introducesDD_AGENT_ALLOW_INSECURE_SECRET=trueas an explicit controller-side opt-in for environments where the operator accepts that the agent secret travels in cleartext. Default behavior is unchanged. -
Security scan digest mode. Every scan cycle now carries a stable
cycleId(UUID v7) and emits asecurity-scan-cycle-completeevent. Triggers can configureSECURITYMODE=digest(orbatch+digest) to receive one summary per cycle. Templates are customizable viaSECURITYDIGESTTITLE/SECURITYDIGESTBODY. (#300) -
Opt-in scheduled-scan notifications — New
DD_SECURITY_SCAN_NOTIFICATIONS=trueflag enablessecurity-alertevent emission from scheduled scans. Default isfalse; on-demand scans always emit. -
Bulk security scan endpoint —
POST /api/v1/containers/scan-allscans all (or a filtered subset of) watched containers server-side, streams per-container progress over the existing scan SSE channel, and honors client-disconnect aborts. Rate-limited to 1 request / 60s per IP (authenticated-admin bypass). -
SSE Last-Event-ID replay (#289) — The server stamps every broadcast event with a monotonic
<bootId>:<counter>id and retains a 5-minute time-bounded ring buffer. Clients reconnecting with aLast-Event-IDheader receive every event they missed; if the buffer has evicted the requested id the client receives add:resync-requiredevent. -
Update-eligibility blockers on container rows — Backend surfaces 12 structured blocker reasons per container, rendered inline on the Containers list so users see why a row isn't updating without opening the detail drawer.
-
GET /update-operations/:idendpoint — Returns the current state of a specific update operation for reconciliation when the terminal SSE is missed. -
Inline update action in Security view (#299) — Image rows in the Security view now show an "Update" action button directly next to the vulnerability data when a newer image is available.
-
Watcher next-run metadata (#288) — Watcher API and Agents view now show when each watcher will next poll for updates, with an absolute-timestamp tooltip on hover.
-
Backend-driven update queue — Container updates are now queued server-side with per-trigger concurrency limits. UI shows Queued → Updating → Updated state progression with sequence labels (e.g. "Updating 1 of 3").
-
Registry 429 / 503 retry with Retry-After and per-host token bucket (commit
ffd1b57b, #342). A newwithRetryhelper wraps every registry HTTP call: on 429 or 503 it honors the upstreamRetry-Afterheader, then falls back to exponential backoff (1 s / 2 s / 4 s, capped at 60 s), up to 3 retries. A new per-host token bucket prevents the watcher from self-inflicting rate limits during a large cron cycle. -
Release notes inline popover (commit
09475fa6). The release-notes icon on container rows now opens an inline popover showing both the current and the available-version release notes side by side, with expand/collapse per panel. -
Container source project shortcut link (Discussion #295) — Containers now render a clickable "View project" link next to release notes when an
org.opencontainers.image.sourceOCI label,dd.source.repooverride label, or GHCR-derived source URL is available. -
Actionable deprecation banners (Discussion #214) — The 5 deprecation warning banners now show the concrete migration action inline and include a "View migration guide" link that deep-jumps to the relevant anchored section of the deprecations docs page.
-
Notification dropdown rework + themeable zebra stripes (Discussion #267) — Header carries the "Notifications" title plus a "Clear" text button, each row shows a per-entry dismiss affordance, and a split footer exposes "Mark all as read" + "Open audit log". Introduces
--dd-zebra-stripe, a new theme token. -
Notification history store — New LokiJS collection (
notifications_history) records a per-(trigger, container, event-kind) result hash soonce=truededup survives process restarts. -
Floating tag detection and UI indicator — New
tagPrecisionclassifier (specific|floating) detects mutable version aliases and auto-enables digest watching on non-Docker Hub registries. Container detail views show a caution badge when a floating tag is detected without digest watching enabled. (Discussion #178) -
Hide Pinned containers toggle — Checkbox in the container list filter bar hides containers pinned to specific versions. Persisted in user preferences. (Discussion #250)
-
Combined batch+digest notification mode — Triggers can now use
MODE=batch+digestto send both immediate batch emails and scheduled digest summaries. (#254) -
Multi-select event-type filter in audit log (commit
5e2d0c70, Discussion #332). The audit log's event-type filter is now a checkbox dropdown supporting any combination of event categories simultaneously. -
Bearer token auth for
/metricsendpoint — SetDD_SERVER_METRICS_TOKENto authenticate Prometheus scrapers viaAuthorization: Bearer <token>. -
Disable default local watcher — Set
DD_LOCAL_WATCHER=falseto prevent the built-in Docker watcher from starting, useful for controller-only nodes that manage remote agents exclusively. -
Multi-server notification identification (#283) — Notifications automatically include a
[server-name]prefix when agents are registered. Controller name configurable viaDD_SERVER_NAME. Custom templates can usecontainer.notificationServerNameandcontainer.notificationAgentPrefix. -
Infrastructure update mode —
dd.update.mode=infrastructurelabel for socket proxy containers enables helper-swap update path bypassing the socket proxy. -
i18n framework migration (refs #329). Bulk vue-i18n migration into per-namespace JSON catalogs under
ui/src/locales/en/(eight namespaces auto-loaded byimport.meta.globinboot/i18n.ts). Foundation for the Crowdin integration. 17 locales ship in the picker. -
Design system components — Added shared UI building blocks:
AppIconButton,AppBadge,StatusDot,DetailField, andAppTabBar. (Discussion #199) -
Podman API version negotiation — Docker watcher probes the daemon's
/versionendpoint over the Unix socket and pins Dockerode to the reported API version. PreventsEAI_AGAINcrashes caused bydocker-modem's redirect-following bug when Podman returns HTTP 301 for unversioned API paths. (#182) -
System log live streaming in UI — Added end-to-end WebSocket support for system logs (
/api/v1/log/stream) with new UI service/composable and live log view integration. -
System log viewer overhaul — Toolbar stays pinned at top, long lines wrap at viewport width, search matches component/level/channel fields, filter toggle shows only matching entries, sort toggle switches between oldest-first and newest-first. (#259, #260)
-
Rollback shortcut in container actions menu — Quick rollback option directly from the container row actions dropdown.
-
SPA + hashed-asset cache-control — Static UI assets with hashed filenames are served with immutable long-lived cache headers; the SPA
index.htmlcarries a short revalidation header.
Changed
-
Legacy aggregate container stats route now fails explicitly.
GET /api/v1/containers/statsis still removed from the public stats API, but the router now catches the legacy path before/:idand returns410 Gonewith migration targets (GET /api/v1/stats/summaryfor aggregate stats,GET /api/v1/containers/:id/statsfor per-container stats) instead of treatingstatsas a container id. -
Default watcher cron relaxed from hourly to every 6 hours (#342 follow-up).
app/watchers/providers/docker/Docker.tsnow defaultscronto0 */6 * * *(every 6 hours) instead of0 * * * *(hourly). Users who setDD_WATCHER_{name}_CRONexplicitly are unaffected. Users who want near-real-time detection can still setDD_WATCHER_{name}_CRON=0 * * * *. -
Action trigger default mode — Action triggers (
docker,dockercompose,command) now default toAUTO=onincludeinstead ofAUTO=all, requiring an explicitdd.action.includelabel before auto-updating containers. (#213) -
Self-update helper now prefers the bind-mounted Docker socket over a TCP watcher connection (commit
aa828d88). The resolution order is now inverted:findDockerSocketBindruns first, and if the target container carries a socket bind the helper uses that direct socket path regardless of the watcher's TCP configuration. TCP is the fallback for pure socket-less deployments. -
DD_SESSION_SECRETauto-generated and persisted when unset. On first boot withoutDD_SESSION_SECRETset, drydock generates 64 random bytes and writes them to asecretscollection inside/store/dd.json. Subsequent boots read the persisted value so sessions survive restarts. The env var still takes precedence when set. (rc.21 restored this after rc.20 made it a hard requirement without a migration path; existing deployments that set the variable see no change.) -
Watcher dispatch is fully fire-and-forget (commit
5cfa2286).Trigger.runUpdateAvailableSimpleTriggerandrunAcceptedUpdateBatchno longer awaitrunAcceptedContainerUpdates, so a slow update lifecycle no longer stalls the next watcher tick. -
"Update started" toasts renamed to "Update queued" (commit
79487115). Dispatch is fire-and-forget — the text now matches what actually happened. -
Shared DataTable column sizing overhaul (commit
596adcd2). All first-party table surfaces now route through the sharedDataTablecomponent with numeric sizing metadata, supporting pointer and keyboard column resizing, double-click autosize, and persistent manual/autosized widths per table. -
Crowdin export configuration aligned with app locale folders.
-
Tag-last release pipeline (fixes #306). Collapsed
release-cut.ymlandrelease-from-tag.ymlinto a single workflow where the git tag push is the last step, performed only after the Docker image has been built, pushed, signed with cosign, attested (SLSA), and the signed release tarball has been verified. Enforces the invariant: if the git tag exists, the image exists. -
Playwright E2E tests moved to a dedicated workflow file (
e2e-playwright.yml) (commitf0989301). OSSF Scorecard's CI-Tests check now scores independently from the main ci-verify suite. -
Translations refreshed from Crowdin (commit
202f3d83). Human translations synced from Crowdin for the rc.23 i18n extraction sweep, updating the 16 non-English locales across all UI namespaces. -
Security alert emit is non-blocking inside the update lifecycle (commit
6c5198dd).SecurityGate.maybeEmitHighSeverityAlertnow returns synchronously after firing the emit; the lifecycle no longer waits for sequential provider notifications. -
Expand all / Collapse all bulk toggle — Replaced the single chevron toggle in the Containers toolbar with an explicit "Expand all" / "Collapse all" button.
-
Soft eligibility blockers de-emphasized by default (Discussion #325). Soft blockers now render with neutral muted styling so hard blockers visually dominate the row, and active blockers sort hard-first.
-
Responsive dashboard layout persistence — Dashboard widget bounds and layout are now breakpoint-aware, persisting separate layouts per viewport tier.
-
Trigger digest flush DRY refactor —
flushDigestBuffer/shouldHandleDigestContainerReportare now parameterized on the event kind so update-digest and security-digest share a single implementation. -
Healthcheck execution path optimized — Default HEALTHCHECK probe replaced with a 65KB static C binary (
/bin/healthcheck). curl is retained for backward compatibility with user-defined HEALTHCHECK overrides during the deprecation window (scheduled for removal in v1.7.0, final warning release v1.6.0). -
Agent reconnect notification — New opt-in
agent-reconnectnotification rule that fires when a remote agent reconnects after losing connection. Disabled by default. -
app/updates/locks.tsrenamed toapp/updates/lock-primitives.ts(commit4c506d21). The module now contains general-purpose synchronisation primitives (Semaphore,LockManager) not tied to the updates subsystem. -
Exact-version package.json pinning — Flipped the four remaining caret specifiers to exact versions so every package.json matches the already-exact lockfile resolutions. Every dependency layer is now SHA-immutable.
-
Dashboard reconnect refresh is live-only. On
dd:sse-connectedthe dashboard now refetches only endpoints that can go stale between frames;dd:sse-resync-requiredstill forces a full fan-out.
Deprecated
-
DD_TRIGGER_*environment variable prefix anddd.trigger.*container labels (deprecated v1.5.0, removal targeted v1.7.0). UseDD_ACTION_*/dd.action.*for update-action triggers (Docker, Docker Compose, Command) andDD_NOTIFICATION_*/dd.notification.*for messaging/notification triggers (Slack, SMTP, Discord, Telegram, ntfy, Pushover, and all others). The legacy prefixes continue to work as aliases through v1.7.0. A migration CLI (drydock config migrate --source trigger) rewrites existing configs automatically. See deprecations for the full schedule. -
dd.action.include/dd.action.exclude(and legacydd.trigger.include/dd.trigger.exclude) become hard manual-update blockers in v1.7.0. v1.5.x keeps them as soft blockers — the pill reads Trigger filtered / Trigger excluded but the manual Update button stays clickable (with a warn-and-confirm). v1.7.0 will lock the button and reject the API call when the labels filter out the matching trigger. See DEPRECATIONS.md for migration guidance. -
curlhealthcheck override —curlis retained in the image for user-defined HEALTHCHECK overrides during the v1.5.x deprecation window; removal is scheduled for v1.7.0 (v1.6.0 is the final warning release). -
Agent-less Home Assistant MQTT topic layout for multi-agent deployments (deprecated v1.5.0, default flips v1.7.0). When more than one node uses the default watcher name
local, the current agent-less topic layout makes same-named containers on different agents publish to and overwrite the same MQTT topics, including watcher running-status topics. SetDD_NOTIFICATION_MQTT_<name>_HASS_AGENTTOPICSEGMENT=trueto opt into the corrected per-agent layout now; it becomes the default in v1.7.0. Single-node deployments are unaffected. Enabling it changes the Home Assistant entity IDs for agent-owned containers — see DEPRECATIONS.md for migration guidance.
Removed
-
Experimental
eligibilityPills.{showSoft,deemphasizeSoft}preferences dropped before release. The de-emphasis is now baseline behavior with no toggle. -
Experimental
containers.showAutoUpdateDiagnosticpreference +compactvariant ofUpdateEligibilityBadgesdropped before release. -
Two pre-existing unused imports flagged by biome's
noUnusedImports(updates/request-update.tsdefaultTriggerimport;Docker.containers.processing-retrieval.test.tsmockGetFullReleaseNotesForContainerimport). -
Legacy
GET /api/v1/containers/statsendpoint andsummarizeContainerResourceUsageclient-side rollup removed; superseded by the new fleet-aggregate stats subsystem (GET /api/v1/stats/summary).
Fixed
-
#418 — Operators behind a TLS-terminating reverse proxy now get an actionable startup log when manual updates fail with
403 CSRF validation failed. The rc.30 same-origin hardening madeDD_SERVER_TRUSTPROXYmandatory behind a proxy that terminates TLS (without it, drydock seesreq.protocolashttpwhile the browser sends anhttpsOrigin, so every state-changing request is rejected) — but the only symptom was an opaque 403 with no pointer to the fix. When trust proxy is disabled and a request arrives carryingX-Forwarded-Proto: https, drydock now logs a one-time warning naming the exact cause and theDD_SERVER_TRUSTPROXY=1fix (linking the reverse-proxy FAQ). The CSRF guard itself is unchanged — genuine cross-site forgery is still rejected. -
#415 — Registries that require a Bearer token exchange for tag lookups (Chainguard
cgr.dev, Codeberg/Forgejocodeberg.org, and other standard Docker v2 registries) no longer fail with401 Unauthorized.callRegistrynow implements the spec-compliant flow: on a401carrying aWWW-Authenticate: Bearer realm=…,service=…,scope=…challenge it fetches a token from the realm (anonymously for public images, or with the configured credentials) and retries the request once. The realm host is validated against the registry's own host via the existingvalidateAuthUrlHostguard, and the handler falls back to the original error if the exchange can't be completed, so it is purely additive for providers that already authenticate. Fixes tag enumeration for public Codeberg images and credentialed Chainguard images. -
#413 — Containers deployed as a Docker Swarm stack (e.g. Portainer agents via
docker stack deploy) now group in the container stack view. The grouping key derivation inapp/api/group.tsrecognizedcom.docker.compose.projectbut notcom.docker.stack.namespace, the label Swarm stacks carry instead of the Compose project label — so Swarm-deployed services fell through to the ungrouped bucket while Compose services grouped correctly.com.docker.stack.namespaceis now the lowest-priority auto-detected group key, afterdd.group>wud.group>com.docker.compose.project. -
#411 — Multi-agent deployments no longer produce a false
409 Conflictwhen identically-named containers are being updated on different agents. The duplicate-update check revived in rc.30 fell back to a global name-only lookup (getActiveOperationByContainerName) with no agent+watcher scope. Containers on different hosts are physically separate — the lookup is now scoped by{ agent, watcher }via an optionalContainerIdentityFilterparameter added to bothgetActiveOperationByContainerNameandgetInProgressOperationByContainerNameinapp/store/update-operation.ts. Legacy operation rows that predate the container-snapshot field (#385) are accepted unconditionally as a backward-compatible fallback. The same scoping is applied to theContainerUpdateExecutorreconciliation path, the list-handler eligibility context, and the SSE container-enrichment path so cross-agent contamination is closed at all five call sites. -
Latent-bug audit of the 1.5 RC bug classes — ten same-class defects fixed before 1.5.0 final. A class-wide sweep of the recurring root-cause classes behind the 1.5 RC churn (container-identity-by-name, missing agent scope, terminal-event snapshots, operationId threading, notification dedup, stale-scan epochs, SSE reconnect, image-reference normalization, health readiness) surfaced the following still-unfixed instances. Each is active by default and ships with a regression test:
- Agent-hosted update operations now carry a container snapshot and can no longer be orphaned mid-prepare.
ContainerUpdateExecutorcreated fresh operation rows without acontainer, so terminalupdate-failed/rollback events emitted with no snapshot and the notification fell back to a store lookup that misses after a recreate — silently dropping the agent-path failure notification. The post-pull tail (clone-runtime-config + rename) was also unguarded, leaving the row stuckin-progressfor the full TTL on error. The snapshot is now persisted at enqueue and the tail marks the operation terminal on failure. Latent remnant of the #385/#386 terminal-lifecycle class. - The direct remote-trigger route (
POST /:type/:name/:agent) now threads the controller-minted operation id (#289 remnant). For update triggers it creates the controller-side queued row, honors a caller-suppliedoperationId(previously validated then silently discarded), forwards it to the agent as runtime context, and returns202 { operationId }instead of200 {};UpdateRequestErroris surfaced with its status. The agent no longer mints a divergent id the controller never tracks. - Digest- and batch-mode notifications no longer re-fire a spurious "update available" after a successful update (#408 digest/batch remnant). The original #408 guard (
recentlyAppliedContainerKeys) was applied only to the simple report path, so a staleupdateAvailable=truereport arriving before the watcher caught up could still re-buffer a just-updated container on the digest and batch paths. The digest path (shouldHandleDigestContainerReportplus thehandleContainerReportDigestlift) and the batch path (shouldHandleBatchContainerReportplus a newhandleContainerReportslift) now share the same suppress-then-lift lifecycle. The batch-path lift is mandatory rather than belt-and-braces: a puremode=batchtrigger registers neither the simple nor the digest handler, so without it the suppression key would never clear and that container's update-available notifications would be muted permanently after the first successful update. - Re-armed the post-update stale-scan guard (#265 regression). The rc.17 operation-store refactor removed the only call that stamped the manual-update epoch, leaving
preserveClearedUpdateStateas dead code; an in-flight cron scan could re-raise the cleared update badge.maybeFastResyncAfterUpdatenow stamps the epoch before the resync scan, suppressing earlier in-flight scans. - The Security view now refreshes its container list on SSE reconnect/resync. It previously refetched only vulnerabilities, leaving update-eligibility, blocker tooltips, and the inline Update action stale after a dropped connection (it could offer Update on a container that no longer exists).
- The self-update helper image reference is now built with
buildImageReference.resolveHelperImagehand-rolled the registry-URL normalization; whenregistry.urlended with a trailing slash (…/v2/) the concatenation produced a double-slash reference (ghcr.io//org/image:tag) that Docker rejects with HTTP 400. It now delegates to the canonical helper that strips the scheme and trailing/v2[/]before concatenation. - Scheduled security scans write their result onto the current container record, not a stale snapshot. The async Trivy scan back-wrote by spreading the container snapshot captured at batch-prep time, carrying stale update-state and — on the recreate path, where the container has a new id — creating a zombie store record for the old id. The write-back now re-reads the live record by id, skips if the container is gone, and merges only the
securityfield. - The agent
/healthendpoint now reflects watcher-registration failure. It was a hardcoded200, so an agent whose watchers all failed to register (unreachable Docker socket, bad TLS — swallowed withlog.warn) reported healthy forever. It now returns503when zero watchers are registered, mirroring the main API's readiness gate; the normal (≥1 watcher) path is unchanged. - The agent batch-completed event parser resolves controller-issued operation ids.
parseBatchUpdateCompletedPayloadwas the only event parser scoping itsoperationIdwithtoAgentScopedIdinstead ofresolveAgentOperationId; a controller-owned id reaching it would be double-scoped and stop matching the controller's row. It now resolves consistently with the other parsers (#289 class). - Container-recreate no longer fails with HTTP 403 on deployments using a hardened socket proxy that enforces a runtime allowlist (commit
af79e612). Drydock copied the daemon-reportedHostConfig.Runtime("runc") verbatim into thePOST /containers/createbody, and a socket proxy with an explicit runtime allowlist rejected it as not-allowlisted — even though the container was only ever using the daemon default. The fix fetches the daemon'sDefaultRuntimeviaGET /infoduring the clone-prepare step and omitsHostConfig.Runtimefrom the create body when it merely restates that default. Explicitly-selected non-default runtimes (nvidia,kata,sysbox-runc) are preserved, and when/infois unavailable the runtime is left untouched (prior behaviour). TheHostConfigis shallow-cloned before editing so the inspect spec kept for rollback is not mutated. - Stuck update operations now terminalise to a non-notifying
expiredstatus instead offailed(#410). The active-TTL sweep and the startup-orphan reconciliation marked timed-outqueued/in-progressrows asfailed, which firedmarkOperationTerminal'supdate-failedlifecycle event — a false "update failed" notification that hit two distinct cases: a genuinely orphaned operation (an agent-scoped controller trigger that errored after the queued row was created but before the agent ran it) and a slow-but-successful update whose agent confirmation simply outran the 30-minute TTL. Both now resolve to a new terminal status,expired, for whichemitTerminalLifecycleEventemits nothing at all, so neither case can cry wolf. The UI treatsexpiredas terminal — the "Updating" badge clears with no failure styling and no toast. A late agent report arriving after expiry is ignored (the row is already terminal), so such an operation reads asexpiredrather than flipping to a delayed success/failure. A second, deeper instance of the same false alarm is now closed: when a duplicate update operation executes against a container that an earlier pass — or Docker Compose, or an agent — already recreated, the rename/refresh hits a genuine Docker 404 (no such container/no longer exists) or a 409 conflict (a real error, not a TTL timeout), which previously terminalised asfailedand fired the same ghost "update failed" notification. The duplicate-op path now defuses these benign post-success errors at three choke points: the dead name-based dedup fallback ingetActiveUpdateOperationForContaineris repaired so a stale-container-id duplicate is blocked up front (409 instead of silently slipping through); and bothContainerUpdateExecutor'srename()catch and theDockerlifecycle outer catch consult a sharedduplicate-op-classificationhelper that downgradesfailed→expiredonly when asucceededterminal row already exists for the same container name inside a 15-minute window. A real failure with no recent success still terminalises asfailedand notifies. Terminal-lifecycle/orphan class, sibling to #385/#386. - The fleet-stats collector now backs off on stream reconnect instead of hot-looping. When a container's Docker stats stream closed immediately on open (an exited container, a daemon error, or a malformed stream),
restartCollectionre-opened it synchronously — a tight reconnect loop bounded only by a single microtask tick that spins CPU and floods logs, once per such container. Reconnects now use capped exponential backoff (1 s doubling to a 60 s ceiling), reset to the base delay once the stream delivers data; the scheduler refuses to stack timers, skips reconnecting once a container is unwatched, and clears the pending timer on the last release. Latent defect in the new-in-1.5 fleet-stats subsystem.
- Agent-hosted update operations now carry a container snapshot and can no longer be orphaned mid-prepare.
-
#386 — Agents permanently showing 0 running containers in the controller UI (the recurrence that survived the rc.20/25/26/28 fixes). Root cause was two compounding bugs outside the handshake/snapshot machinery the earlier fixes targeted. (1) The controller's own local Docker watcher was pruning every agent's containers. Its store query
getContainers({ watcher: this.name })was scoped by watcher name only, and remote agent containers are stored under the same default watcher name (local), so each controller watch cycle treated the agents' rows as stale (their IDs are not present on the local Docker), failed toinspect()them, and deleted them — roughly every 6h until the agent was restarted. This is also why the rc.28 handshake-0 guard appeared not to work: the rows were wiped seconds before the handshake ran, so there was no "last-known state" left to preserve.Docker.getContainersnow scopescontainersFromTheStorebynormalizeAgentValue(this.agent), so a watcher only ever prunes its own agent's containers. (2) A lostdd:watcher-snapshotwas never recovered until the next 6h cron. The agent emits its authoritative snapshot only at the end of each cron; if the controller's SSE stream was silently half-open or mid-reconnect at that instant the snapshot was written to a dead socket and lost, leaving the controller empty. The agent now caches the latest snapshot per watcher and replays it to each new SSE client immediately afterdd:ack, so the controller converges on the agent's true container set on any (re)connect. This also explains why agents with many containers were immune (constant per-container SSE traffic kept the connection warm) while low-traffic agents went idle and dropped. -
#386 follow-up — six more cross-agent contamination bugs of the same class, found by a class-wide audit. The prune/snapshot fix above closed the headline symptom; an audit of every store read/key/match that scoped by watcher name or container name without also scoping by agent (the controller's own
localcontainers and a remote agent'slocalcontainers share the default watcher name) found and fixed the following further sites. These are active by default:- Post-update fast-resync (
Docker.maybeFastResyncAfterUpdate) matched the resync target bywatcher_nameand could pick a different agent's same-named container, leaving a stale "update available" badge until the next cron — the candidate set is now agent-scoped. - Deferred update reconciliation (
scheduleDeferredReconciliation) resolved the container by name only and could no-op against another agent's same-named row, leaving the local operation stuck inrollback-deferreduntil restart — it now resolves by operation/container id with an agent-scoped name fallback. - Watcher-list stats (
GET /api/watchers,GET /api/watchers/:id) keyed the per-watcher stat buckets by bare watcher name, collapsing everylocalwatcher into a single inflated bucket — they are now keyed by the unique registry id (docker.localvsml.docker.local), so each watcher row reports its own counts. - Dashboard stats aggregator (
stats/aggregator.ts) mapped agent containers to the controller's local watcher, silently dropping them from the fleet summary (or misattributing CPU/memory on a name collision) — it now resolves the agent-aware watcher id, so agent containers are no longer queried against the wrong Docker socket. - Security-scan result cache (
store/container.insertContainer) keyed cached Trivy results bywatcher_nameonly, so a remote agent's same-named container could be stamped with the controller's scan verdict (false-clean / false-alarm) — the cache (only ever written for controller-local containers) is now skipped for agent containers. - Name-based webhook routing (
POST /watch/:name,POST /update/:name) resolved the container by scanning the entire store by name and fanned the action out to all watchers; it now dispatches only to the resolved owning watcher and returns409 Conflictwhen a name is ambiguous across agents/watchers (disambiguate with?agent=/?watcher=). Single-match (single-host) behavior is unchanged.
The Home Assistant facet of this class (watcher-level sensor counts summing across agents, and discovery-entity cleanup leaking ghost entities) is corrected by the new opt-in
DD_NOTIFICATION_MQTT_<name>_HASS_AGENTTOPICSEGMENTflag, because closing it requires a change to the MQTT topic structure — see Added and Deprecated. - Post-update fast-resync (
-
The Docker image builds again after Alpine
edge/testingdropped the pinnedtrivy=0.70.0-r1package.apk addfailed withbreaks: world[trivy=0.70.0-r1], which broke the multi-arch image build (and the Cucumber E2E job that builds it). trivy is now pinned to the package name only, matching the existingcurlexception in the same layer:edge/testingrotates and drops-rNbuilds quickly and per-arch mirrors desync during the window, so an exact-rNpin is inherently fragile for multi-arch builds. -
#408 — A successful update no longer fires a spurious "new version available" notification.
handleContainerUpdateAppliedEventclears theoncenotification history after an update is applied, which re-opens theoncegate before the watcher's next scan setsupdateAvailable=false. A lingering container report still carryingupdateAvailable=true(common during "Update All") then fired a duplicate "update available" message right after "updated successfully". A per-container suppression set now withholdsupdate-availablenotifications for a just-applied container until a watcher report confirmsupdateAvailable=false, after which the key is lifted so future real updates still notify. Additionally,update-appliedandupdate-failedlifecycle notifications now bypass the semver threshold filter, so a post-updateupdateKind.kindofunknown(e.g. digest-only updates) under a non-allthreshold no longer silently drops the success/failure notice. -
#391 — A failed Docker Compose update no longer destroys the running container.
refreshComposeServiceWithDockerApipreviously removed the old container before recreating it, so any failure of the create step (e.g. the newly-pulled image is not available for the host platform) left the service with zero containers and no recovery. The update path now (1) performs a pre-flight check that the pulled image is architecture-compatible with the host and aborts before removing the old container if it is not, and (2) wraps the stop/remove → recreate sequence so that, if recreate still fails, the original container is restored from its captured spec and the original error is re-thrown. The running container is never lost on a failed update. -
#402 —
/healthnow returns503until passport authentication strategies are fully registered, closing a startup race where the port accepted connections before auth was ready (intermittent 401s on startup).registerRoutes()in the auth subsystem now callssetAuthReadyFnbeforeauth.init(), wiring the readiness gate before strategy registration begins. Deployments that hit intermittent 401 errors during the first seconds after container start should see the issue resolved. -
#386 — A fresh-restart agent whose in-memory store has not yet been re-populated no longer wipes the controller's last-known container state on handshake.
AgentClient._doHandshakenow skipspruneOldContainerswhenevercontainers.length === 0(andhasConnectedOnceis true) and emits a warning. Pruning is deferred to the next authoritativedd:watcher-snapshot, which is already gated on!containerEnumerationFailed && enrichmentErrors === 0and is therefore unambiguous. -
#386 — Agents intermittently showing 0 running containers in the controller UI (multiple recurrences across rc.20–rc.26). The complete fix spans three layers: (1) rc.20 introduced
containerEnumerationFailedinDocker.watch()to suppress snapshots whengetContainers()throws; (2) rc.25 extended suppression to per-container enrichment errors via adiagnostics.enrichmentErrorsout-parameter; (3) rc.26 added a dedicatedAgentStatsChangedevent so the controller UI also refreshes on Docker-event-driven container changes (starts/stops between cron cycles), not only completed cron cycles. -
#289 — Agent-hosted container updates no longer leave an orphaned queued operation row on the controller that the 30-minute TTL sweep force-fails into a misleading "update failed" notification. The fix threads the controller's
operationIdend-to-end:AgentTrigger.trigger/triggerBatchnow accept and forwardruntimeContext;AgentClient.runRemoteTriggerextracts per-container operationIds and includes them in the agent payload; the agent-side controllerrunTriggeraccepts and threads theoperationIdintorequestContainerUpdate; a newAgentClient.resolveAgentOperationIdhelper reuses the controller-side row when found. The controller-side queued row therefore transitions directly toin-progressandsucceeded/failedfrom the agent's lifecycle events, eliminating the spurious "update failed" notification. -
#289 — Update-applied and update-failed notification triggers and UI success toasts no longer silently drop for containers running on a connected agent. The agent's container snapshot is now threaded through
buildAgentOperationBase,ensureAgentOperationForTerminal,markAgentOperationTerminal, and related helpers, stampingagent: this.name. The store's terminal-lifecycle emit therefore naturally carries the agent's container intoemitContainerUpdateApplied/emitContainerUpdateFailed, and both the notification trigger and the SSE toast fire end-to-end on the controller for agent-originated updates. -
#289 — Container rows no longer drop sort position during an in-flight update, and every terminal outcome (succeeded / failed / rolled-back) now fires a toast. The operation display hold captures a sort-field snapshot at hold start so the row stays pinned through the docker recreate window.
-
#290 — "Updated Successfully" toast no longer drops intermittently after a container update. A new
useGlobalUpdateToastcomposable mounted once atApp.vueis the single source of truth: listens fordd:sse-update-applied/dd:sse-update-failed/dd:sse-batch-update-completed, survives route navigation, dedupes byoperationIdover a 5-minute window, and waits for the matchingdd:sse-container-added/updated/removedevent before firing. -
#291 — Dashboard update flow now fires the same toast sequence as the Containers view and shares the same
useOperationDisplayHoldcomposable, fixing the last reporter symptoms (updating row no longer drops to the bottom mid-update; dropped terminal SSE no longer leaves the dashboard silent). -
#342 — A container is no longer shown as "update available" with a blank target version after a transient registry error.
hasRawUpdateinapp/model/container.tsnow performs the tag comparison only when bothimage.tag.valueandresult.tagare defined, matching the existing guard ingetRawTagUpdate. -
#342 — GitHub release-notes lookups now survive GitHub's secondary rate limit instead of giving up on the first burst.
GithubProviderclassifies a403as a secondary rate limit only when it carries aretry-afterheader orx-ratelimit-remaining: 0, retries those, and arms a short module-level cooldown driven by GitHub's own retry hint. ThewithRetryhelper gains optionalretryPredicateandretryDelayMshooks. -
#342 — Registry routing always uses the credentialed instance when one is configured (commit
069274fe). The router now gives explicit priority to credentialed instances. The silent anonymous fallback on 401/403 is also removed; auth rejection now throws an actionable error that surfaces as the "Check failed" badge in the UI. -
#342 — The registry-error tooltip on the Containers view now names the registry that failed.
registryErrorTooltipnow derives the registry hostname from the container'sregistryUrland renders it through a new i18n string, e.g.ghcr.io — Request failed with status code 429. -
#342 — Hybrid
image:tag@sha256:digestrefs no longer trigger a spurious "Cannot get a reliable tag" warning when Docker'sRepoTagsis empty.Docker.resolveImageNamenow detects hybrid refs and parses them directly viaparse-docker-image-name; only true digest-only refs fall through to the existingresolveDigestOnlyImagepath. -
#342 — Containers list "Version" column shows the correct data for all container types. Digest-pinned containers continue to show the
sha256:abc… → sha256:def…pair; floating-tag + digest-watch containers render the human-readable tag with the digest delta in the tooltip; hybrid digest containers show the tag with the digest pair inline. -
#355 —
update-failednotifications no longer drop silently when the controller's container store races against post-failure prune.UpdateLifecycleExecutornow carries the failing container on theupdate-failedpayload, andTrigger.handleContainerUpdateFailedEventacceptspayload.containeras the primary source with the store lookup as fallback. -
#357 — Transient Trivy failures no longer wipe previously-stored scan history. The scheduler now keeps the existing record when the new result is an error and the existing record is less than 7 days old. Error results are also no longer indefinitely re-spawning fresh Trivy invocations —
scanImageWithDedupuses a 15-minute error retry floor. -
#357 / #355 — Trivy scan and SBOM no longer require
/var/run/docker.sockinside the drydock container. The forced--image-src dockerflag is removed; Trivy now uses its default source order and falls back to a registry pull when the local daemon isn't reachable. SetDD_SECURITY_TRIVY_IMAGE_SRC=remoteto skip the local-daemon probe. -
#370 — Containers list "Version" column again shows the human-readable image tag for floating-tag + digest-watch containers (rc.20 inadvertently reverted the #356 fix). The
updateKind === 'digest' && !isDigestPinnedbranch has been restored to the correct behavior:CopyableTagwith the full digest delta in the cell tooltip. -
#371 — Containers "Group By Stack" view no longer dissolves a multi-container stack into "Ungrouped" while its last container is mid-update. A new
groupAssignedSizeMapref records each group's API-assigned member count; the flatten condition now requires bothbuckets[key].length === 1andgroupAssignedSizeMap.value[key] === 1. -
#374 — Security scans no longer hand Trivy a raw registry v2 API URL. The
resolveContainerImageFullNamefallback now strips the URL scheme and the/v2path segment, matchingRegistry.getImageFullName, yielding a plainregistry-1.docker.io/image:tagreference. -
#385 — Telegram, Pushover, and other notification triggers no longer silently swallow
update-appliedandupdate-failedevents after a compose recreate or on multi-agent deployments. The fix persists a snapshot of theContaineron the operation entry at enqueue time (createAcceptedContainerUpdateRequest) andbuildTerminalLifecycleEventBasenow forwards that snapshot on the terminal-lifecycle payload. The agent SSE wire was also extended to forward the container snapshot end-to-end. -
#328 — Triggering a security scan emitted a "container update" notification instead of the security digest.
renderBatchTitle/renderBatchBody/composeBatchMessagenow honourruntimeContext.title/.bodyverbatim when set, so the security-digest path produces the configuredsecuritydigesttitle/securitydigestbodyoutput instead of update-available output. -
#317 — A notification trigger configured with
auto: falseno longer also silently loses lifecycle notifications (update-applied,update-failed,security-alert,agent-connected,agent-disconnected). Auto-fire-on-detection handlers stay gated byauto; lifecycle handlers register unconditionally. -
#317 — Update button bypassed eligibility blockers, queuing requests the API would only reject one-by-one. The API now rejects manual updates on any hard blocker with the blocker's user-friendly message; the UI locks the Update button on hard blockers and shows a confirm-modal warning on soft blockers.
-
#315 — Self-update now works against private registries whose
registry.urlis stored as the v2 API base (e.g.https://ghcr.io/v2).resolveHelperImagenow normalizes the reference to matchRegistry.getImageFullName. -
#308 — Per-row scanning chip is now correctly anchored to the container being scanned and no longer floats in viewport-fixed gutter space. The scan lifecycle uses a
useScanLifecyclecomposable maintaining ascansInFlightset keyed by container id (with a 120s safety timeout). A siblingdd-row-scanningclass provides the containing block without dimming the row. -
#305 — Hide Pinned now hides every pinned container again, matching rc.8 behavior. (#293 had carved out an exception for pinned rows with a pending update.)
-
#301 —
GET /api/containerspreloads all active update operations in a single indexed scan, replacing the rc.8 per-container 3-scan fan-out. Synthetic per-attach time drops from ~5–25 ms to ~0.02–0.04 ms on large inventories. -
#296 — Controller identity detection now runs for host-based watchers (TCP to a local socket-proxy, the common Synology / Docker Compose pattern). Set
DD_SERVER_NAMEto override. -
#293 — Hide Pinned filter no longer hides pinned containers that have a pending update. Filter decluttering is preserved for static pinned containers; rows with a pending update (
newTagtruthy) surface through the filter. -
#282 —
batch+digestmode now sends both the immediate batch email and the scheduled morning digest for each detected update, matching the documented semantics. The fix splits the digest channel off as its ownNotificationEventKind('update-available-digest') so batch-channel and digest-channel dedup are independent. -
#270 — Hide-pinned filter now uses computed
tagPinnedproperty instead of stale stored field. Unconditional startup repair ensures tagPrecision data is always correct. -
#256 — Update operation state disambiguated by container ID instead of name, preventing cross-host bleed between same-name containers on different hosts.
-
#253 — Shorthand trigger references resolved in notification rule matching; notification buffering keys stabilized; debug logging added to every silent filter path.
-
#248 — API guard against duplicate container updates (409 conflict).
-
#245 — Container update fails with 500 error when no healthcheck — Health gate now skipped when the container has no healthcheck. (This entry originally said the gate was skipped when
dd.rollback.autois not set. That condition was removed before 1.5.0 shipped: the gate applies to any running container with a healthcheck, anddd.rollback.autoonly controls monitoring after the update completes.) Pre-healthy timeout usesmax(120s, dd.rollback.window)instead of a fixed value. -
#238 — Container inspect Config.Image fallback — When Docker summary only exposes a
sha256:…image ID (no RepoTags), container discovery falls back to container inspectConfig.Imageto recover the original tagged reference. -
#229 / #228 — Spurious SMTP emails after update —
clearDetectedUpdateState()now clears rawresult/updateKinddata instead of the derivedupdateAvailableboolean. -
#223 — Dashboard layout customizations lost on page reload — Added
gridLayouttoPreferencesSchema; reorder now usesloadPersistedLayout. -
#222 — Dashboard customize panel responsive on mobile — Panel is opt-in on mobile (sliders icon to open), full-screen overlay with backdrop dismiss.
-
#217 — Dashboard Resource Usage widget minimum height raised from 3 grid units to 7 grid units so per-container CPU and Memory lists stay visible.
-
#213 — Dashboard Host Status widget no longer auto-scrolls to the last host when the host list changes. The scroll-snap classes, dynamic tail-spacer element, and measurement machinery have all been removed.
-
#208 — Dashboard updates widget 6-item cap removed — Removed hard-coded
RECENT_UPDATES_LIMITthat silently dropped entries beyond 6 in the Updates Available widget. -
#202 — CalVer zero-padded month in strict family filter — Tags like
2026.02.0were rejected when the current tag was2025.11.1because zero-padded single digits were treated as a family mismatch. -
#200 — Dashboard widget mobile scroll — Added
overscroll-containto all scrollable dashboard widgets. -
#192 — Digest-only image visibility — Watchers no longer silently drop containers with digest-only image references.
-
#186 — Registry failures in Updates Available widget — Containers with "check failed" status no longer appear in the dashboard "Updates Available" section.
-
#183 — Cascading -old container updates — "Update All" batch no longer triggers updates on containers renamed with
-old-{timestamp}suffix during a prior update. -
#182 — Podman pod infra containers skipped — Watchers now skip Podman pod infrastructure containers that have an empty
Imagefield. -
#180 — Duplicate containers after recreate — Three-layer deduplication filtering prevents alias containers from entering the store during Docker recreate cycles.
-
#156 — Container alias name canonicalization —
getContainerName()now strips Docker recreate alias prefixes (e.g.8bf70beac570_termix→termix) before the name enters the store, so all triggers receive canonical names. MQTT Home Assistant sensor preserved during recreate (replacementExpectedflag prevents premature empty retained discovery payload). -
Discussion #295 — Release-notes icon in the container table now always opens the same popover, even when only an external release URL is available. The popover now renders uniformly for both cases.
-
discussion #295 —
DD_SESSION_SECRETno longer crashes startup when unset — The fallback is now a persisted secret: on first boot withoutDD_SESSION_SECRETset, drydock generates 64 random bytes and writes them to the store. Subsequent boots read the persisted value. (See also theChangedentry above.) -
#362 — SSE reconnect exponential backoff no longer collapses to a flat 1 s loop when the agent is struggling. The backoff now only resets after the stream has stayed open for
SSE_STABLE_CONNECTION_MS(30 s). -
AgentClienttimers are now cleared when an agent is removed, preventing orphaned timeouts (commit03bf7211). A new idempotentstop()method cancels bothstableConnectionTimerandreconnectTimer. -
#368 — OIDC custom-dispatcher paths (cafile /
DD_AUTH_OIDC_*_INSECURE=true) no longer fail with an opaqueTypeError: fetch failedon Node 24. The fix importsfetchfromundiciand uses it whenever a custom dispatcher is required so both halves share the same dispatcher version. -
OIDC warn logs now surface the full
error.causechain, making TLS and DNS failures actionable (commit720d99a3). -
OIDC error logs now redact RFC-1918 IP addresses and absolute filesystem paths (commit
9b79de77). -
OIDC SSO broken after upgrade/restart — OIDC discovery made lazy so startup failure no longer drops the provider. Redirect, callback, and token paths retry on first use. (#246)
-
Image reference construction — unanchored
/v2strip could silently corrupt references when the image name contained a/v2path segment. The fix extracts a shared pure helperbuildImageReference(app/registries/image-reference.ts) that cleans the registry URL before concatenation using anchored regexes. -
ECR stale auth token cache write avoided on concurrent key change. The cache write is now keyed on the credentials snapshot captured at request start.
-
Icon proxy serves fallback image on upstream CDN timeout or 5xx. Non-existence failures now route through the existing fallback path and serve the placeholder image.
-
SBOM endpoint returns
503instead of500when the security scanner is disabled. -
Malformed
dd.tag.transformregex patterns — The regex-transform label validator now throws at config time for malformed or oversized patterns. -
dd.registry.lookup.imagelabel no longer corrupts deploy identity (commit594a07e8, fixes #336).normalizeContainerno longer overwritesimage.name/image.registry.url; a newgetImageForRegistryQueryhelper applies the substitution only at each query boundary. -
Password-manager autofill restored on login form (commit
3abe2fa6, fixes #335). Username and password inputs now carrynameandidattributes. -
security-scan-skippedaudit row now fires when the gate is disabled globally (commitae24e0a9). -
Command action security warning updated to canonical
DD_ACTION_COMMAND_*prefix (commitaa5fc98d). -
Docker event history pruning amortized to reduce per-event splice cost (commit
d6690cc8). The threshold is now2×maxEntries, so splices are amortized across many appends. -
Agent container list no longer shares mutable LokiJS references (commit
1f7d8034). The handler now clones each container viacloneContainerbefore stripping metadata. -
Docker multi-arch build no longer fails when Alpine repos drift between archs. The curl entry is now unpinned so apk installs whatever's current per-arch.
-
Telegram MarkdownV2 escaping in all trigger paths — Body text in
trigger()andtriggerBatch()was not escaped for MarkdownV2 reserved characters, causing Telegram API 400 errors and silent notification failures. (Discussion #211) -
#310 — Restored the
[server]/[agent]prefix on default notification body templates that rc.10 had stripped. -
#309 — Status column in the Containers list now shows its label alongside the icon at typical widths.
-
#296 — Notification server-name prefix no longer renders the container ID on Docker Compose setups.
-
#283 — Duplicate server name in notification prefix and suffix suppressed.
-
#271 — Log sort order persists across navigation.
-
#265 — Stale update detection suppressed from pre-clear watcher scans.
-
#323 — Popovers on the Containers list rendered off-screen for the last row + drifted on scroll — Added a
buildPopoverStylehelper that measures available space and flips the popover upward when needed; added a global scroll listener that closes both popovers. -
Watcher "Last Run" display — Watchers page now shows relative timestamps for last run. (#189)
-
#187 — Agent column picker positioning fixed.
-
#184 — Dashboard confirm dialog —
ConfirmDialogmoved to global app shell so update prompts from the dashboard appear immediately. -
Stack/group view no longer collapses to ungrouped mid-update when containers are recreated.
loadGroups()now indexes the map under id, name, AND displayName. -
#340 — Self-update no longer preserves stale Drydock version metadata. The self-update clone path now drops image-inherited environment variables and labels from the old image when the target image changed them.
-
#345 — Host names with numeric suffixes no longer lose the differentiating character in the Containers table.
-
Truncated release notes body now marked with trailing ellipsis (commit
3a9bd098). -
Accepted update dispatch failures now logged (commit
674a0ed8). -
Row update overlays anchored to first data cell width (commit
4bdb8d65). -
Update state lost on navigation/refetch — Backend list endpoint now enriches containers with in-progress update operation state.
-
Digest-only image visibility — Watchers no longer silently drop containers with digest-only image references. Digest watch now stays enabled when Docker summary exposes a
sha256:…image but container inspect recovers a tagged reference. -
Same-name container update holds isolated to the correct instance (commit
02433a02). Added anidentityKeydiscriminator so the hold follows the container's stable identity through id changes. -
Security view container chooser traps keyboard focus (commit
e98603c1). Added standard focus-trap: focus is moved to the first focusable element on open, Tab/Shift+Tab cycle within the popover, Escape returns focus to the previously-focused element. -
Legacy
xlink:hrefSVG attributes stripped by icon sanitizer (commit0309bacb).hrefis now in the allowlist; the deprecatedxlink:hrefform remains blocked. -
Floating semver aliases excluded from greater-than check (commit
0b9eaaf3).isGreaterCandidateTagnow requires strictly greater semver in one direction and not-greater in the reverse, so floating aliases like3.3and3.3.0drop out of the candidate set entirely.
Security
- Mau registry auth scope is now percent-encoded.
Mau.authenticateinterpolatedimage.nameinto the JWT authscopequery parameter without encoding, diverging from the parent Gitlab provider which encodes it; a URL-significant character in an image name could corrupt the query or inject extra parameters. The scope is now encoded identically to Gitlab.
The following entries are hardening from a 2026-06-01 multi-agent security review (no critical/high findings).
-
OIDC UserInfo response is bound to the id_token subject on login (commit
661c21b9). The authorization-code login path calledfetchUserInfowithskipSubjectCheck, omitting the OIDC Core 5.3.2 check that the UserInfosubmatches the tokensub. The validated id_token subject is now enforced; the bearer-token path (no id_token, no reliable expected subject) is unchanged. -
CSRF same-origin check no longer trusts forged
X-Forwarded-Host(commita132318e).getExpectedOriginreadX-Forwarded-Host/-Protounconditionally, so a client could forge them to satisfy the mutation same-origin check even withtrust proxydisabled. The forwarded host is now honored only when Expresstrust proxyis enabled, the protocol derives from the already-gatedreq.protocol, and theHostport is preserved for non-standard-port deployments. Upgrade note: this aligns enforcement with the long-documented requirement to setDD_SERVER_TRUSTPROXYbehind a TLS-terminating reverse proxy (reverse-proxy setup). A deployment that terminated TLS at a proxy but never setDD_SERVER_TRUSTPROXYpreviously worked only because the unconditional header trust masked the misconfiguration; it must now setDD_SERVER_TRUSTPROXY(hop count, e.g.1) or state-changing requests will return403. -
GitHub release-notes token withheld for untrusted source repos (commit
7186195c). Add.source.repocontainer label (settable by anyone who controls the container) could redirect the operator's release-notes token — including the GHCR PAT fallback — to an arbitrary GitHub repo. Source-repo resolution now carries provenance, and no token is attached when the repo originates from that per-deployment label or from a persistedcontainer.sourceRepofallback without provenance; image-label, OCI-label, GHCR-path, and live Docker Hub metadata lookups remain trusted. A follow-up (commit0a014304) also segregates the release-notes cache by trust (#auth/#anon), so an untrusted not-found result can no longer suppress a later trusted, token-bearing fetch for the same repo. -
Playwright E2E image pinned by digest (commit
8234ef33).mcr.microsoft.com/playwright:v1.60.0-nobleis now pinned by itssha256digest so a mutated/republished tag cannot silently change the image the E2E job pulls and runs. -
Container-query filter values constrained to primitives (commit
a5ae7a89). Express'sqsparser turns?key[$regex]=…into a nested object, and the store's query-sanitizer only guarded prototype-pollution keys — so an authenticated container-list request could inject a LokiJS$regex(native RegExp → ReDoS, bypassing the project's re2js immunity) or$ne/$gtoperators. Exact-match filter values are now restricted to primitives at the store choke point, neutralizing operator injection for every caller. -
Security digest templates no longer evaluated as JavaScript (commit
e74fc56d).renderSecurityDigestTemplaterendered the operator-suppliedSECURITYDIGESTTITLE/SECURITYDIGESTBODYvianew Function('scan', …), executing them as a template literal — arbitrary code execution in the digest path. The renderer now routes through the same sandboxed${…}interpolation engine as every other trigger template; the per-severity lists and plural noun the default template relied on are pre-computed in code and exposed asscan.criticalList/scan.highList/scan.containerNoun, so default output is unchanged. The digest feature shipped only in the v1.5.0 RC train, so this is a within-cycle hardening; any RC user who set a custom digest template using JS expressions (arrow functions, array methods, arithmetic) should switch to the documented${scan.*}variables, as unsupported expressions now resolve to an empty string instead of running. -
Template method-call arguments split on top-level commas only (commit
c485f412).safeInterpolatesplit method arguments on every comma, so a comma inside a quoted string literal or nested parentheses (e.g.${x.replace(',', ';')}) was mis-parsed; argument splitting now respects quotes and parentheses, hardening the template parser the digest renderer depends on. -
Registry token-fetch requests now honor operator TLS settings (commit
dfbbd159). GAR, GitLab, Mau, DHI and public-ECR built their own token-fetch request and calledaxios()directly, bypassingwithTlsRequestOptions()— so the credential exchange ignored the configuredcafile/insecure/ client-cert and validated against the system trust store. The shared TLS helper is now applied to those token fetches (Ecrwas realigned to extendBaseRegistry). -
Bearer token-endpoint requests now set
maxRedirects: 0to prevent credential exfiltration via HTTP redirect. A host-validated token endpoint returning a 3xx could otherwise redirect the operator'sBasiccredentials to an attacker-controlled host; settingmaxRedirects: 0causes axios/follow-redirects to fail closed on any redirect response, and the existingfailClosedAuthcatch path turns that into a clean auth failure without forwarding credentials. Applied toBaseRegistry.authenticateBearerFromAuthUrland to the four providers (GAR, GitLab, Mau, DHI) that build their own credentialed token-fetch requests outside the shared helper. The malformed-realm fallback debug log is also sanitized to avoid logging attacker-controlled realm values verbatim (committed separately as786c2317). -
CSRF same-origin enforcement extended to authenticated
/authmutations (commitd1611f88). The/authrouter sat outside the middleware chain that runsrequireSameOriginForMutations, leaving bodylessPOST /auth/logoutandPOST /auth/rememberwithout same-origin protection (exploitable underDD_SERVER_COOKIE_SAMESITE=none). -
OIDC username falls back to the
subclaim beforeunknown(commit8e00dfd3). Identities without anemailclaim previously collapsed to a singleunknownusername sharing one session-eviction bucket. -
schemaVersion-1 manifest parsing guarded (commit
fee0bbcc). A malformed/missingv1Compatibilityfrom a registry threw an unhandled error that silently dropped the container from the watch cycle; it is now fully optional-chained and wrapped with a descriptive error. -
Diagnostic debug dump redacts plural
*_TOKENS_*env vars (commit238727f5). The redaction set matchedtokenbut nottokens, soDD_SERVER_WEBHOOK_TOKENS_*values printed in plaintext. -
Agent HTTP server is rate-limited before authentication (commit
8cddaeab). The shared-secret-gated agent endpoints had no throttle on repeated failed attempts; a 60s/300 limiter now sits ahead of the auth middleware (/healthexempt; the long-lived SSE stream counts once on open). -
Basic-auth string comparison no longer leaks length via timing (commit
6ff55463).timingSafeEqualStringearly-exited on length mismatch; both operands are now hashed to a fixed-length sha256 digest beforetimingSafeEqual, matchingverifyShaPassword. -
Quay pagination tokens percent-encoded (commit
a19e0102).next_page/lastvalues parsed from the registryLinkheader were appended to the request URL unencoded, allowing query-parameter injection from a malicious/buggy registry response. -
Startup warning when falling back to a store-persisted session secret (commit
9431c1d2). WhenDD_SESSION_SECRETis unset, a one-time warning now recommends setting it explicitly and keeping the store directory non-world-readable. -
Startup warning when
trust proxyis set to booleantrue(commit71eac008).DD_SERVER_TRUSTPROXY=truetrusts allX-Forwarded-Forhops, letting clients spoofreq.ipand evade per-IP login lockout; the warning steers operators to a hop count (=1). -
TCP Docker host is validated before the self-update controller passes it to Dockerode (commit
441b4358).validateTcpDockerHostrejects values that contain a URL scheme prefix, a userinfo segment, whitespace, or path separators, throwing a descriptive error before any network connection is attempted. -
Proxied SVG icons sanitized before caching (commit
54d93a3b). SVG payloads fetched from upstream icon CDNs are run through an allowlist-based sanitizer before being written to the icon cache. -
Command action trigger env values sanitized to strip shell metacharacters (commit
1113d8ca). Container-derived values injected into the command subprocess environment are now stripped of shell metacharacters ($,`,;,(,)) before the environment map is passed toexecFile. -
Credential redaction expanded to
x-registry-auth,*-token, andapi-keyfields (commit4417ce25). A second regex pass now redactsx-registry-auth, any field matching*-token, andapi-key/api_keyvalues before the payload leaves the server. -
Credential status pattern matching uses RE2 (commit
df9b914a).BaseRegistry.getRejectedCredentialStatusnow usesRE2JS.compile(…)to maintain the project-wide ReDoS immunity guarantee. -
Registry instances using
insecure=truenow log a warning on every request (commitcd14e3a9). -
DD_SESSION_SECRETis now required; auto-generated and persisted on first boot when unset. (SeeChangedentry above for the full history of rc.20 vs rc.21.) -
Agent connections over plain HTTP with a configured secret are now rejected at startup (commit
7c6f6c20). Use HTTPS or a TLS-terminating proxy for agent connections that require a shared secret. -
GHCR token fallback treats whitespace-only tokens as missing (commit
711d583c). All token checks now call.trim().length > 0. -
GHSA-xq3m-2v4x-88gg (critical) — Bumped
protobufjs7.5.4 → 7.5.5 to close the prototype-chain arbitrary-code-execution advisory. -
GHSA-r4q5-vmmm-2653 (medium) — Bumped
follow-redirectsto 1.16.0 to close the custom-auth-header cross-domain leak advisory. -
Hook command grammar validator — User-supplied pre-update / post-update hook commands are now validated against a restricted shell-safe grammar at config time.
-
OIDC authorization endpoint strict match — The OIDC flow now requires an exact match against the discovered authorization endpoint.
-
OIDC token redaction in error logs — Error log lines from the OIDC pipeline redact bearer / id / refresh tokens.
-
Rate-limit key derivation — Unauthenticated rate-limit buckets now key on
socket.remoteAddressin preference torequest.ip, eliminating theX-Forwarded-Forspoof-ability. -
CORS origin required when enabled — Enabling CORS now requires
DD_SERVER_CORS_ORIGINto be set explicitly. -
OIDC redirect target allowlist — Post-login redirects are now validated against the backend's endpoint allowlist.
-
Healthcheck HTTPS probe hardening —
/bin/healthcheckno longer usespopen()with shell command interpolation. The probe now locates the openssl binary explicitly, fork/execs it with pipes, and uses poll-driven I/O with SIGPIPE handling. -
SSE log IP hashing with opt-in raw mode — SSE connect/disconnect lines log a salted hash of the source IP (
h:xxxxxxxx) by default. SetDD_SSE_DEBUG_LOG_IP=trueto temporarily log raw IPs. -
HTTP trigger proxy URL scheme validation — HTTP trigger proxy URLs must now be
http://orhttps://schemes. -
Vulnerability CSV export escape hardening — Every CSV field is now quoted unconditionally, and tab/CR leading characters are escaped alongside
=+-@. -
Vite CVE patches — Updated vite to 8.0.7 (ui) and 7.3.2 (demo) to fix CVE-2026-39363, CVE-2026-39364, CVE-2026-39365.
-
Axios CVE-2025-62718 — Updated axios 1.13.6 → 1.15.0.
-
fast-xml-parser override 5.5.8 → 5.7.1 — Addresses GHSA-gh4j-gqv2-49f6 / CVE-2026-41650.
-
uuid 13.0.0 → 14.0.0 — Addresses GHSA-w5hq-g745-h8pq.
-
fast-xml-parserupgraded to 5.5.8 — Addresses CVE-2026-33349 (numeric entity expansion bypass). -
Log injection prevention — Removed version string interpolation from startup and migration log messages.
-
Reflected XSS in Podman redirect guard — 404 handler no longer reflects request URL in response body.
-
WebSocket origin and lockout hardening — Added stricter WebSocket origin validation and safer lockout file-permission handling.
-
Agent log entry sanitization — Agent log proxy endpoint now uses an allowlist-based normalizer that only forwards known fields.
-
Security bouncer enforcement on container updates — Update and Update All actions now enforce the security bouncer gate.
-
Vulnerability URL and CSV sanitization — Vulnerability URLs validated before rendering; CSV export fields sanitized against formula injection.
-
Snyk policy file — Added a repo-level
.snykfile for reviewed false-positive Snyk Code findings. -
Supply-chain toolchain refresh — Bumped pinned Alpine edge/testing package versions for
cosignandtrivyin the Dockerfile. -
Binary indices and drain concurrency cap for notification outbox (commit
9393253e).findReadyForDeliveryfields switched to binary indices for O(log n) B-tree lookups.OutboxWorkergains amaxDrainConcurrencyoption (default 10) backed by aDrainSemaphore.
Dependencies
-
Vite 7.3 upgraded to 8.0 — Migrated to Vite 8.0 with Rolldown bundler.
-
Patch/minor dependency bumps — Updated all patch/minor dependencies and upgraded knip to v6.
-
Vulnerable transitive dependency patches — nodemailer 8.0.3→8.0.4, picomatch→4.0.4, brace-expansion→5.0.5, smol-toml→1.6.1, yaml→2.8.3, next 16.2.1→16.2.2 (CVE-2025-59472), lodash 4.17.23→4.18.1 (CVE-2026-2950, CVE-2026-4800).
-
Pinned
piniaandvue-i18nto exact versions (commitfd0b02a5). Both were the only^-ranged UI dependencies; pinned to the locked3.0.4/11.4.2to match the exact-pinning discipline used everywhere else. -
re2js 1.2.3 → 2.8.3 (major). Upgraded the ReDoS-safe RE2 regex engine behind
safeRegExp()(tag include/exclude/transform) across the1.x → 2.xboundary. The only 2.0.0 breaking change — native-ECMAScriptreplaceAll/replaceFirstreplacement semantics — is not exercised by drydock; the compiledcompile/matcher/find/groupsurface is unchanged and Node ≥24 satisfies re2js 2.x's Node ≥18 floor. All 298 tag/regex tests pass unchanged. -
Runtime/security dependency bumps — helmet 8.1.0→8.2.0, undici 8.2.0→8.3.0, ws 8.20.1→8.21.0, axios 1.16.0→1.16.1 (dependency + override), express-rate-limit 8.5.1→8.5.2, nodemailer 8.0.7→8.0.10, semver 7.8.0→7.8.1.
Documentation
-
v1.5.0 deprecation sweep. Migrated every documentation example and test fixture off the v1.5.0-deprecated
DD_TRIGGER_*/dd.trigger.*prefixes onto canonicalDD_NOTIFICATION_*+dd.notification.*(messaging providers) andDD_ACTION_*+dd.action.*(update executors). Touched 29 files incontent/docs/current/**, the in-repo README roadmap, CONTRIBUTING, and all QA/CI/demo compose fixtures. -
Guide/API endpoint synchronization — Updated current docs and guides to consistently use canonical
/api/v1/*paths, expanded container list API docs, and added dashboard customization guide. -
#342 follow-up — Registry env-var naming convention now explained in the registries index. A new "Naming registry instances" callout explains that the
{REGISTRY_NAME}placeholder is a user-chosen label that namespaces multiple instances of the same registry type. -
#342 follow-up — Watcher cron callout explains rate-limit interaction with hourly polling.
1.5.0-rc.29 — 2026-05-30
Fixed
-
Docker Compose update failure safety (#391, #407). Prevented a failed Compose update from destroying the running container.
-
ZAP SARIF path handling (#404, #405). Relativized HTTP artifact URIs through SARIF
originalUriBaseIdsand preserved the leading slash so root-path findings validate in GitHub Code Scanning. -
DAST authentication alignment (#403). Updated the baseline DAST credentials to match the QA compose password hash.
-
Authentication readiness gate (#402). Closed a strategy-registration timing gap so
/healthreports readiness only after authentication strategies are registered.
1.4.5 — 2026-03-17
Added
- Dashboard Update buttons — Per-row update buttons and "Update all" button in the Updates Available dashboard widget. (#173)
- Getting Started guide — New step-by-step onboarding guide covering watchers, tag filters, registries, notifications, auto-updates, safety features, and multi-host setup. (#153)
Fixed
- Container recreate alias filtering — Hardened Docker watcher timestamp parsing, added event handler early return for transient aliases, canonical MQTT topic naming, and stale topic cleanup for recreated containers. (#156)
- About modal version display — Version is now fetched dynamically from the API instead of being hardcoded, ensuring the modal always reflects the running server version. (#167)
- Version resolution fallback —
DD_VERSION=unknownis now skipped so the version is correctly read frompackage.jsonat startup. - Theme circle transition origin — The theme toggle circle animation now originates from the click point instead of the viewport center.
- Trigger code bugs — Gotify URL and Apprise URL now correctly enforce
.required()validation; KafkaclientIdcasing normalized withclientIdkept as a deprecated compatibility alias until v1.6.0.
Security
- OIDC debug log redaction — Sensitive OIDC parameters (
client_id,code_challenge,state, etc.) are now redacted in debug logs. (#168) - Agent API error sanitization — Error logs and responses in the agent API are sanitized to prevent leaking internal details.
- Registry config value redaction — Trigger group configuration now logs keys only, not values, to prevent secret leakage.
Changed
- API versioning — All UI fetch calls migrated from
/api/to/api/v1/paths. - OIDC empty bearer token log level — Downgraded empty bearer token log from
warntodebug. (#169)
Documentation
- Docs audit (78 files) — Fixed 18 doc accuracy issues, 3 code bugs, 22+ broken links, and restructured 8 pages (90 callouts → 36). Triggers overview reduced from 17 callouts to 3 with threshold reference table. Template variables expanded from 12 to 30 with example values. Docker Compose trigger now linked from triggers overview. (#153, #172)
Dependencies
- Security —
fast-xml-parser5.3.8 → 5.5.6 (CVE: numeric entity expansion bypass),next16.1.6 → 16.1.7 (HTTP smuggling, CSRF bypass, DoS) - CI —
step-security/harden-runnerv2.15.1 → v2.16.0,github/codeql-actionv4.32.6 → v4.33.0 - App —
@aws-sdk/client-ecr,@slack/web-api,express-rate-limit,fast-check,knip,nodemailer,undici,@types/node - UI —
@iconify-json/lucide,knip,jsdom28 → 29 - Website —
fumadocs-core,fumadocs-mdx,fumadocs-ui,lucide-react,lefthook,@vercel/analyticsv1 → v2,@vercel/speed-insightsv1 → v2 - Demo —
@iconify-json/lucide,@vitejs/plugin-vue,msw2.10 → 2.12 - E2E —
@dotenvx/dotenvx,fast-xml-parser,minimatch
Chore
- QA infrastructure — Added Portainer, slow-shutdown container, and watchevents service for end-to-end container recreate testing. (#156)
- Coverage and types cleanup — Coverage read retry, OpenAPI re-export, and auth types cleanup.
- Demo version bump — Bumped demo package version to 1.4.5.
- Alias filtering test coverage — Added timestamp edge-case tests for Docker watcher alias filtering. (#156)
- msw worker regenerated — Updated
mockServiceWorker.jsfor msw 2.12 (origin-based → clientId-based validation).
1.4.4 — 2026-03-16
Added
- Click-to-copy on version tags — CopyableTag component with clipboard feedback on all version displays (dashboard, container list, detail panels). (#164)
- Dark mode icon inversion — Simple Icons (
si:prefix) auto-invert in dark mode via Tailwind dark variant. - Tailwind v4 class-based dark mode —
@custom-variant darkdirective for proper.darkclass detection.
Changed
- Dashboard Updates Available version column centered.
- Sidebar search button border removed —
⌘Kbadge improved dark mode contrast. - URL rebrand — All URLs updated from
drydock.codeswhat.comtogetdrydock.com.
Fixed
- Dashboard host-status widget showing 0 for non-agent remote watchers — Dashboard host-status widget incorrectly showed zero container counts for non-agent remote watchers by using
watcher.idinstead ofwatcher.nameas the lookup key. (#155) - Container recreate alias duplicates — Unconditional 30s transient window skip, single inspect per event, event path guard prevent Docker's transient
<id-prefix>_<name>aliases from producing duplicate container entries in triggers. (#156) - Tooltip viewport overflow — Replaced CSS pseudo-element tooltips with body-appended popup using
position:fixedand auto-flip. (#165) - Theme switcher broken — Restored document binding for
startViewTransitionAPI.
1.4.3 — 2026-03-16
Fixed
- DNS resolution failures on Alpine (EAI_AGAIN) — Node.js 24 defaults to
verbatimDNS ordering, which on Alpine's musl libc can causegetaddrinfo EAI_AGAINerrors when IPv6 records are returned first on dual-stack networks. Drydock now defaults to IPv4-first DNS ordering at startup, configurable viaDD_DNS_MODE(ipv4first|ipv6first|verbatim, default:ipv4first). (#161) - Multi-platform Docker build validation — CI now validates multi-platform Docker builds to catch architecture-specific failures early.
Security
- Zizmor findings blocking in CI and lefthook — GitHub Actions security findings from zizmor are now blocking in both CI and local pre-push hooks.
- Secrets scoped to dedicated GitHub environments — CI secrets are now scoped to dedicated GitHub environments instead of repository-level access.
Documentation
- Docker socket security guide — Expanded watcher docs with comprehensive security section: comparison table of all access methods, socket proxy setup (recommended), remote Docker over TLS with cert generation walkthrough, rootless Docker guide, full Docker API endpoint reference showing exactly which endpoints Drydock uses for read-only monitoring vs write operations (updates).
Dependencies
- CI actions — Bumped upload-artifact to v7 and replaced nick-fields/retry.
1.4.2 — 2026-03-15
Added
- Update-operations pagination —
GET /api/containers/:id/update-operationsnow supportslimitandoffsetquery parameters with_linksnavigation, matching the existing container list pagination pattern. - Periodic audit log pruning — Audit store now runs a background timer (hourly, unref'd) to prune stale entries even with low insert volume, in addition to the existing insert-count-based pruning.
- Container release notes enrichment — Watch cycles now enrich update candidates with GitHub release metadata (
result.releaseNotesandresult.releaseLink), and full notes are available viaGET /api/containers/:id/release-notes. - Container list sort and filter query params —
GET /api/containersnow supportssort(name,status,age,created, with optional-prefix for descending), plusstatus,kind,watcher, andmaturityfilters. - Update age and maturity API signals — Container payloads now track
updateAgeand support maturity states (hot,mature,established) for filtering and policy workflows. - Suggested semver tag hints — Containers tracked on non-semver tags such as
latestnow exposeresult.suggestedTagto surface the best semver target. onincludetrigger auto mode — Triggerautonow supportsoninclude, which only auto-runs for containers explicitly matched by include labels. (#160)- Container runtime observability APIs — Added
/api/containers/stats,/api/containers/:id/stats, and/api/containers/:id/stats/streamfor resource telemetry, plus richer per-container runtime snapshots. - Real-time container log streaming — Added WebSocket streaming at
/api/v1/containers/:id/logs/streamwithstdout/stderr,tail,since, andfollowcontrols. - Container logs and runtime stats panels — Container detail views now include dedicated live Logs and Runtime Stats tabs (including full-page logs route support).
- Signed registry webhook receiver — Added
POST /api/webhooks/registrywith HMAC signature verification and provider-specific payload parsing for registry push events. - Auth lockout Prometheus observability — Added Prometheus metrics for login success/failure and lockout activity.
Changed
- Extract maturity-policy module — Consolidated scattered day-to-millisecond conversions and maturity policy constants into
app/model/maturity-policy.ts, shared by backend, UI, and demo app. - Extract security-overview module — Moved security vulnerability aggregation and pagination logic from
crud.tsinto dedicatedapp/api/container/security-overview.tsfor improved readability and testability. - Refactor Docker Compose trigger — Extracted YAML parsing/editing into
ComposeFileParserand post-start hook execution intoPostStartExecutor, reducing the monolithicDockercompose.tsby ~400 lines. - Decompose useContainerActions — Split the 1200-line composable into focused modules:
useContainerBackups,useContainerPolicy,useContainerPreview, anduseContainerTriggers. - Registry error handling — Replaced
catch (e: any)withcatch (e: unknown)andgetErrorMessage(e)in component registration and trigger/watcher startup. - E2E test resilience — Container row count assertions now use
toBeGreaterThan(0)instead of hardcoded counts, preventing false failures when the QA environment has a different number of containers. - Registry digest cache dedup per poll cycle — Digest cache lookups now deduplicate repeated requests within a single poll cycle, reducing redundant registry calls and improving metric accuracy.
- Extract runtime config evaluation context type — Consolidated scattered runtime field evaluation parameters into a typed
ClonedRuntimeFieldEvaluationContextinterface for trigger providers. - Argon2 hash parsing type safety — Extracted
Argon2Parametersinterface, parameter key type guard, and PHC parameter parsing into a reusable function for improved type safety. - Extract agent client initialization methods — Extracted URL parsing, HTTPS detection, protocol validation, and TLS configuration from monolithic constructor into focused private methods.
- Extract shared self-hosted registry config schema — Deduplicated the registry configuration schema (url, login, password, auth, cafile, insecure, clientcert, clientkey) into a reusable helper shared by Custom and SelfHostedBasic registry providers.
Documentation
- Podman docs expansion — Added Podman setup/compatibility guidance plus SELinux,
podman-compose, and production notes in watcher and FAQ docs. - Docs site URL rebrand — Replaced
drydock.codeswhat.comlinks withgetdrydock.comacross docs pages, sitemap/robots metadata, and website copy.
Fixed
- CSRF validation behind reverse proxies — Same-origin mutation checks now honor
X-Forwarded-ProtoandX-Forwarded-Hostwhen present before falling back to direct request protocol/host, preventing false403 CSRF validation failedresponses in TLS-terminating proxy setups. (#146) - Hosts page missing env-var-configured watchers — The Hosts page hardcoded a single "Local" entry and only added agent-based hosts. Watchers configured via
DD_WATCHER_*environment variables (e.g. remote Docker hosts) were never displayed, even though their containers appeared correctly on the Containers page. The page now fetches all watchers from the API and displays each local watcher with its actual name and connection address. (#151) - Docker events reconnect with malformed errors — Reconnect failure logging no longer crashes when the error object has no
messageproperty. - Security overview container count — The security vulnerability overview now uses the lightweight store count instead of loading all container objects just to count them.
- Compose path deduplication — Replaced
indexOf-based dedup withSetfor compose file path detection in container security view. - Build provenance attestation — CI attestation step now runs with
if: always()so provenance is attested even when prior optional steps are skipped. - Container recreate alias duplicates in triggers — When containers were recreated externally (via Portainer or
docker compose up), Docker's transient<id-prefix>_<name>aliases were treated as new containers, producing duplicate entries in MQTT Home Assistant discovery sensors and Telegram notifications. Added alias deduplication filtering and force-removal of stale container IDs on recreate detection. (#156) - Stale store data after external container recreation — Watch-at-start scan was suppressed when the store already had container data, leaving stale records from the previous run after external container recreation. Removed the suppression so every startup runs a full scan with alias filtering. (#157)
- Watcher container counts on Hosts page — Per-watcher container counts on the Hosts page used
watcher.id(e.g.docker.esk83) as the lookup key instead ofwatcher.name(e.g.esk83), causing counts to display as zero for env-var-configured watchers. (#155) - Docker images tagged
:mainwith no version — CI release workflow triggered on both main branch pushes and version tags, producing Docker images tagged:mainthat showedDD_VERSION=maininstead of a real version number. Release workflow now only triggers on version tags (v*). (#154) - Maturity badge sizing and tooltip clipping — Fixed maturity badge height mismatch with other text badges and removed
overflow-hiddenfrom DataListAccordion that clipped tooltips in list view.
Security
- Agent secret over plaintext HTTP warning — Agent clients now log a warning when a shared secret is configured over unencrypted HTTP, advising HTTPS configuration.
- Auth audit log injection — Login identity values are now sanitized with
sanitizeLogParam()before inclusion in audit log details, preventing log injection via crafted usernames. - SSE self-update ack hardening — Added validation for empty
clientId/clientToken, non-ack broadcast mode, and client-not-bound-to-operation rejection. - FAQ: removed insecure seccomp advice — Removed the "Core dumped on Raspberry PI" FAQ entry that recommended
--security-opt seccomp=unconfined, which completely disables the kernel's syscall sandbox. The underlying libseccomp2 bug was fixed in all supported OS versions since 2021.
Dependencies
- biome — Bumped to 2.4.7 with import ordering fixes for new lint rules.
- vitest — Bumped to 4.1.0 in app workspace with fast-check/vitest 0.3.0 and knip 5.86.0.
- UI packages — Bumped vue, vitest, storybook, and icon packages.
- CI actions — Bumped zizmor-action to v0.5.2 and cosign-installer to v4.1.0.
1.4.1 — 2026-03-14
Added
- Headless mode (
DD_SERVER_UI_ENABLED) — Run drydock as an API-only service by settingDD_SERVER_UI_ENABLED=false. The REST API, SSE, and healthcheck endpoints remain fully functional while the UI is not served. Useful for controller nodes that only manage agents. - Maturity-based update policy — Per-container update maturity policy configurable from the container row's Update policy menu (or
PATCH /api/v1/containers/:id/update-policy). Modes:all(default — allow any update) andmature(block updates detected less thanmaturityMinAgeDaysago, default 7). UI shows NEW/MATURE badges with flame/clock icons on containers with available updates. (#120) ?groupByStack=trueURL parameter — Bookmarkable URL parameter to enable stack grouping on the containers page. Also accepts?groupByStack=1. (#145)
Changed
- Connection-lost overlay animation — The connection-lost and reconnecting overlays now use a bounce animation for improved visual feedback.
- Watch target resolution refactored to discriminated union — Internal refactor of the watch target resolution logic for improved type safety and maintainability.
Fixed
- Agent handshake and SSE validation failure — Fixed agent API returning redacted container data (with
sensitivefield on env entries) instead of raw data, causing controller-side Joi validation to reject the handshake and crash on real-time SSE container events. SSE payloads now prefer canonical raw store data with sanitization fallback. (#141) - Mangled argon2 hash detection — Docker Compose
$interpolation can strip$delimiters from argon2 PHC hashes, producing an invalid hash that silently failed registration. Drydock now detects mangled hashes at startup and surfaces an actionable error message. (#147) - Anonymous auth fallback — When all configured auth providers fail to register (e.g. due to a mangled hash), Drydock now falls back to anonymous mode if
DD_ANONYMOUS_AUTH_CONFIRM=trueis set, instead of leaving the user with no way to log in. (#147) - Auth registration errors on login page — Registration warnings (e.g. invalid hash format) are now surfaced on the login page so users can see exactly what went wrong instead of a generic "No authentication methods configured" message. (#147)
- CSP inline style violations — Replaced runtime
element.stylemutations (DataTable column resize, tooltip directive, theme transitions, preference restore) with CSS custom properties and class-based styling. Relaxedstyle-srcto include'unsafe-inline'for vendor libraries (iconify-icon, Vue Transition) that setelement.styleprogrammatically. - Compose trigger affinity across remapped roots — Compose trigger file-path matching now uses suffix-based comparison as a fallback when the container's compose label path (host mount) differs from the trigger's configured path (container-internal), preventing missed trigger associations in bind-mount setups.
- Compose trigger association — Enforce compose-file affinity when associating triggers with containers, preventing incorrect trigger-container matching. (#139)
- Update All button icon centering — Fixed icon-text alignment in the Update All group header button to match the split button pattern used elsewhere.
- Selected card border clipping — Fixed card border clipping on the first grid column in card view.
Security
- Username enumeration timing side-channel — Eliminated timing difference between valid and invalid usernames during authentication.
- LokiJS metadata exposure — Stripped internal LokiJS fields (
$loki,meta) from settings API responses, agent API container responses, and/appendpoint. - Permissions-Policy header — Added
Permissions-Policyheader to restrict browser feature access (camera, microphone, geolocation, etc.). - CSP and Cross-Origin-Embedder-Policy — Tightened Content Security Policy and added COEP header.
- Production image hardening — Removed
wget,nc, andnpmfrom the production Docker image; upgraded zlib.
Dependencies
- undici — Bumped to 7.24.1 (fixes 12 CVEs including WebSocket memory consumption, CRLF injection, and request smuggling).
1.4.0 — 2026-02-28
Breaking Changes
- MQTT
HASS_ATTRIBUTESdefault changed fromfulltoshort— This changes Home Assistant entity payloads by default, excluding large SBOM documents, scan vulnerabilities, details, and labels. To retain the previous payload behavior, setDD_TRIGGER_MQTT_{name}_HASS_ATTRIBUTES=fullexplicitly.
Added
- Audit log for container state changes — External container lifecycle events (start, stop, restart via Portainer or CLI) now generate
container-updateaudit entries with the new status, so the audit log reflects all state changes, not just Drydock-initiated actions. (#120) - mTLS client certificate support — Registry providers now accept
CLIENTCERTandCLIENTKEYoptions for mutual TLS authentication with private registries that require client certificates.
Backend / Core
- Container recent-status API —
GET /api/containers/recent-statusreturns pre-computed update status (updated/pending/failed) per container, replacing the client-side audit log scan and reducing dashboard fetch payload size. - Dual-slot security scanning — "Scan Now" automatically scans both the current running image and the available update image when an update exists. Results are stored in separate slots (
scan/updateScan) and the Security page shows a delta comparison badge (+N fixed, -N new) next to each image that has both scans. DD_LOG_BUFFER_ENABLEDtoggle — Disable the in-memory log ring buffer viaDD_LOG_BUFFER_ENABLED=falseto reduce per-log processing overhead. When disabled,/api/log/entriesreturns an empty array. Defaults totrue.- Scheduled security scanning — Set
DD_SECURITY_SCAN_CRONto automatically scan all watched containers on a cron schedule.DD_SECURITY_SCAN_JITTER(default 60s) spreads load with random delay before each cycle. - Security scheduler shutdown on exit — Security scan scheduler is now explicitly shut down during graceful exit, preventing orphan timers from delaying process termination.
- On-demand sensitive env value reveal — Container environment variables are redacted by default in API responses. Individual values can be revealed on-demand via
/api/containers/:id/env/revealwith audit logging. - On-demand scans populate digest cache — Manual container scans now populate the digest-based dedup cache, preventing redundant rescans of the same image digest.
- Per-container webhook opt-out — New
dd.webhook.enabled=falsecontainer label to exclude individual containers from webhook triggers without disabling the webhook API globally. - Scan cancellation and mobile scan progress — Security batch scans can now be cancelled mid-flight. Mobile scan progress UI improved with responsive layout.
- Security scan coverage counts — Security view header shows scanned/total container counts for at-a-glance scan coverage.
- Notification rule management API and persistence —
/api/notificationsCRUD endpoints backed by LokiJS-persisted notification rules forupdate-available,update-applied,update-failed,security-alert, andagent-disconnectevent types. - Rule-aware runtime dispatch — Trigger event dispatch resolves notification rules at runtime so per-event enable/disable and trigger assignments actively control which triggers fire.
- Security-alert and agent-disconnect events — New event types with audit logging and configurable deduplication windows. Security alerts fire automatically on critical/high vulnerability scan results.
- Compose-native container updates — Compose-managed containers now update via
docker compose up -dlifecycle instead of Docker API recreate, preserving compose ownership and YAML formatting. - Rename-first rollback with health gates — Non-self container updates use a rename-first strategy (rename old → create new → health-gate → remove old) with crash-recoverable state persisted in a new
update-operationstore collection. Rollback telemetry viadd_trigger_rollback_total{type,name,outcome,reason}counter. - Tag-family aware semver selection — Docker watcher infers the current tag family (prefix/suffix/segment style) and keeps semver updates within that family by default, preventing cross-family downgrades like
5.1.4→20.04.1. Addeddd.tag.familylabel (strictdefault,looseopt-out) and imgset support. (#104) - Entrypoint/cmd drift detection — Docker trigger detects whether entrypoint/cmd were inherited from the source image vs user-set, replacing inherited values with target image defaults during update. Adds
dd.runtime.entrypoint.originanddd.runtime.cmd.originlabels. - Self-update controller with SSE ack flow — Dedicated controller container for self-update replaces the shell helper pattern. UI acknowledgment via SSE with operation ID tracking.
- Server-issued SSE client identity — Replaced client-generated UUIDs with server-issued
clientId/clientTokenpairs for self-update ack validation, preventing spoofed acknowledgments. config migrateCLI —node dist/index.js config migrateconverts legacyWUD_*and Watchtower env vars/labels toDD_*/dd.*format across.envand compose files. Supports--dry-runpreview and--source/--fileselection.- Legacy compatibility usage metric — Prometheus counter
dd_legacy_input_total{source,key}tracks local runtime consumption of legacy inputs (WUD_*env vars,wud.*labels) without external telemetry. Startup warns when legacy env vars are detected; watcher/trigger paths emit one-time deprecation warnings onwud.*label fallback. - Bundled selfhst icons for offline startup — Common container icons (Docker, Grafana, Nextcloud, etc.) bundled in the image so the UI works without internet on first boot.
- Runtime tool status endpoint —
/api/server/security/runtimereports Trivy/Cosign availability for the Security view. - Gzip response compression — Configurable via
DD_SERVER_COMPRESSION_ENABLEDandDD_SERVER_COMPRESSION_THRESHOLD(default 1024 bytes), with automatic SSE exclusion. - Container runtime details — Ports, volumes, and environment exposed in the container model and API for the detail panel.
- Update detected timestamp —
updateDetectedAtfield tracks when an update was first seen, preserved across refresh cycles. - No-update reason tracking —
result.noUpdateReasonfield surfaces why tag-family or semver filtering suppressed an available update. - Remove individual skip entries —
remove-skippolicy action allows removing a single skipped tag or digest without clearing all skips. - Update-operation history API —
GET /api/containers/:id/update-operationsreturns persisted update/rollback history for a container. - Settings backend —
/api/settingsendpoints with LokiJS collection for persistent UI preferences (internetless mode). Icon proxy cache with atomic file writes and manual cache clear. - SSE real-time updates — Server-Sent Events push container state changes to the UI without polling.
- Remember-me authentication — Persistent login sessions via remember-me checkbox on the login form.
- Docker Compose trigger — Refresh compose services via Docker Compose CLI when updates are detected.
- Advisory-only security scanning —
DD_SECURITY_BLOCK_SEVERITY=NONEruns vulnerability scans without blocking updates. Scan results remain visible in the Security view and audit log. - OpenAPI 3.1.0 specification and endpoint — Machine-readable API documentation originally shipped on the unversioned OpenAPI route, covering all v1.4 endpoints with request/response schemas. Current releases expose the canonical spec at
GET /api/v1/openapi.json. - Watcher agent support initialization — Watchers now initialize agent support on startup for distributed monitoring readiness.
- Security vulnerability overview endpoint — New
GET /api/containers/security/vulnerabilitiesreturns pre-aggregated vulnerability data grouped by image with severity summaries, so the Security view no longer needs to load all containers. - MQTT attribute filtering for Home Assistant — MQTT trigger supports attribute-based filtering for Home Assistant integration, allowing selective publishing based on container attributes.
- Docker Compose post_start env validation — Docker Compose trigger validates environment variables in
post_starthooks before execution, preventing runtime errors from missing or invalid env var references. - MQTT HASS entity_picture from container icons — When Home Assistant HASS discovery is enabled,
entity_pictureis now automatically resolved from the container'sdd.display.iconlabel. Icons withsh:,hl:, orsi:prefixes map to jsDelivr CDN URLs for selfhst, homarr-labs, and simple-icons respectively. Direct HTTP/HTTPS URLs pass through unchanged. (#138) dd.display.picturecontainer label — New label to override the MQTT HASSentity_pictureURL directly. Takes precedence over icon-derived pictures when set to an HTTP/HTTPS URL.
UI / Dashboard
- Tailwind CSS 4 UI stack — Complete frontend migration from Vuetify 3 to Tailwind CSS 4 with custom shared components. All 13 views rebuilt with Composition API.
- Shared data components — Reusable DataTable, DataCardGrid, DataListAccordion, DataFilterBar, DetailPanel, DataViewLayout, and EmptyState components used consistently across all views with table/cards/list view modes.
- 6 color themes — One Dark (clean/balanced), GitHub (clean/familiar), Dracula (bold purple), Catppuccin (warm pastels), Gruvbox (retro earthy warmth), and Ayu (soft golden tones). Each with dark and light variants. Circle-reveal transition animation between themes.
- 7 icon libraries — Phosphor Duotone (default), Phosphor, Lucide, Tabler, Heroicons, Iconoir, and Font Awesome. Switchable in Config > Appearance with icon size slider.
- 6 font families — IBM Plex Mono (default/bundled), JetBrains Mono, Source Code Pro, Inconsolata, Commit Mono, and Comic Mono. Lazy-loaded from local
/fonts/directory with internetless fallback. - Command palette — Global Cmd/Ctrl+K search with scope filtering (
/pages,@runtime,#config), keyboard navigation, grouped sections, and recent history. - Notification rules management view — View, toggle, and assign triggers to notification rules with direct save through
/api/notifications. - Audit history view — Paginated audit log with filtering by container, event text, and action type. Includes security-alert and agent-disconnect event type icons.
- Container grouping by stack — Collapsible sections grouping containers by compose stack with count and update badges.
- Container actions tab — Detail panel tab with update preview, trigger list, backup/rollback management, and update policy controls (skip tags, skip digests, snooze).
- Container delete action — Remove a container from tracking via table row or detail panel.
- Container ghost state during updates — When a container is updated, stopped, or restarted, its position is held in the UI with a spinner overlay while polling for the recreated container, preventing the "disappearing container" UX issue. (#80)
- Skip update action — Containers with pending updates can be individually skipped, hiding the update badge for the current session without requiring a backend endpoint.
- Slide-in detail panels on all views — Row-click detail panels for Watchers, Auth, Triggers, Registries, Agents, and Security views.
- Interactive column resizing — Drag-to-resize column handles on all DataTable instances.
- Dashboard live data and drag-reorder — Stat cards (containers, updates, security, registries) computed from real container data with drag-reorderable layout and localStorage persistence. Security donut chart, host status, and update breakdown widgets.
- Log viewer auto-fetch and scroll lock — Configurable auto-fetch intervals (2s/5s/10s/30s) with scroll lock detection and resume for both ConfigView logs and container logs.
- Keyboard shortcuts — Enter/Escape for confirm dialogs, Escape to close detail panels.
- SSE connectivity overlay — Connection-lost overlay with self-update awareness and auto-recovery.
- Login page connectivity monitor — Polls server availability and shows connection status on the login screen.
- Server name badge for remote watchers — Shows the watcher name instead of "Local" for multi-host setups.
- Dynamic dashboard stat colors — Color-coded update and security stats based on severity ratio.
- About Drydock modal — Version info and links accessible from sidebar.
- View wiring — Watcher container counts, trigger Test buttons with success/failure feedback, host images count, and registry self-hosted port matching all wired to live API data.
- Font size preference — Adjustable font size slider in Config > Appearance for UI-wide text scaling.
- Announcement banner — Dismissible banner component for surfacing release notes and important notices in the dashboard.
- Dashboard vulnerability sort by severity — Top-5 vulnerability list on the dashboard now sorted by total count descending with critical count as tiebreaker, so the most severe containers appear first.
- Rollback confirmation dialog — Container rollback actions now require explicit confirmation through a danger-severity dialog before restoring from backup.
- Update confirmation dialog — Container update actions now require explicit confirmation through a dialog before triggering an update.
- SHA-1 hash deprecation banner — Dashboard shows a dismissible deprecation banner when legacy SHA-1 password hashes are detected, prompting migration to argon2id.
- Config tab URL deep-linking — Config view tab selection syncs to the URL query parameter, enabling shareable direct links to specific config tabs.
Changed
- Compose trigger uses Docker Engine API — Compose-managed container updates now use the Docker Engine API directly (pull, stop, recreate) instead of shelling out to
docker compose/docker-composeCLI. Eliminatesspawn docker ENOENTerrors in environments without Docker CLI binaries installed. - Compose self-update delegates to parent orchestrator — Self-update for compose-managed Drydock containers now uses the parent Docker trigger's helper-container transition with health gates and rollback, instead of direct stop/recreate.
- Compose runtime refresh extracted — Shared
refreshComposeServiceWithDockerApi()helper eliminates the recursiverecreateContainer→updateContainerWithComposecall chain. Both code paths now converge on the same explicit, non-recursive method. - Compose-file-once batch mode re-enabled —
COMPOSEFILEONCE=truenow works with the Docker Engine API runtime. First container per service gets a full runtime refresh; subsequent containers sharing the same service skip the refresh. - Self-update controller testable entrypoint — Extracted process-level entry logic to a separate entrypoint module, making the controller independently testable without triggering process-exit side effects.
- Dockercompose YAML patching simplified — Removed redundant type guards and dead-code branches from compose file patching helpers, reducing code paths and improving maintainability.
- Dashboard fetches recent-status from backend — Dashboard now fetches pre-computed container statuses from
/api/containers/recent-statusinstead of scanning the raw audit log client-side. - Prometheus collect() callback pattern — Switched container gauge from interval-based polling to the Prometheus
collect()callback, letting Prometheus control collection timing and eliminating the background 5s timer. - Container security API refactored — Container security routes refactored into a dedicated module with type-safe SecurityGate integration, concurrent scan limiting (max 1), and trivy DB status-based cache invalidation.
- DashboardView composable extraction — Extracted 700+ line monolith into
useDashboardData,useDashboardComputed,useDashboardWidgetOrder, and shareddashboardTypesfor better testability and separation of concerns. - Event-driven connectivity polling — AppLayout SSE connectivity monitoring now starts on disconnect and stops on reconnect instead of running a fixed interval, reducing unnecessary network requests.
- Vulnerability loading optimized — Vulnerability data loaded from the container list API payload (
includeVulnerabilitiesflag) instead of separate per-container fetches, reducing API calls on the Security view. - Default log format is JSON — Official Docker image now defaults to
DD_LOG_FORMAT=jsonfor structured production logs. Override withDD_LOG_FORMAT=textfor pretty logs. - Scan endpoint rate limit reduced —
POST /api/containers/:id/scanrate limit lowered from 100 to 30 requests/min to prevent resource exhaustion during aggressive scanning. - Single Docker image — Removed thin/heavy image variants; all images now bundle Trivy and Cosign.
- Removed Vuetify dependency — All Vuetify imports, components, and archived test files removed. Zero Vuetify references remain.
- Fail-closed auth enforcement — Registry bearer-token flows error on token endpoint failures instead of falling through to anonymous. HTTP trigger auth errors on unsupported types. Docker entrypoint requires explicit
DD_RUN_AS_ROOT+DD_ALLOW_INSECURE_ROOTfor root mode. - Fail-closed anonymous auth on fresh installs — New installs with no authentication configured and no
DD_ANONYMOUS_AUTH_CONFIRM=truefail closed at startup (all API calls return 401). Users upgrading from a previous version are allowed anonymous access with a startup warning. SetDD_AUTH_BASIC_<name>_USER/DD_AUTH_BASIC_<name>_HASHto configure authentication, or setDD_ANONYMOUS_AUTH_CONFIRM=trueto explicitly allow anonymous access. - Dashboard streamlined — Stat cards reduced from 7 to 4 (Containers, Updates, Security, Registries). Recent Activity widget removed to fit on single viewport. Background refresh prevents loading flicker on SSE events.
- Notifications view is full rule management — Editable notification rules (enable/disable and trigger assignments) that save directly through
/api/notifications. - Standardized API responses with collection pattern — All collection endpoints (
/api/containers,/api/registries,/api/watchers,/api/authentications,/api/audit) now return{ data: [...], total }instead of raw arrays. Supports pagination viaoffset/limitquery parameters. - Paginated API discoverability links — Paginated collection responses now include
_links.selfand_links.nextwhere applicable (for example/api/containersand/api/audit) to make page traversal explicit for API consumers. - Versioned API base path with transition alias —
/api/v1/*is now the canonical API path for integrations./api/*remains as a backward-compatible alias during migration and is planned for removal in a future major release (target: v2.0.0). [Editorial note: the removal target was later brought forward to v1.6.0 — see deprecations.] - Agent-scoped path order normalized — Agent-qualified component and trigger routes now use
/:type/:name/:agent(for example/api/triggers/:type/:name/:agentand/api/containers/:id/triggers/:triggerType/:triggerName/:triggerAgent) instead of the old agent-first order. This is a breaking change for external API clients using the old path shape. - Machine-readable API error contract — All error responses return a consistent
{ "error": "..." }JSON structure with an optionaldetailsobject for contextual metadata. - 6 color themes — Replaced original Drydock theme with popular editor palettes: One Dark, GitHub, Dracula, Catppuccin, Gruvbox, and Ayu. Each with dark and light variants.
- Argon2id password hashing — Basic auth now uses argon2id (OWASP recommended) via Node.js built-in
crypto.argon2Sync()instead of scrypt for password hashing. Default parameters: 64 MiB memory, 3 passes, parallelism 4. - PUT
/api/settingsdeprecated —PUT /api/settingsnow returns RFC 9745Deprecationand RFC 8594Sunsetheaders. UsePATCH /api/settingsfor partial updates. PUT alias removal targeted for v1.5.0. [Editorial note: the removal target was later moved to v1.6.0 — see deprecations.] - Basic auth argon2id PHC compatibility — Basic authentication now accepts PHC-format argon2id hashes (
$argon2id$v=19$m=...,t=...,p=...$salt$hash) in addition to the existing Drydockargon2id$memory$passes$parallelism$salt$hashformat. Hash-generation guidance now recommends the standardargon2CLI command first, with Node.js as a secondary option. - Borderless UI redesign — Removed borders from all views, config tabs, detail panels, and shared data components for a cleaner visual appearance.
- Dashboard version column alignment — Version column in the dashboard updates table is now left-aligned for better readability.
- Detail panel expand button redesigned — Full-page expand button in the detail panel now uses a frame-corners icon instead of the previous maximize icon.
- Sidebar active indicator removed — Removed the blue active indicator bar from sidebar navigation items for a cleaner look.
Fixed
-
Log level setting had no effect —
DD_LOG_LEVEL=debugwas correctly parsed but debug messages were silently dropped because pino's multistream destinations defaulted toinfolevel. Stream destinations now inherit the configured log level. (#134) -
Server feature flags not loaded after login — Feature flags (
containeractions,delete) were permanently stuck as disabled when authentication was required, because the pre-login bootstrap fetch failure marked the flags as "loaded" and never retried. Now failed fetches allow automatic retry after login. (#120) -
Compose trigger silently skips containers — Multiple failure paths in the compose trigger were logged at
debuglevel, making it nearly impossible to diagnose why a trigger reports success but containers don't update. Key diagnostic messages (compose file mismatch, label inspect failure, no containers matched) promoted towarnlevel, and the "already up to date" message now includes container names. (#84) -
Fallback icon cached permanently — The Docker placeholder icon was served with
immutablecache headers, causing browsers to cache it permanently even after the real provider icon becomes available. Fallback responses now useno-store. -
Basic auth upgrade compatibility restored — Basic auth now accepts legacy v1.3.9 Basic auth hashes (
{SHA},$apr1$/$1$,crypt, and plain fallback) to preserve smooth upgrades. Legacy formats remain deprecated and continue showing a migration banner, with removal still planned for v1.6.0. -
Compose trigger rejects lowercase env var keys — Configuration keys like
COMPOSEFILEONCE,DIGESTPINNING, andRECONCILIATIONMODEwere lowercased by the env parser but the Joi schema expected camelCase. Schema now maps lowercase keys to their camelCase equivalents. (#120) -
Compose trigger strips docker.io prefix — When a compose file uses an explicit
docker.io/registry prefix, compose mutations now preserve it instead of stripping it to a bare library path. (#120) -
Compose trigger fails when FILE points to directory —
DD_TRIGGER_DOCKERCOMPOSE_{name}_FILEnow accepts directories, automatically probing forcompose.yaml,compose.yml,docker-compose.yaml, ordocker-compose.ymlinside the directory. (#84) -
Container healthcheck fails with TLS backend — The Dockerfile healthcheck now detects
DD_SERVER_TLS_ENABLED=trueand switches tocurl --insecure https://for self-signed certificates. Also skips the healthcheck entirely whenDD_SERVER_ENABLED=false. (#120) -
Agent CAFILE ignored without CERTFILE — The agent subsystem now loads the CA certificate from
CAFILEeven whenCERTFILEis not provided, fixing TLS verification for agents behind reverse proxies with custom CA chains. -
Service worker accepts cross-origin postMessage — The demo service worker now validates
postMessageorigins against the current host, preventing potential cross-origin message injection. -
Action buttons disable and show spinner during in-progress actions — Container action buttons (Stop, Start, Restart, Update, Delete) now show a disabled state with a spinner while the action runs in the background, providing clear visual feedback. The confirm dialog closes immediately on accept instead of blocking the UI.
-
Command palette clears stale filter on navigation — Navigating to a container via Ctrl+K search now clears the active
filterKind, preventing stale filter state from hiding the navigated container. -
Manual update button works with compose triggers — The update container endpoint now searches for both
dockeranddockercomposetrigger types, matching the existing preview endpoint behavior. Previously, users with only a compose trigger saw "No docker trigger found for this container". -
CI: qlty retry on timeout — Changed
retry_onfromerrortoanyso qlty timeouts trigger retries. Increased timeout from 5 to 8 minutes. -
OIDC docs clarified
DD_PUBLIC_URLrequirement — OIDC documentation now explicitly marksDD_PUBLIC_URLas required and includes it in all provider example configurations (Authelia, Auth0, Authentik, Dex). Without this variable, the OIDC provider fails to register at startup. -
Compose trigger docs updated for Engine API — Removed stale references to
docker compose config --quietCLI validation anddocker compose pull/upcommands. Docs now reflect in-process YAML validation and Docker Engine API runtime. Added callout that Docker Compose CLI is not required. -
Container actions docs updated for compose triggers — Update action documentation now mentions both Docker and Docker Compose trigger support.
-
Compose trigger rejects compose files mounted outside app directory — Removed overly strict working-directory boundary enforcement from
runComposeCommandthat rejected compose files bind-mounted outside/home/node/app, breaking documented mount patterns like/drydock/docker-compose.yml. Compose file paths are operator-configured and already validated during resolution. -
Compose trigger uses host paths instead of container paths — Docker label auto-detection (
com.docker.compose.project.config_files) now remaps host-side paths to container-internal paths using Drydock's own bind mount information. Previously, host paths like/mnt/volume1/docker/stacks/monitoring/compose.yamlwere used directly inside the container where they don't exist, causing "does not exist" errors even when the file was properly mounted. -
Compose trigger logs spurious warnings for unrelated containers — When multiple compose triggers are configured, each trigger now silently skips containers whose resolved compose files don't match the trigger's configured
FILEpath, eliminating noisy cross-stack "does not exist" warnings. -
Silent error on recheck failure — "Recheck for Updates" button now displays an error banner when the backend request fails instead of silently stopping the spinner with no feedback.
-
Silent error on env reveal failure — Environment variable reveal in the container detail panel now shows an inline error message when the API call fails instead of silently failing.
-
Security scans persist across navigation — Navigating away from the Security view no longer cancels in-flight batch scans. Module-scoped scan state survives unmount and the progress banner reappears on return.
-
SSE stale sweep timer on re-initialization — Stale client sweep interval now starts even when
init()is called after a hot reload, preventing leaked SSE connections. -
About modal documentation icon — Documentation link in the about modal now shows a book icon instead of the expand/maximize icon.
-
Log auto-fetch pauses in background tabs —
useAutoFetchLogsnow stops polling when the browser tab is hidden and automatically resumes when it becomes visible again. -
SBOM download DOM isolation — Isolated DOM element creation and
URL.createObjectURLreferences in the SBOM download composable, fixing potential memory leaks and test failures from uncleared object URLs. JSON serialization skipped when SBOM panel is hidden. -
Dashboard resource fetch error propagation — Dashboard API fetch errors are now propagated consistently to the UI error state instead of being silently swallowed.
-
Docker image "latest" tag restricted to stable releases — CI release workflow no longer tags prerelease versions as
latest, preventing unstable images from being pulled by default. -
Security table refreshes progressively during batch scan — Security view table updates incrementally as each container scan completes instead of waiting for the entire batch to finish.
-
Vulnerability data fetched from per-container endpoint — Security view now fetches vulnerability data from the correct per-container endpoint instead of a missing bulk endpoint.
-
OIDC callback session loss with cross-site IdPs — Session cookies now default to
SameSite=Laxfor auth compatibility, fixing callback flows that could fail underSameSite=Strict. AddedDD_SERVER_COOKIE_SAMESITE(strict|lax|none) for explicit control. (#52) -
Compose trigger handles unknown update kinds — Containers with
updateKind.kind === 'unknown'now triggerdocker compose pullinstead of silently skipping. (#91) -
Compose image patching uses structured YAML edits — Replaced regex/indent heuristics with YAML parser targeting only
services.<name>.image, preserving comments and formatting. -
Hub/DHI public registries preserved with legacy token envs — Public registry fallback no longer lost when a private token is configured. Fail-closed behavior remains for private registry auth and runtime token exchange failures.
-
GHCR retries anonymously on credential rejection — Public image checks continue when configured credentials are rejected by GHCR/LSCR.
-
Partial registry registration failures isolated —
Promise.allSettledprevents a single bad registry from taking down all registries including the public fallback. -
Auth-blocked remote watchers stay registered — Remote watchers that fail auth now show as degraded instead of crashing watcher init.
-
Docker event stream reconnects with exponential backoff — Watcher reconnects automatically (1s doubling to 30s max) instead of staying disconnected after Docker socket interruption.
-
SSE frames flushed immediately — Added
X-Accel-Buffering: noand explicitflush()to prevent nginx/traefik from buffering real-time events. -
Store flushed on graceful shutdown — Explicit
save()call on SIGTERM/SIGINT prevents data loss between autosave intervals. -
Digest value populated on registration and refresh — Digest-watch containers no longer show undefined digest in the UI.
-
Icon fallback for missing upstream — Icon proxy returns bundled Docker fallback instead of 404 when upstream providers return 403/404. Fixes registry port parsing in icon URLs.
-
Container groups route no longer shadowed —
/containers/groupsmounted before/containers/:idto prevent Express treating group requests as container ID lookups. -
Runtime env values redacted in API responses — Container environment variable values no longer exposed through the API.
-
Logger init failure produces structured stderr — Falls back to structured JSON on stderr instead of silent no-op when logger init fails.
-
Mobile sidebar closes on route change — Safety-net watcher ensures mobile menu closes on any navigation.
-
Security badge counts only scan vulnerabilities — No longer inflated by major version updates.
-
Trigger test failure shows parsed error message — Actionable error reason displayed below trigger card on test failure.
-
Viewport scrollbar eliminated — Fixed double-nested scroll contexts; long tags truncated with tooltips.
-
Self-hosted registries ignore port when matching — Registry matching now respects port numbers in self-hosted registry URLs, preventing mismatches between registries on different ports of the same host.
-
Socket proxy ECONNREFUSED with
:romount — Removed:roflag from Docker socket mount in all socket proxy compose examples. The read-only flag can prevent the proxy from connecting to the Docker daemon on some Linux kernels; the proxy's environment variable filtering (CONTAINERS=1,EVENTS=1, etc.) is the actual security boundary. Added health check anddepends_on: condition: service_healthyto all socket proxy examples for proper startup ordering. Added FAQ entry for troubleshooting ECONNREFUSED with socket proxies. -
Apprise trigger missing Content-Type header — Apprise notify requests now set explicit
Content-Type: application/jsonheader, fixing failures with Apprise servers that require it. -
Toggle switch contrast — Improved toggle thumb contrast across all themes for better visibility.
-
Empty env var values rejected during container validation — Joi schema on
details.env[].valueused.string().required()which implicitly disallows empty strings. Containers with empty environment variable values (e.g.FOO=) were silently skipped during watch cycles. Fixed with.allow(''). (#120) -
OIDC login broken by same-origin redirect check —
LoginView.vuerestricted OIDC redirects to same-origin URLs, but OIDC Identity Provider URLs are always cross-origin (Authentik, Keycloak, etc.). Removed the same-origin check, keeping only HTTP/HTTPS protocol validation. -
Blank white screen on plain HTTP deployments — Helmet.js defaults enabled HSTS and
upgrade-insecure-requestsCSP even when TLS was not configured, causing browsers to block sub-resource loads on plain HTTP. Now conditionally omitsupgrade-insecure-requestsand HSTS when TLS is off. Strict boolean check ontls.enabledprevents string coercion. (#120) -
Stale theme preferences after upgrade from v1.3 — Preferences migration
deepMergeoverwrote defaults with persisted values unconditionally, so invalid enum values (e.g. removeddrydocktheme family) survived migration and caused rendering failures. Addedsanitize()pass that strips invalid theme families, variants, font families, icon libraries, scales, radius presets, view modes, and table actions before merge. -
OIDC discovery fails on HTTP IdP URLs — openid-client v6 enforces HTTPS by default for all OIDC requests. Users running IdPs behind reverse proxies or on private networks with HTTP discovery URLs got "only requests to HTTPS are allowed" errors. Now passes
allowInsecureRequestswhen the discovery URL protocol ishttp:. -
Docker Compose trigger fails with EBUSY on bind-mounted stacks —
writeComposeFileAtomic()used a singlefs.rename()call that failed permanently when another process (e.g. Dockge) held the file or when Docker bind-mount overlay contention blocked the rename. Now retries up to 5 times with 200ms backoff, then falls back to a directwriteFileif rename remains blocked. (#84) -
Drydock container display name hardcoded — The Docker watcher hardcoded
drydockas the display name for drydock's own container instead of using the actual container name like every other container. -
Non-existent DD_OIDC_ALLOW_HTTP env var referenced in UI — The UI OIDC HTTP banner referenced a
DD_OIDC_ALLOW_HTTPenv var that does not exist — the backend auto-detectshttp://discovery URLs and passesallowInsecureRequestsautomatically. Removed the misleading reference. -
Load test and start scripts broken by standardized API responses —
jqqueries inrun-load-test.shandstart-drydock.shused raw array syntax instead of.data[]to match the new collection response pattern. -
Container status not reconciled on cron poll — Containers that changed state between Docker event stream updates (e.g. stopped externally) were not reconciled during periodic cron polls. Now reconciles container status on each poll cycle.
-
DD_AUTH_ANONYMOUS_CONFIRM env var alias rejected —
DD_AUTH_ANONYMOUS_CONFIRMwas not accepted as an alias for the canonicalDD_ANONYMOUS_AUTH_CONFIRMenv var, forcing users to use only the non-prefixed form. -
LSCR cross-host auth failure — LinuxServer Container Registry (lscr.io) token exchange failed because the auth flow required cross-host authentication via the ghcr.io token endpoint. Now allows cross-host auth for lscr.io.
-
iPad sidebar clipping — Used
dvh(dynamic viewport height) instead ofvhfor sidebar height to fix clipping on iPad and other mobile browsers where the toolbar reduces available viewport height. -
Mobile dashboard scroll clipping — Dashboard content was clipped on mobile viewports when scrolling, preventing users from reaching all content.
-
Lockout counter reset on failed login — Brute-force lockout counter could reset prematurely, and strategy IDs were not protected from enumeration. Fixed counter persistence and added strategy ID protection.
-
Stale vulnerability data on fetch error — Security view retained stale vulnerability data when a fetch error occurred instead of clearing it, showing outdated information.
-
Audit service missing limit parameter — Audit service requests did not always send the
limitquery parameter, causing inconsistent pagination behavior. -
Backup retention on failed updates — Backup entries are now pruned on the failure path, not just after successful updates, preventing indefinite accumulation of stale backups.
-
Backup pruning with undefined maxCount —
pruneOldBackups()no longer deletes all backups whenmaxCountisundefined(e.g. whenDD_BACKUPCOUNTis not configured). Now correctly no-ops on invalid or non-finite values. -
Auto-rollback audit fromVersion accuracy — Rollback audit entries now correctly record
fromVersionas the failing new image tag (viaupdateKind.remoteValue) instead of the pre-update old tag. -
HASS entity_picture URL broken after logo rename — MQTT HASS discovery payload referenced a renamed logo file (
drydock.pnginstead ofwhale-logo.png), causing missing entity pictures in Home Assistant. (#138) -
Watcher crashes on containers with empty names — Docker watcher's same-name deduplication filter threw errors when containers had empty or missing names. Now skips deduplication for unnamed containers.
-
Container names not reconciled after external recreate — Containers recreated externally (via Portainer or
docker compose up) retained stale names in the store until the next full poll cycle. Now reconciles container names immediately on detection. -
Nested icon prefixes fail proxy request — Icon proxy rejected icons with doubled prefixes like
mdi:mdi-docker. Now normalizes nested prefixes before proxying. -
Colon-separated icon prefixes rejected —
dd.display.iconlabels using colon separators (e.g.,sh:nextcloud) were rejected by the API validation pattern. Validation now accepts colon-prefixed icon identifiers. -
Bouncer-blocked state missing from container details — Container detail views didn't reflect bouncer-blocked status. Now correctly wires the blocked state into detail panel display.
Security
- Security API error sanitization — SBOM and scan error responses now return generic messages (
Error generating SBOM,Security scan failed) instead of leaking internal error details. Detailed errors logged server-side only. - Agent log query parameter validation — Agent log level and component query parameters are validated against an allowlist and safe-character pattern, returning 400 for invalid values.
- TLS key/cert paths stripped from config response — Server configuration API response no longer includes filesystem paths for TLS private key and certificate files.
- Type-safe store and auth modules — Settings, notification, and auth store modules upgraded from
anyto explicit typed interfaces, preventing implicit type coercion vulnerabilities. - Fail-closed auth enforcement across registries and triggers — Bearer token, OIDC, and credential flows use
failClosedAuthwith typedRequestOptions, rejecting requests when required credentials are missing. - Input validation hardened — Additional input validation and error redaction across auth, API, and configuration modules.
- Mutation-only JSON body parser — Express JSON body parsing restricted to mutation methods (POST/PUT/PATCH) only on both API and auth routers, reducing attack surface on read requests.
- CSRF Sec-Fetch-Site validation — CSRF middleware now rejects requests with
Sec-Fetch-Site: cross-siteheader, blocking cross-site state-changing requests even when the Origin header is absent. - HTTPS enforcement for SameSite=none cookies —
DD_SERVER_COOKIE_SAMESITE=nonenow requires HTTPS configuration (DD_SERVER_TLS_ENABLED=trueorDD_SERVER_TRUSTPROXY) and throws at startup if neither is set. - Remember-me endpoint requires authentication —
/auth/rememberPOST moved afterrequireAuthenticationmiddleware, preventing unauthenticated access. - Env reveal rate limit tightened —
/api/containers/:id/envrate limit reduced from 100/min to 10/min to prevent credential enumeration. Server error responses return generic messages instead of internal details. - Trivy command path validation — Trivy binary paths are validated against shell metacharacters and path traversal before execution.
- Digest scan cache LRU eviction — Scan result cache uses LRU eviction (max 500 entries, configurable via
DD_SECURITY_SCAN_DIGEST_CACHE_MAX_ENTRIES) to prevent unbounded memory growth. Trivy DB status lookups are deduplicated across concurrent calls. - CSP configured for Iconify CDN — Content-Security-Policy updated to allow
connect-srcfor the Iconify CDN origin, preventing blocked icon fetches in the browser. - CSP connect-src restricted in internetless mode — Content-Security-Policy
connect-srcdirective tightened to'self'when running in internetless mode, blocking outbound connections from the browser. - Legacy auth methods endpoint rate-limited —
/api/auth/methodsrate-limited to prevent enumeration of available authentication providers. - Removed plaintext credentials from login request body — The Basic auth login was redundantly sending username and password in both the Authorization header and the JSON body. The backend only reads the Authorization header via Passport, so the body credentials were unnecessary exposure.
- Server-issued SSE client identity — Self-update ack requests validated against server-issued tokens, preventing spoofed acknowledgments.
- Fail-closed auth across watchers, registries, and triggers — Token exchange failures no longer fall through to anonymous access.
- Runtime env values redacted — Container environment variable values stripped from API responses to prevent credential leakage.
- OIDC authorization redirect URL validation — Added allowlist-based validation for OIDC authorization redirect URLs, preventing open redirect attacks through crafted callback parameters.
- Auth, registry token, and log sanitization hardening — Consolidated security pass hardening authentication flows, registry token validation, and log output sanitization.
- Command trigger shell execution warning — Command trigger now logs a one-time security warning on first execution, reminding operators that commands run with drydock process privileges.
- Login brute-force lockout — Per-account and per-IP lockout after configurable failed login attempts (
DD_AUTH_ACCOUNT_LOCKOUT_MAX_ATTEMPTS,DD_AUTH_IP_LOCKOUT_MAX_ATTEMPTS) with configurable window and duration. - Concurrent session limits — Maximum authenticated sessions per user (default 5, configurable via
DD_SERVER_SESSION_MAXCONCURRENTSESSIONS). Oldest sessions are revoked first when the limit is reached. - Destructive action confirmation header — Dangerous operations (delete container, restore backup, delete all containers) now require an
X-DD-Confirm-Actionheader, returning 428 when missing. - Native argon2id password hashing — Replaced abandoned
passpackage withnode:cryptoargon2Sync for Basic auth. OWASP-aligned parameters with timing-safe comparison. Legacy{SHA}hashes accepted with deprecation warnings. - Full credential redaction —
Component.mask()now returns[REDACTED]instead of leaking prefix/suffix characters in logs and API responses. - Trigger infrastructure config redaction — Webhook URLs, hostnames, channels, and usernames are redacted from trigger configuration in API responses.
- Website SRI integrity hashes — Post-build script injects subresource integrity hashes for static assets on the documentation website.
- Fail-closed webhook token enforcement — Per-endpoint webhook tokens now fail closed when a token is configured but the request provides no token or a mismatched token, preventing bypass through missing headers.
- API error message sanitization — API routes no longer expose raw Joi validation or exception messages to clients. New
sanitizeApiError()helper returns generic messages while logging real details server-side. - Trigger request body schema validation —
POST /api/triggers/:type/:nameand remote trigger endpoints now validate request bodies with Joi (requireidstring, reject type coercion viaconvert: false). - HTTP trigger auth schema enforcement at startup — HTTP trigger Joi schema now conditionally requires
user+passwordfor BASIC auth andbearerfor BEARER auth at registration time, catching misconfigurations before first trigger execution. - CORS implicit wildcard origin deprecation warning — Startup warning when
DD_SERVER_CORS_ENABLED=truewithout explicitDD_SERVER_CORS_ORIGIN. Default wildcard will require explicit opt-in in v1.5.0. - Identity-aware rate limit keying — Opt-in
DD_SERVER_RATELIMIT_IDENTITYKEYING=truekeys authenticated route rate limits by session/username instead of IP, preventing collisions for multiple users behind shared proxies. Unauthenticated routes remain IP-keyed. Disabled by default. - Reactive server feature flags in UI — Container action buttons (update, rollback, scan, triggers) are now gated by server-side feature flags via a
useServerFeaturescomposable. When features likeDD_SERVER_FEATURE_CONTAINERACTIONSare disabled, buttons show a disabled state with tooltip explaining why instead of silently failing at runtime. - Compose trigger hardening — Auto compose file detection from container labels (
com.docker.compose.project.config_files) with Docker inspect fallback, pre-commitdocker compose config --quietvalidation before writes, compose file reconciliation (warn/block modes for runtime vs compose image drift), optional digest pinning (DIGESTPINNINGtrigger config), compose-file-once batch mode for multi-service stacks, multi-file compose chain awareness with deterministic writable target selection, compose metadata in update preview API, and compose file path display in container detail UI. - Unsupported hash formats fail closed — Basic auth now rejects unsupported hash formats instead of falling through to plaintext comparison, preventing accidental plaintext password acceptance.
Performance
- Production source maps disabled — UI production builds now exclude source maps, reducing bundle size and improving deployment efficiency.
- Audit store indexed date-range queries — Audit entries now store a pre-parsed
timestampMsindex for numeric comparisons. Date-range queries use the LokiJS chain API with indexed filtering instead of full-collection scans. Automatic 30-day retention with periodic pruning. - Backup store indexed lookups — Backup collection adds indices on
data.containerNameanddata.id, usingfindOne()for single-document lookups and indexedfind()for name-filtered queries instead of full scans. - LokiJS autosave interval set to 60 seconds — Fixed autosave interval at 60s instead of the LokiJS default, reducing disk I/O while maintaining acceptable data durability.
- SSE shared heartbeat interval — Deduplicated per-client SSE heartbeat timers into a single shared interval that starts on first connection and stops when all clients disconnect.
- LoginView exponential backoff — Login page connectivity retry uses exponential backoff (5s doubling to 30s max) instead of fixed intervals, reducing server load during outages.
- Gzip response compression — API responses compressed above configurable threshold with automatic SSE exclusion.
- Skip connectivity polling when SSE connection is active — Eliminates unnecessary
/auth/userfetches every 10s during normal operation. - Set-based lookups replace linear scans — Repeated array lookups converted to Set operations in core paths.
- Vite chunk splitting — Production build now splits vendor code into
framework,icons, andvendorchunks for better browser cache efficiency across deployments. - Session index cache with atomic login locks — Auth session lookups use an indexed cache for O(1) access. Login operations use atomic locks to prevent race conditions during concurrent authentication attempts.
- Proactive store cache eviction — LokiJS store proactively evicts stale cache entries before insertion, reducing memory pressure during high-throughput container watch cycles.
- Async secret file loading — Secret file loading (
DD_*__FILEenv vars) usesfs/promisesfor non-blocking reads during startup configuration, improving initialization time with many secret files.
Dependencies
- cron-parser v2 to v5 — Upgraded
cron-parserfrom v2 to v5 (CronExpressionParserAPI). Note:joi-cron-expressionstill uses cron-parser v2 internally as its own dependency. - Removed
passpackage — Replaced abandonedpasspassword hashing with nativenode:crypto(covered under Security below). - Trivy upgraded to 0.69.3 — Bumped Trivy version in qlty configuration for latest vulnerability database and scanner improvements.
Deprecated
- SHA-1 basic auth password hashes — Legacy
{SHA}<base64>password hashes are accepted with deprecation warnings. SHA-1 support will be removed in v1.6.0. Migrate to argon2id hashing.
1.3.9 — 2026-02-22
Fixed
- Release signing broken by cosign v3 API change —
cosign sign-blobv3 silently ignores--output-signatureand--output-certificatein keyless OIDC mode, producing an empty.sigfile that fails upload. Release workflow now extracts signature and certificate from the cosign.bundleJSON as a fallback, handling both old (base64Signature/cert) and new (messageSignature.signature/verificationMaterial.certificate.rawBytes) bundle formats. - Shellcheck SC2086 in release signing step — Unquoted
${TAGS}expansion in container image signing replaced withread-loop into array to eliminate word-splitting/globbing risk.
Changed
- CI and lefthook now run identical lint checks — CI lint job previously ran
qlty check --filter biome(1 plugin) while lefthook ranqlty check(17 plugins). Both now runqlty check --allfrom the repo root, ensuring local pre-push catches exactly what CI catches. - Pre-commit hook auto-fixes lint issues —
qlty check --fixruns on staged files at commit time, followed by a verify step. Lint drift no longer accumulates until push time. - Lefthook pre-push is sequential fail-fast — Switched from
piped: false(parallel) topiped: truewith priority ordering so failures surface immediately with clear output.
1.3.8 — 2026-02-22
Fixed
- Docker Compose trigger silently no-ops for
updateKind: unknown— When the update model classifies a change asunknown(e.g. created-date-only updates, unrecognized tag formats),getNewImageFullNameresolved the update image identically to the current image, causing both compose-update and runtime-update filters to return empty arrays and log "All containers already up to date". The runtime-update filter now also triggers whencontainer.updateAvailable === true, ensuring containers with confirmed updates are recreated regardless ofupdateKindclassification. Compose file rewrites remain gated on explicit tag deltas. (#91) - Digest watch masks tag updates, pulling old image — When digest watch was enabled on a container with both a tag change and a digest change (e.g.
v2.59.0-s6→v2.60.0-s6), the update model gave digest unconditional priority, returningkind: 'digest'instead ofkind: 'tag'. The trigger then resolved the image to the current tag (correct for digest-only updates) instead of the new tag, pulling the old image. Tag updates now take priority over digest when both are present. This bug was inherited from the upstream project (WUD). (#91) - Database not persisted on container shutdown — LokiJS relies on its autosave interval to flush data to disk, but the graceful shutdown handler called
process.exit()before the next autosave tick could fire, causing any in-memory changes since the last autosave to be lost. This manifested as stale version numbers, lost update policies, and missing audit log entries after restarting the drydock container. Now explicitly saves the database during shutdown before exiting. This bug was inherited from the upstream project (WUD) but made deterministic by our graceful shutdown changes. (#96)
1.3.7 — 2026-02-21
Fixed
- Tag regex OOM crash with re2-wasm — Replaced
re2-wasmwithre2js(pure JavaScript RE2 port). The WASM binary had a hard 16 MB memory ceiling with no growth allowed, causingabort()crashes on valid regex patterns like^v(\d+\.\d+\.\d+)-ls\d+$. Sincere2-wasmis abandoned (last npm publish Sep 2021) with no path to a fix,re2jsprovides the same linear-time ReDoS protection without WASM memory limits or native compilation requirements. (#89) - Self-signed/private CA support for self-hosted registries — Added optional
CAFILEandINSECURETLS options for self-hosted registry providers (Custom, Gitea, Forgejo, Harbor, Artifactory, Nexus). This allows private registries with internal or self-signed certificates to pass TLS validation via a mounted CA bundle, or to explicitly disable verification for trusted internal networks. (#88) - Docker Compose trigger silently no-ops on digest updates — Digest-only updates (same tag, new image hash) were filtered out entirely because the compose image string didn't change, causing the trigger to report success without recreating the container. Now digest updates skip the compose file write (correct — tag hasn't changed) but still trigger container recreation to pull the new image. (#91)
Changed
- Gitea refactored to shared base class — Gitea now extends
SelfHostedBasicdirectly instead of duplicating its logic fromCustom, reducing code and ensuring consistent behavior with Harbor, Nexus, and Artifactory. - Lint tooling migrated from biome CLI to qlty — Removed
@biomejs/biomeas a direct devDependency from all workspaces; biome is now managed centrally via qlty. Lint and format scripts updated to useqlty check/qlty fmt. - Dependabot replaced with Renovate — Switched dependency update bot for better monorepo grouping, auto-merge of patch updates, and pinned GitHub Actions digests.
- Socket Firewall switched to free mode — The CI supply chain scan now uses
firewall-free(blocks known malware, no token required) instead offirewall-enterprise. - CI pipeline improvements — Added npm and Docker layer caching, parallelized e2e/load-test jobs, reordered job dependencies for faster feedback, added harden-runner to all workflow jobs.
- CI credential hardening — Bumped
harden-runnerv2.11.1 → v2.14.2 (fixes GHSA-cpmj-h4f6-r6pq) and addedpersist-credentials: falseto allactions/checkoutsteps across all workflows to prevent credential leakage through artifacts. - Zizmor added to local pre-push checks — GitHub Actions security linter now runs via qlty alongside biome, catching workflow misconfigurations before push.
- Lefthook pre-push runs piped — Commands now run sequentially with fail-fast instead of parallel, so failures surface immediately instead of hanging while other commands complete.
1.3.6 — 2026-02-20
Fixed
- GHCR anonymous auth returns 401 on public repos — The v1.3.3 fix for anonymous bearer tokens (
Og==) removed the auth header entirely, but GHCR requires a token exchange even for unauthenticated pulls. Replaced direct bearer auth with proper token exchange viahttps://ghcr.io/token, matching the Hub/Quay pattern. Authenticated requests add Basic credentials to the token request; anonymous requests omit them. LSCR inherits the fix automatically. (#85, #86)
1.3.5 — 2026-02-19
Fixed
- Container exits immediately when socket GID has no named group —
Docker.entrypoint.shtreatedgetent group <gid>failures as fatal underset -e -o pipefail, so mounts where/var/run/docker.sockhad a numeric GID not present in/etc/groupcaused an immediate exit (status=exited,exit=2) before app startup. The group lookup is now tolerant and falls back to creating a matching group as intended. (#82) - Log pretty-printing no longer depends on shell pipes — Moved human-readable formatting from the entrypoint pipeline (
node | pino-pretty) into the app logger configuration. This preserves properexec/signal behavior undertiniwhile keepingDD_LOG_FORMAT=jsonsupport.
1.3.4 — 2026-02-19
Fixed
- Backup lookup broken after container update — Backups were keyed by Docker container ID, which changes on every recreate (e.g. after an update). Switched all backup queries to use the stable container name, so backups are always found regardless of container ID changes. (#79)
- Image prune deletes backup image —
cleanupOldImagesremoved the previous image tag after updates, making rollback impossible. Now checks retained backup tags before pruning and skips images that are needed for rollback. - Auto-rollback monitor uses stale container ID — After an update recreates the container,
maybeStartAutoRollbackMonitorpassed the old (now-deleted) container ID to the health monitor. Now looks up the new container by name and passes the correct ID. - Backup stores internal registry name instead of Docker-pullable name — Backup
imageNamewas stored as the internal registry-prefixed name (e.g.hub.public/library/nginx) which is not a valid Docker image reference. Rollback would fail with DNS lookup errors. Now stores the Docker-pullable base name (e.g.nginx) using the registry'sgetImageFullNamemethod. - Rollback API docs incorrect endpoint — Fixed documentation showing
/api/backup/:id/rollbackinstead of the correct/api/containers/:id/rollback.
1.3.3 — 2026-02-18
Fixed
- Self-update leaves container stopped — When drydock updated its own container, stopping the old container killed the Node process before the new one could be created, leaving the UI stuck on "Restarting..." indefinitely. Now uses a helper container pattern: renames old container, creates new container, then spawns a short-lived helper that curls the Docker socket to stop old → start new → remove old. (#76)
- Stale digest after container updates — After a container was updated (new image pulled, container recreated), the next watch cycle still showed the old digest because the early-return path in
addImageDetailsToContainerskipped re-inspecting the Docker image. Now re-inspects the local image on each watch cycle to refresh digest, image ID, and created date. (#76) - express-rate-limit IPv6 key generation warning — Removed custom
keyGeneratorfrom the container scan rate-limiter that bypassed built-in IPv6 normalization, causingERR_ERL_KEY_GEN_IPV6validation errors. - express-rate-limit X-Forwarded-For warning — Added
validate: { xForwardedForHeader: false }to all 6 rate-limiters to suppress noisyERR_ERL_UNEXPECTED_X_FORWARDED_FORwarnings when running withouttrust proxy(e.g. direct Docker port mapping). - Quay auth token extraction broken — Fixed
authenticate()readingresponse.tokeninstead ofresponse.data.token, causing authenticated pulls to silently run unauthenticated. Also affects Trueforge via inheritance. - GHCR anonymous bearer token — Fixed anonymous configurations sending
Authorization: Bearer Og==(base64 of:) instead of no auth header, which could break public image access. - Created-date-only updates crash trigger execution — Fixed
getNewImageFullName()crashing on.includes()ofundefinedwhen a container had only a created-date change (no tag change). Now rejectsunknownupdate kind in threshold logic. - Compose write failure allows container updates — Fixed
writeComposeFile()swallowing errors, allowingprocessComposeFile()to proceed with container updates even when the file write failed, causing runtime/file state desynchronization. - Self-update fallback removes running old container — Fixed helper script running
removeOldafter the fallback path (startOld), which would delete the running old container. Now only removes old after successful new container start. - Registry calls have no timeout — Added 30-second timeout to all registry API calls via Axios. Previously a hung registry could stall the entire watch cycle indefinitely.
- HTTP trigger providers have no timeout — Added 30-second timeout to all outbound HTTP trigger calls (Http, Apprise, Discord, Teams, Telegram). Previously a slow upstream could block trigger execution indefinitely.
- Kafka producer connection leak — Fixed producer connections never being disconnected after send, leaking TCP connections to the broker over time. Now wraps send in try/finally with disconnect.
- Rollback timer labels not validated — Invalid
dd.rollback.windowordd.rollback.intervallabel values (NaN, negative, zero) could causesetIntervalto fire continuously. Now validates withNumber.isFinite()and falls back to defaults. - Health monitor overlapping async checks — Added in-flight guard to prevent overlapping health checks from triggering duplicate rollback executions when inspections take longer than the poll interval.
- Anonymous login double navigation guard — Fixed
beforeRouteEntercallingnext()twice when anonymous auth was enabled, causing Vue Router errors and nondeterministic redirects. - Container API response not validated — Fixed
getAllContainers()not checkingresponse.okbefore parsing, allowing error payloads to be treated as container arrays and crash computed properties.
Security
- fast-xml-parser DoS via entity expansion — Override
fast-xml-parser5.3.4→5.3.6 to fix CVE GHSA-jmr7-xgp7-cmfj (transitive dep via@aws-sdk/client-ecr, upstream hasn't released a fix yet). - tar arbitrary file read/write — Removed
tarfrom dependency graph entirely by replacing nativere2(which pulled innode-gyp→tar) withre2-wasm(v1.3.3), later replaced byre2js(v1.3.7) due to WASM memory limits. Previously affected by CVE GHSA-83g3-92jg-28cx. - Unauthenticated SSE endpoint — Moved
/api/events/uibehindrequireAuthenticationmiddleware and added per-IP connection limits (max 10) to prevent connection exhaustion. - Session cookie missing sameSite — Set
sameSite: 'strict'on session cookie to mitigate CSRF attacks. - Predictable session secret — Added
DD_SESSION_SECRETenvironment variable override so deployments can provide proper entropy instead of the default deterministic UUIDv5. - Global error handler leaks internal details — Replaced
err.messagewith generic'Internal server error'in the global error handler to prevent leaking hostnames, paths, and Docker socket info to unauthenticated callers. - Entrypoint masks crash exit codes — Enabled
pipefailinDocker.entrypoint.shsonode | pino-prettycorrectly propagates non-zero exit codes for restart policies.
1.3.2 — 2026-02-16
Added
- Log viewer auto-fetch polling — Configurable auto-fetch interval (Off / 2s / 5s / 10s / 30s) for both application and container log viewers, replacing manual-only refresh. Defaults to 5 seconds for a near-real-time tail experience. (#57)
- Log viewer scroll lock — Scrolling away from the bottom pauses auto-scroll, showing a "Scroll locked" indicator and "Resume" button. New log data continues to load in the background without yanking the user's scroll position. (#57)
- Log viewer auto-scroll — New log entries automatically scroll the view to the bottom when the user is near the end, providing a tail-like experience. (#57)
- Shared log viewer composable — Extracted
useLogViewerBehaviorcomposable withuseLogViewport(scroll management) anduseAutoFetchLogs(interval timer lifecycle) to eliminate duplication between application and container log views. - 7 new registry providers — Added OCIR (Oracle Cloud), IBMCR (IBM Cloud), ALICR (Alibaba Cloud), GAR (Google Artifact Registry), Harbor, JFrog Artifactory, and Sonatype Nexus. Includes a shared
SelfHostedBasicbase class for self-hosted registries with basic auth. - 4 new trigger providers — Added Mattermost, Microsoft Teams (Adaptive Cards), Matrix, and Google Chat notification triggers.
Fixed
- v1 manifest digest watch using image ID instead of repo digest — Fixed
handleDigestWatch()incorrectly readingConfig.Image(the local image ID) as the digest for v1 manifest images, causing perpetual false "update available" notifications. Now uses the repo digest fromRepoDigestsinstead. (getwud/wud#934) - Discord trigger broken after request→axios migration — Fixed
sendMessage()usingrequest-style properties (uri,body) instead of axios properties (url,data), causing "Invalid URL" errors on all Discord webhook calls. (getwud/wud#933)
1.3.1 — 2026-02-15
Fixed
- Release SBOM generation for multi-arch images — Replaced
anchore/sbom-action(which fails on manifest list digests from multi-platform builds) with Docker buildx native SBOM generation (sbom: true), producing per-platform SBOMs embedded in image attestations.
Security
- Pin Trivy install script by commit hash — Replaced mutable
mainbranch reference in Dockerfilecurl | shwith a pinned commit SHA to satisfy OpenSSF Scorecard pinned-dependencies check and prevent supply-chain risk from upstream changes.
1.3.0 — 2026-02-15
Fixed
- OIDC session resilience for WUD migrations — Corrupt or incompatible session data (e.g. from WUD's connect-loki store) no longer causes 500 errors. Sessions that fail to reload are automatically regenerated. All OIDC error responses now return JSON instead of plain text, preventing frontend parse errors. Added a global Express error handler to ensure unhandled exceptions return JSON.
- Disabled X-Powered-By header — Removed the default Express
X-Powered-Byheader from both the main API and agent API servers to reduce information exposure. - Trivy scan queue — Serialized concurrent Trivy invocations to prevent
"cache may be in use by another process"errors when multiple containers are scanned simultaneously (batch triggers, on-demand scans, SBOM generation). - Login error on wrong password —
loginBasic()attempted to parse the response body as JSON even on 401 failures, causingUnexpected token 'U', "Unauthorized" is not valid JSONerrors instead of the friendly "Username or password error" message. - Snackbar notification colors ignoring level — The SnackBar component had a hardcoded
color="primary"instead of binding to thelevelprop, causing error and warning notifications to display as blue instead of red/amber. - SBOM format key mismatch — Fixed container model schema validating SBOM formats against
cyclonedxinstead of the correctcyclonedx-jsonkey.
Added
- Snyk vulnerability monitoring — Integrated Snyk for continuous dependency scanning of
app/package.jsonandui/package.json. Added Snyk badge to README withtargetFileparameter for monorepo support. - Update Bouncer (Trivy safe-pull gate) — Added pre-update vulnerability scanning for Docker-triggered updates. Candidate images are scanned before pull/restart, updates are blocked when vulnerabilities match configured blocking severities, and latest scan data is persisted on
container.security.scan. AddedGET /api/containers/:id/vulnerabilitiesendpoint for retrieving scan results. - Update Bouncer signature verification (cosign) — Added optional pre-update image signature verification. When enabled, Docker-triggered updates are blocked if candidate image signatures are missing/invalid or verification fails.
- Update Bouncer SBOM generation — Added Trivy SBOM generation (
spdx-json,cyclonedx-json) for candidate images with persistence incontainer.security.sbomand a newGET /api/containers/:id/sbomAPI endpoint (withformatquery support). - Container card security status chip — Added a vulnerability chip on container cards showing Update Bouncer scan status (
safe,blocked,scan error) with severity summary tooltip data fromcontainer.security.scan. - On-demand security scan — Added
POST /api/containers/:id/scanendpoint for triggering vulnerability scan, signature verification, and SBOM generation on demand. Broadcastsdd:scan-startedanddd:scan-completedSSE events for real-time UI feedback. Added shield button to container card actions and mobile overflow menu. - Direct container update from UI — Added
POST /api/containers/:id/updateendpoint that triggers a Docker update directly without requiring trigger configuration. The "Update now" button in the UI now calls this single endpoint instead of looping through configured triggers. - Trivy and cosign in official image — The official drydock image now includes both
trivyandcosignbinaries, removing the need for custom images in local CLI mode.
Changed
- README badge layout — Added line breaks to badge rows for a cleaner two-line layout across all three badge sections.
- Grafana dashboard overhaul — Updated overview dashboard with standard datasource naming (
DS_PROMETHEUS), added bar chart and pie chart panels, and restructured panel layout for better monitoring coverage. - Mobile responsive dashboard — Stat cards now stack full-width on small screens with tighter vertical spacing for a cleaner mobile layout.
- Self-update overlay rendering — Switched logo images from
v-iftov-showto avoid re-mount flicker during self-update phase transitions. - Container sort simplification — Simplified null-group sorting in ContainersView using sentinel value instead of multi-branch conditionals.
- Test coverage improvements — Expanded app test coverage for API routes (backup, container-actions, preview, webhook), OIDC authentication, registry component resolution, tag parsing, and log sanitization. Expanded UI test coverage across 38 spec files with improved Vuetify stub fidelity (v-tooltip activator slot, v-list-item slots, app-bar-nav-icon events).
- Vitest coverage config — Narrowed coverage to
.js/.tsfiles only (excluding.vueSFCs) to avoid non-actionable template branch noise. - Prometheus counter deduplication — Extracted shared
createCounterfactory inapp/prometheus/counter-factory.ts, reducing boilerplate across audit, webhook, trigger, and container-actions counter modules. - API error handler deduplication — Extracted shared
handleContainerActionErrorhelper inapp/api/helpers.ts, consolidating duplicate catch-block logic across backup, preview, and container-actions routes. - Lint and code quality fixes — Fixed biome
noPrototypeBuiltinswarning in OIDC tests, addedidattributes to README HTML headings to resolve markdownlint MD051, and tuned qlty smell thresholds.
Security
- CodeQL alert fixes — Fixed log injection vulnerabilities by sanitizing user-controlled input before logging. Removed unused variables flagged by static analysis. Added rate limiting to the on-demand scan endpoint.
- Build provenance and SBOM attestations — Added supply chain attestations to release workflow for verifiable build provenance.
1.2.0
Added
- Grafana dashboard template — Importable Grafana JSON dashboard with panels for overview stats, watcher activity, trigger execution, registry response times, and audit entries. Uses datasource templating for portable Prometheus configuration.
- Audit log backend —
AuditEntrymodel, LokiJS-backed store with pagination and pruning,GET /api/auditendpoint with filtering,dd_audit_entries_totalPrometheus counter, and automatic logging of container lifecycle events (update-available, update-applied, update-failed, rollback, preview, container-added, container-removed). - Font Awesome 6 migration — Replaced all Material Design Icons (
mdi-*) with Font Awesome 6 equivalents. Configured Vuetify FA icon set, updated all service icon getters, component templates, and 54 test files. - Dry-run preview API —
POST /api/containers/:id/previewreturns what an update would do (current/new image, update kind, running state, networks) without performing it. - Pre-update image backup and rollback — LokiJS-backed backup store records container image state before each Docker trigger update.
GET /api/backups,GET /api/:id/backups, andPOST /api/:id/rollbackendpoints. Configurable retention viaDD_TRIGGER_DOCKER_{name}_BACKUP_COUNT(default 3). - Frontend wiring — Preview dialog with loading/error/success states wired to dry-run API. Full audit log table with filtering, pagination, and responsive column hiding replacing the MonitoringHistory placeholder. Recent Activity dashboard card showing latest 5 audit entries.
- Container action bar refactor — Replaced 3-column text button layout with compact icon-button toolbar and tooltips (desktop) or overflow menu (mobile).
- Dashboard second row — Added Recent Activity and stats cards as a second row on the dashboard.
- UI modernization — Consistent
pa-4padding, outlined/rounded cards, tonal chips, styled empty states, and Font Awesome icons across all views and components. - Container actions (start/stop/restart) — New API endpoints and UI buttons to start, stop, and restart Docker containers directly from the dashboard. Gated by
DD_SERVER_FEATURE_CONTAINERACTIONS(default: enabled). Includes audit logging, Prometheus counter (dd_container_actions_total), desktop toolbar buttons with disabled-state awareness, and mobile overflow menu integration. - Webhook API for on-demand triggers — Token-authenticated HTTP endpoints (
POST /api/webhook/watch,/watch/:name,/update/:name) for CI/CD integration. Gated byDD_SERVER_WEBHOOK_ENABLEDandDD_SERVER_WEBHOOK_TOKEN. Includes rate limiting (30 req/15min), audit logging, Prometheus counter (dd_webhook_total), and a configuration info panel on the Server settings page. - Container grouping / stack views — New
GET /api/containers/groupsendpoint returns containers grouped by stack. Supports explicit group assignment viadd.group/wud.grouplabels with automatic fallback tocom.docker.compose.project. CollapsibleContainerGroupcomponent with group header showing name, container count, and update badges. "Smart group" filter option for automatic stack detection (dd.group>wud.group> compose project). "Update all in group" action to batch-update all containers in a group. - Graceful self-update UI — Self-update detection when drydock updates its own container. Server-Sent Events (SSE) endpoint at
/api/events/uifor real-time browser push. Full-screen DVD-style bouncing whale logo overlay during self-updates with smooth phase transitions (updating, restarting, reconnecting, ready). Automatic health polling and page reload after restart. - Lifecycle hooks (pre/post-update commands) — Execute shell commands before and after container updates via
dd.hook.preanddd.hook.postlabels. Pre-hook failures abort the update by default (dd.hook.pre.abort=true). Configurable timeout viadd.hook.timeout(default 60s). Environment variables exposed:DD_CONTAINER_NAME,DD_IMAGE_NAME,DD_TAG_OLD,DD_TAG_NEW, etc. Includes audit logging for hook success/failure and UI display in ContainerDetail panel. - Automatic rollback on health check failure — Monitors container health after updates and automatically rolls back to the previous image if the container becomes unhealthy. Configured via
dd.rollback.auto=true,dd.rollback.window(default 300s), anddd.rollback.interval(default 10s). Requires Docker HEALTHCHECK on the container. Uses existing backup store for rollback images. Includes audit logging and UI display in ContainerDetail panel. - selfhst/icons as primary icon CDN — Switched to selfhst/icons as the primary icon CDN with homarr-labs as fallback, improving icon availability and coverage.
Fixed
- Navigation drawer not visible — Used computed model for permanent/temporary modes; passing
model-value=undefinedcaused Vuetify to treat the drawer as closed. - Dark theme missing colors — Added
info,success, andwarningcolor definitions to the dark theme. - ContainerPreview updateKind display — Fixed structured
updateKindobject rendering with semver-diff color coding. - Invalid
text-body-3CSS class — Replaced with validtext-body-2in ConfigurationItem and TriggerDetail. - 404 catch-all route — Added catch-all redirect to home for unknown routes.
- False downgrade suggestion for multi-segment tags — Fixed semver parsing/comparison for numeric tags like
25.04.2.1.1so newer major tags are no longer suggested as downgrades. (#47) - Configured path hardening for filesystem reads — Added validated path resolution helpers and applied them to store paths, watcher TLS files, and MQTT TLS files before filesystem access.
Changed
- Audit event wiring — Wired audit log entries and Prometheus counter increments for rollback, preview, container-added, container-removed, update-applied, and update-failed events. Registered
ContainerUpdateFailedevent with try/catch in Docker trigger. - Test updates — 20+ test files updated for v1.2.0 icon changes, CSS selectors, HomeView data model, theme toggle relocation, and audit module wiring. Removed obsolete specs.
- Updated doc icon examples — Switched icon examples to prefer
hl:andsi:prefixes over deprecatedmdi:. - Code quality tooling consolidation — Replaced Codacy + SonarCloud with Qlty + Snyk. Rewrote
lefthook.ymlpre-push hooks to runqlty check,snyk test,snyk code test(informational), builds, and tests. Addedscripts/snyk-code-gate.shwrapper. - Biome formatting — Applied
biome formatacross entire codebase for consistent code style. - README badges — Replaced Codacy/SonarCloud badges with CI status, Qlty maintainability, and Snyk badges.
- ConfigurationItem redesign — Icon moved to the left with name as prominent text and type as subtitle, replacing the old badge/chip pattern across all configuration pages.
- TriggerDetail redesign — Same modern layout treatment as ConfigurationItem (icon left, name prominent, type subtitle).
- Registry page brand colors — Added brand-colored icon backgrounds for each registry provider (Docker blue, GitHub purple, AWS orange, Google blue, etc.) via
getRegistryProviderColor()helper and newiconColorprop on ConfigurationItem. - Consistent card styling — Unified
variant="outlined" rounded="lg"across ContainerItem, ContainerGroup, ContainerTrigger, and WebhookInfo cards for a cohesive look. - Home page severity badges removed — Removed redundant MAJOR/MINOR severity badges from the container updates list; version chip color already indicates severity.
- History page filter bar — Removed redundant "Update History" heading (already in app bar) and added a collapsible filter bar with active filter chips.
- Logs page spacing — Fixed spacing between the config item and logs card.
- Self-update overlay responsive — Mobile-responsive self-update overlay uses static top-center positioning with fade-in animation on small screens instead of DVD bounce.
- QA compose enhancements — Added HTTP trigger, basic auth, and webhook configuration to
test/qa-compose.ymlfor integration testing. - Login page redesign — Redesigned login page with new font, icon colors, and layout polish.
- Docker Hub and Quay.io multi-registry publishing — Container images now published to Docker Hub and Quay.io alongside GHCR for broader registry availability.
- Mobile responsive dashboard — Per-type colored update badges (major=red, minor=warning, patch=success, digest=info) and icon-only tabs on mobile viewports.
- Dark mode app bar logo inversion — App bar logo now inverts correctly in dark mode for improved visibility.
- History page mobile improvements — Shorter timestamps, hidden status column, and truncated container names on mobile viewports.
- Container filter mobile labels — Short labels ("Updates", "Time") on mobile breakpoint for compact filter display.
- Biome and Qlty config alignment — Aligned Biome and Qlty configurations for consistent code quality enforcement.
Security
- RE2 regex engine — Replaced native
RegExpwith Google's RE2 (re2npm package) for all user-supplied regex patterns (includeTags, excludeTags, transformTags). RE2 uses a linear-time matching algorithm that is inherently immune to ReDoS catastrophic backtracking. - Docs dependency vulnerability fixes — Fixed 9 CVEs in docs/ transitive dependencies via npm overrides (dompurify 2→3, marked 1→4, got 9→11).
Removed
- Dead code removal — Deleted unused
AppFooterandConfigurationStateViewcomponents, dead computed props (filteredUpdates,upToDateCount), duplicateisTriggeringreset, deadmdi:prefix replacement in IconRenderer, deadcontainer-deletedlistener, and Maintenance Windows placeholder. - Removed
@mdi/fontdependency — Dropped unused Material Design Icons package. - Removed Codacy and SonarCloud — Replaced with Qlty (local code quality) and Snyk (dependency + SAST scanning) for a unified local-first quality gate.
- Removed stale tracking docs — Deleted
SONARQUBE-ISSUES.md,docs/sonar-smells-tracking.md, anddocs/codacy-high-findings-tracking.md.
Documentation
- Popular imgset presets — Added a curated preset guide at
docs/configuration/watchers/popular-imgsets.mdand linked it from watcher docs.
1.1.3
Fixed
- ERR_ERL_PERMISSIVE_TRUST_PROXY on startup — Express
trust proxywas hard-coded totrue, which triggers a validation error inexpress-rate-limitv8+ when the default key generator infers client IP fromX-Forwarded-For. Replaced with a configurableDD_SERVER_TRUSTPROXYenv var (default:false). Set to1(hop count) when behind a single reverse proxy, or a specific IP/CIDR for tighter control. (#43)
1.1.2
Fixed
- Misleading docker-compose file error messages — When a compose file had a permission error (EACCES), the log incorrectly reported "does not exist" instead of "permission denied". Now distinguishes between missing files and permission issues with actionable guidance. (#42)
- Agent watcher registration fails on startup — Agent component path resolved outside the runtime root (
../agent/componentsinstead ofagent/components), causing "Unknown watcher provider: 'docker'" errors and preventing agent watchers/triggers from registering. (#42)
Changed
- Debug logging for component registration — Added debug-level logging showing resolved module paths during component registration and agent component registration attempts, making path resolution issues easier to diagnose.
1.1.1 - 2026-02-11
Fixed
- Read-only Docker socket support — Drydock's privilege drop prevented non-root users from connecting to
:rosocket mounts. AddedDD_RUN_AS_ROOT=trueenv var to skip the drop, improved EACCES error messages with actionable guidance, and documented socket proxy as the recommended secure alternative. (#38) - Prometheus container gauge crash with agent containers — The container gauge used a blacklist filter that let unknown properties (like
agent) slip through and crash prom-client. Switched to a whitelist of known label names so unknown properties are silently ignored. (#39) - Snackbar toast transparency — Used
flatvariant for solid background on toast notifications. - Container filter layout broken on narrow viewports — Filter columns rendered text vertically when the nav drawer was open because all 8
v-colelements had no width constraints. Added responsive breakpoints (cols/sm/md) so filters wrap properly across screen sizes. (#40)
1.1.0 - 2026-02-10
Added
- Application log viewer — New Configuration > Logs page with a terminal-style viewer for drydock's own runtime logs (startup, polling, registry checks, trigger events, errors). Backed by an in-memory ring buffer (last 1,000 entries) exposed via
GET /api/log/entries. Supports level filtering (debug/info/warn/error), configurable tail count (50/100/500/1,000), color-coded output, and auto-scroll to newest entries. An info tooltip shows the configured server log level. - Agent log source selector — When agents are configured, a "Source" dropdown appears in the log viewer to switch between the controller's own logs and any connected agent's logs. Disconnected agents are shown but disabled. Agent logs are proxied via
GET /api/agents/:name/log/entries. - Container log viewer — New "Logs" tab in the container detail expansion panel to view container stdout/stderr output directly in the UI with tail control and refresh.
1.0.2 - 2026-02-10
Fixed
- Registry and trigger crashes in agent mode —
getSummaryTags()andgetTriggerCounter()also returnundefinedin agent mode. Added optional chaining to all remaining Prometheus call sites so agent mode doesn't crash when processing containers or firing triggers. (Fixes #33)
1.0.1 - 2026-02-10
Fixed
- Prometheus gauge crash in agent mode —
getWatchContainerGauge()returnsundefinedin agent mode since Prometheus is not initialized. Added optional chaining so the.set()call is safely skipped. This was the root cause of containers not being discovered in agent mode. (Fixes #23, #31)
Changed
- su-exec privilege dropping — Entrypoint detects the docker socket GID and drops from root to the
nodeuser viasu-execwhen possible. Stays root only for GID 0 sockets (Docker Desktop / OrbStack). (Refs #25) - tini init system — Added
tinias PID 1 for proper signal forwarding to the Node process. - Graceful shutdown —
SIGINT/SIGTERMhandlers now callprocess.exit()after cleanup so the container actually stops.
1.0.0 - 2026-02-10
First semver release. Drydock adopts semantic versioning starting with this release, replacing the previous CalVer (YYYY.MM.PATCH) scheme.
Security
- ReDoS prevention — Replaced vulnerable regexes in trigger template evaluation (
Trigger.ts) with linear-time string parsing (parseMethodCall,isValidPropertyPath). AddedMAX_PATTERN_LENGTHguards in tag transform (tag/index.ts) and Docker watcher (Docker.ts) to reject oversized user-supplied regex patterns. - XSS prevention — Added
escapeHtml()sanitizer to Telegram triggerbold()method, preventing HTML injection via container names or tag values. - Workflow hardening — Set top-level
permissions: read-allinrelease.ymlandcodeql.yml. Pinned all CodeQL action refs to commit hashes. Added CodeQL config to excludejs/clear-text-loggingfalse positives. - CVE-2026-24001 — Updated
diffdependency in e2e tests (4.0.2 → 4.0.4).
Changed
- +285 UI tests — 15 new spec files and 7 expanded existing specs covering configuration views, container components, trigger detail, services, router, and app shell. UI test count: 163 → 285.
- +59 app tests — New edge-case tests for ReDoS guard branches,
parseMethodCallparsing, and Docker watcher label resolution. App test count: 1,254 → 1,313. - Complexity refactors — Extracted helpers from high-complexity functions:
parseTriggerList/applyPolicyAction(container.ts),resolveLabelsFromContainer/mergeConfigWithImgset(Docker.ts). - Biome lint fixes —
import typecorrections and unused variable cleanup across 17 files. - Fixed doc links — Corrected broken fragment links in
docs/_coverpage.md.
Removed
- Removed legacy
vue.config.js— Dead Vue CLI config file; project uses Vite.
2026.2.3 - 2026-02-10
Fixed
- NTFY trigger auth 401 — Bearer token auth used unsupported
axios.auth.bearerproperty; now sendsAuthorization: Bearer <token>header. Basic auth property names corrected tousername/password. (#27) - Agent mode missing /health — Added unauthenticated
/healthendpoint to the agent server, mounted before the auth middleware so Docker healthchecks work without the agent secret. (#27)
Changed
- Lefthook pre-push hooks — Added
lefthook.ymlwith pre-push checks (lint + build + test). - Removed startup warning — Removed "Known Issue" notice from README now that container startup issues are resolved.
2026.2.2 - 2026-02-10
Security
- Cosign keyless signing — Container image releases are now signed with Sigstore cosign keyless signing for supply chain integrity.
- Least-privilege workflow permissions — Replaced overly broad
read-allwith minimum specific permissions across all CI/CD workflows. - CodeQL and Scorecard fixes — Resolved all high-severity CodeQL and OpenSSF Scorecard security alerts.
- Pinned CI actions — All CI action references pinned to commit hashes with Dockerfile base image digest.
Added
- Auto-dismiss notifications after container update — New
resolvenotificationsoption for triggers (default:false). When enabled, notification triggers automatically delete the sent message after the Docker trigger successfully updates the container. Implemented for Gotify via itsdeleteMessageAPI. Other providers (Slack, Discord, ntfy) can add support by overriding the newdismiss()method on the base Trigger class. NewcontainerUpdateAppliedevent emitted by the Docker trigger on successful update.
Fixed
- Agent mode Prometheus crash — Guard
getWatchContainerGauge().set()against undefined in Agent mode where Prometheus is not initialized, fixing "Cannot read properties of undefined (reading 'set')" crash (#23) - Sanitize version logging — Sanitize version strings from env vars before logging to resolve CodeQL clear-text-logging alerts in
index.tsandstore/migrate.ts - Broken event test assertion — Fix
expect()without matcher in event test
Changed
- 97% test coverage — Boosted from 76% to 97% with 449 new tests (1,254 total across 95 test files).
- Fuzz testing — Added property-based fuzz tests with fast-check for Docker image name parsing.
- Static analysis fixes — Optional chaining,
String#replaceAll(),readonlymodifiers,Number.NaN, concise regex syntax, removed unused imports, moved functions to outer scope. - Reduced code duplication — Refactored duplicated code in registries, triggers, and store test files flagged by SonarCloud.
- Pino logging — Replaced bunyan with pino to eliminate vulnerable transitive dependencies. Added pino-pretty for human-readable log output.
- Renamed wud to drydock — Project references updated from upstream naming across Dockerfile, entrypoint, package files, scripts, and test fixtures.
- CONTRIBUTING.md — Added contributor guidelines.
- OpenSSF Best Practices badge — Added to README.
- SonarCloud integration — Added project configuration.
- Multi-arch container images — Docker images now built for both
linux/amd64andlinux/arm64architectures, published to GHCR. - Lefthook pre-push hooks — Added lefthook config with pre-push checks (lint + build + test) and
npm run checkconvenience script. - CodeQL query exclusion — Exclude
js/clear-text-loggingquery (false positives on DD_VERSION env var).
2026.1.0
Added
- Agent mode — Distributed monitoring with remote agent architecture. Agent components, SSE-based communication, dedicated API routes.
- OIDC token lifecycle — Remote watcher HTTPS auth with
Basic+Bearertoken support. TLS/mTLS compatibility forDD_WATCHER_{name}_HOST. - OIDC device-flow (Phase 2) — RFC 8628 Device Authorization Grant for headless remote watcher auth. Auto-detection, polling with backoff, and refresh token rotation.
- Per-image config presets —
imgsetdefaults for per-image configuration. AddedwatchDigestandinspectTagPathimgset properties. - Hybrid triggers — Trigger group defaults (
DD_TRIGGER_{name}_THRESHOLD) shared across providers. Name-only include/exclude for multi-provider trigger management. - Container update policy — Skip/snooze specific update versions. Per-container policy stored in DB, exposed via API and UI.
- Metrics auth toggle —
DD_SERVER_METRICS_AUTHenv var to disable auth on/metricsendpoint. - Trigger thresholds — Digest and no-digest thresholds for triggers.
- NTFY provider-level threshold — Provider-level threshold support for ntfy trigger.
- Docker pull progress logging — Rate-limited pull progress output during docker-compose updates.
- Registry lookup image override —
lookupImagefield on registry config to override the image used for tag lookups. - Docker inspect tag path — Support custom tag path in Docker inspect output.
- Anonymous LSCR and TrueForge registries — Allow anonymous access to LSCR (LinuxServer) and Quay-backed TrueForge.
- DHI registry — New
dhi.ioregistry provider with matcher, auth flow, and docs. - Custom URL icons — Support URL-based icons via
dd.display.iconlabel. - Version skip — Skip specific versions in the UI.
- Log viewer — In-app container log viewer. View Docker container stdout/stderr output directly in the UI via a new "Logs" tab on each container. Supports configurable tail line count (50/100/500), manual refresh, and Docker stream demultiplexing. Works for both local and remote agent containers.
- Semver tag recovery — Recover include-filter mismatched semver tags from watchers. Extended to advise best semver tag when current tag is non-semver (e.g.,
latest). - Dashboard update chips — Replaced verbose update status text with compact colored chips: green "up to date" or warning "N update(s)" (clickable).
Fixed
- eval() code injection — Replaced
eval()in trigger template rendering with safe expression evaluator supporting property paths, method allowlist, ternaries, and string concatenation. - Digest-only update prune crash — Docker trigger prune logic now correctly excludes current image during digest-only updates and handles post-prune errors gracefully.
- Swarm deploy-label debug logging — Added warn-level logging when Swarm service inspect fails, and debug logging showing which label sources contain
dd.*labels. - OIDC session state races — Serialized redirect session checks, multiple pending callback states per session.
- semverDiff undefined — Normalized
semverDifffor non-tag (digest-only/created-date-only) updates. - Docker event stream crash — Buffered and parsed split Docker event stream payloads.
- Multi-network container recreate — Reconnects additional networks after container recreation.
- Remote watcher delayed first scan —
watchatstartnow checks watcher-local store for new remote watchers. - docker-compose post_start hooks — Hooks now execute after updates.
- docker-compose image-only triggers — Only trigger on compose services with actual image changes.
- docker-compose imageless services — Skip compose services without an
imagefield. - docker-compose implicit latest tag — Normalize
image: nginxtoimage: nginx:latestso compose triggers don't treat implicit latest as a version mismatch. - Express 5 wildcard routes — Named wildcard route params for express 5 compatibility.
- Semver filtering — Fixed semver part filtering and prefix handling.
- SMTP TLS_VERIFY inverted —
rejectUnauthorizedwas inverted;TLS_VERIFY=falsenow correctly allows self-signed certificates. - HA MQTT deprecated object_id — Replaced
object_idwithdefault_entity_idfor Home Assistant 2025.10+ compatibility. - Open redirect on authenticated pages — Validate
nextquery parameter to only allow internal routes. - Trigger test updateKind crash — Test-button triggers no longer crash with "Cannot read properties of undefined (reading 'updateKind')" on unvalidated containers.
- Docker rename event not captured — Added
renameto Docker event listener so container name updates are captured after compose recreates. - UI duplicate drawer logo — Removed duplicate logo in navigation drawer.
Changed
- TypeScript migration (app) — Entire backend converted from JavaScript to TypeScript with ES Modules (
NodeNext). 232.tsfiles added/renamed, all.jssource files removed. - TypeScript migration (UI) — Vue 3 frontend migrated from JS to TS. 29
.vuefiles updated, component props/emits typed. - Jest → Vitest (app) — All 64 app test files (664 tests) migrated from Jest to Vitest. Test runner unified across app and UI.
- Jest → Vitest (UI) — UI unit tests migrated from Jest to Vitest with improved coverage.
- Vitest 4 + modern deps — Upgraded vitest 3→4, uuid 11→13, flat 5→6, snake-case 3→4. Fixed vitest 4 mock constructor breaking change.
- ESM baseline — Cut over to
NodeNextmodule resolution. Removed Babel, addedtsconfig.json. - Biome linter — Replaced ESLint with Biome for formatting and linting.
- CI cleanup — Removed Code Climate config, renamed Travis config to
ci.config.yml.
Dependencies
| Package | Upstream (8.1.1) | drydock |
|---|---|---|
| vitest | 3.x (Jest) | 4.x |
| uuid | 9.x | 13.x |
| flat | 5.x | 6.x |
| snake-case | 3.x | 4.x |
| express | 4.x | 5.x |
| typescript | — | 5.9 |
| biome | — | 2.3 |
Stats: 392 files changed, +25,725 insertions, -25,995 deletions, 872 total tests (709 app + 163 UI).
Upstream Backports
The following changes from upstream/main (post-fork) have been ported to drydock:
| Description | Status |
|---|---|
| Add Codeberg to default registries | Ported (new TS provider) |
Increase maxAliasCount in YAML parsing | Ported |
Fix authentication for private ECR registry (async getAuthPull) | Ported across all registries |
Prometheus: add DD_PROMETHEUS_ENABLED config | Ported |
| Fix Authelia OIDC docs (field names) | Ported |
| Buffer Docker event stream before JSON parse | Already fixed independently |
| SMTP trigger: allow display name in from address (#908) | Ported |
Remaining upstream-only changes (not ported — not applicable to drydock):
| Description | Reason |
|---|---|
| Fix e2e tests (x2) | JS-based, drydock tests are TS |
| Fix prettier | drydock uses Biome |
| Fix codeberg tests | Covered by drydock's own tests |
| Update changelog | Upstream-specific |