v1.5.1
Full Changelog: v1.5.1-rc.6...v1.5.1
[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
-
Intermittent Trivy scan timeout can fail an update on large images. A security scan that exceeds the internal execution budget reports an
unknownresult, and the error path prunes the pulled image so the next attempt re-pulls it. Workaround: raiseDD_SECURITY_TRIVY_TIMEOUT(e.g.600000) or retry the update. Long-standing since v1.3.0; the fix ships in v1.6. (#490) -
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)
Warning
Upgrade notes: behavioral changes, please read before updating. Three security-hardening fixes that change runtime behavior first shipped in 1.4.6 and carry through the entire 1.5 line. Anyone updating from a release older than 1.4.6 is affected, whatever version you land on (1.4.6, any 1.5.x, or later), because these changes sit across the 1.4.6 boundary rather than in one specific version. These are not deprecations: there is no compatibility shim or grace period, so a previously-working deployment can change behavior on upgrade.
- OIDC login now requires
authorization_endpointin your provider's discovery metadata. The authorization-redirect allowlist no longer falls back to a broad same-origin match. Mainstream identity providers (Keycloak, Authentik, Authelia, Okta, Google, Entra/Azure AD, Zitadel, …) publish this field and are unaffected. If your/.well-known/openid-configurationdoes not advertiseauthorization_endpoint, OIDC sign-in will now fail closed — make sure the discovery document exposes it. - Unauthenticated rate-limit buckets now key on the TCP peer address instead of
X-Forwarded-For. Behind a reverse proxy (nginx / Traefik / Caddy), all unauthenticated clients now share a single bucket (the proxy's address), regardless ofDD_SERVER_TRUSTPROXY. Internet-facing or multi-user instances may begin to see unexpected429 Too Many Requestson unauthenticated endpoints. Authenticated requests are keyed per session and are unaffected. - HTTP-trigger
proxyURLs must now use thehttp://orhttps://scheme. Any other scheme (e.g.socks5://) is rejected at config load. Such values were previously accepted but only ever treated as an HTTP proxy — switch to anhttp(s)://proxy URL.