Skip to content

Releases: CodesWhat/drydock

v1.5.1-rc.4

v1.5.1-rc.4 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 29 Jun 20:01
b7cfa6e

v1.5.1-rc.4

Full Changelog: v1.5.1-rc.3...v1.5.1-rc.4

[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 with DD_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/myapp vs /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.version OCI label, falling back to the running container's inspect metadata — as image.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 key version, preserved so saved column preferences keep working) continues to show the image tag; the new Version column shows image.softwareVersion, falling back to the tag when no software version is available. dd.inspect.tag.path now dual-writes the extracted value into image.softwareVersion as 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-only opt-in label. When dd.inspect.tag.path is set, the extracted value normally overwrites the image tag (enabling update detection against the semver embedded in the running container). Setting dd.inspect.tag.version-only=true routes the extracted value to image.softwareVersion only, 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.StartedAt timestamp), 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.json written 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 new details.startedAt and image.softwareVersion fields from dd.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-compose yes/no preview 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 English in <group> — it now interpolates the group name through a translatable key), the security-view severity tooltips (CRITICAL/HIGH/MEDIUM/LOW), the backup operation unknown fallback 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-alpine base from a stale digest (Node 24.16.0, Alpine 3.21) to the current digest (Node 24.18.0, Alpine 3.24) and added libexpat to the targeted apk upgrade set. This resolves all 11 Node binary CVEs reported by the image scan — including the one critical (CVE-2026-48930) and four high — plus 13 medium libexpat CVEs (now 2.8.2-r0). A rebuild + rescan confirms zero critical/high/Node/libexpat findings remain. The three busybox/ssl_client findings (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.

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.

  1. OIDC login now requires authorization_endpoint in 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-configuration does not advertise authorization_endpoint, OIDC sign-in will now fail closed — make sure the discovery document exposes it.
  2. 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 of DD_SERVER_TRUSTPROXY. Internet-facing or multi-user instances may begin to see unexpected 429 Too Many Requests on unauthenticated endpoints. Authenticated requests are keyed per session and are unaffected.
  3. HTTP-trigger proxy URLs must now use the http:// or https:// 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 an http(s):// proxy URL.

v1.5.1-rc.3

v1.5.1-rc.3 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 28 Jun 20:01
1819478

v1.5.1-rc.3

Full Changelog: v1.5.1-rc.2...v1.5.1-rc.3

[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 with DD_RELEASE_NOTES_MAX_INTERMEDIATE (default 20; set to 0 to disable). When the range exceeds the cap, the popover shows a non-silent "N older releases not shown" notice. Supports the __FILE secret-file convention (DD_RELEASE_NOTES_MAX_INTERMEDIATE__FILE). (#453)

  • New GET /api/containers/{id}/intermediate-release-notes endpoint. 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. Accepts from (required) and to (defaults to the container's pending update tag) query parameters. (#453)

  • Warn log when a dd.source.repo container label shadows a trusted OCI image source label. Adding a dd.source.repo label to a running container when the image already carries a trusted org.opencontainers.image.source (or org.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 a warn-level line each watch cycle when it detects this conflict, naming both repos. (#452)

Changed

  • DD_RELEASE_NOTES_GITHUB_TOKEN is now forwarded to release-notes lookups for repos resolved from a dd.source.repo container label or a persisted container.sourceRepo value. 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 with public_repo scope 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.json files 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.

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.

  1. OIDC login now requires authorization_endpoint in 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-configuration does not advertise authorization_endpoint, OIDC sign-in will now fail closed — make sure the discovery document exposes it.
  2. 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 of DD_SERVER_TRUSTPROXY. Internet-facing or multi-user instances may begin to see unexpected 429 Too Many Requests on unauthenticated endpoints. Authenticated requests are keyed per session and are unaffected.
  3. HTTP-trigger proxy URLs must now use the http:// or https:// 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 an http(s):// proxy URL.

v1.5.1-rc.2

v1.5.1-rc.2 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 28 Jun 05:59
c85643e

v1.5.1-rc.2

Full Changelog: v1.5.1-rc.1...v1.5.1-rc.2

[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, and currentReleaseNotes. (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.tsv and the JSON-to-SARIF converter handles the suppression so the security workflow stays green without masking real findings. (#374)

Warning

Upgrade notes — behavioral changes, please read before updating. Releases 1.4.6 and the entire 1.5 line ship security-hardening fixes that change runtime behavior. These are not deprecations: there is no compatibility shim or grace period, so a previously-working deployment can change behavior on upgrade.

  1. OIDC login now requires authorization_endpoint in 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-configuration does not advertise authorization_endpoint, OIDC sign-in will now fail closed — make sure the discovery document exposes it.
  2. 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 of DD_SERVER_TRUSTPROXY. Internet-facing or multi-user instances may begin to see unexpected 429 Too Many Requests on unauthenticated endpoints. Authenticated requests are keyed per session and are unaffected.
  3. HTTP-trigger proxy URLs must now use the http:// or https:// 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 an http(s):// proxy URL.

v1.5.1-rc.1

v1.5.1-rc.1 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 27 Jun 03:32
47dd10f

v1.5.1-rc.1

Full Changelog: v1.5.0...v1.5.1-rc.1

[1.5.1-rc.1] — 2026-06-26

Added

  • Remote agents now report their log level and watcher schedule. The agent handshake (dd:ack) includes logLevel and pollInterval (the watcher's cron), so the GET /api/v1/agents response 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 than maturityMinAgeDays clears 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 stamps updateDetectedAt returned undefined immediately whenever updateAvailable was false, 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, resetting updateDetectedAt to 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 _, so my_prod + nginx and my + prod_nginx produced 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 maturityMinAgeDays rather 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, which docker login rejects, so any action trigger targeting a private gcr.io or *-docker.pkg.dev image failed to authenticate and could not apply the update. It now returns the _json_key username 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 Link headers. The next_page cursor was matched with a greedy pattern that swallowed the trailing >; rel="next" from an RFC 5988 Link header and corrupted the cursor, so repositories with more than one page of tags could silently stop paginating. The parser now reads next_page and last cursors 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 _CLIENTCERT were 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 an insecure setting 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 the command action already applies.

  • DD_SESSION_SECRET__FILE is now honored. The session secret was read straight from process.env, bypassing the __FILE secret-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 via DD_SESSION_SECRET__FILE is 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_*__FILE secret is readable by group or others, drydock logs a non-fatal warning recommending chmod 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- or echo-added \n can't corrupt a credential, matching the common Docker *_FILE convention.

  • Debug dump and container environment no longer leak credentials. The debug dump's redaction missed SMTP passwords (*_PASS keys) 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 *_PASS gap 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 maturityMinAgeDays will clear the maturity gate immediately on the first poll after upgrading. Notification triggers in always mode 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.

Warning

Upgrade notes — behavioral changes, please read before updating. Releases 1.4.6 and the entire 1.5 line ship security-hardening fixes that change runtime behavior. These are not deprecations: there is no compatibility shim or grace period, so a previously-working deployment can change behavior on upgrade.

  1. OIDC login now requires authorization_endpoint in 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-configuration does not advertise authorization_endpoint, OIDC sign-in will now fail closed — make sure the discovery document exposes it.
  2. 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 of DD_SERVER_TRUSTPROXY. Internet-facing or multi-user instances may begin to see unexpected 429 Too Many Requests on unauthenticated endpoints. Authenticated requests are keyed per session and are unaffected.
  3. HTTP-trigger proxy URLs must now use the http:// or https:// 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 an http(s):// proxy URL.

v1.5.0

Choose a tag to compare

@github-actions github-actions released this 23 Jun 01:35
7f39525

Drydock v1.5.0

The reliability release — a durable update queue, a notification outbox with dead-letter recovery, live container-log streaming, full 17-locale i18n, and a top-to-bottom security-hardening sweep.

Docker
Docs
Signed with cosign


Full Changelog: v1.4.6...v1.5.0

[1.5.0] — 2026-06-22

Added

  • Trigger taxonomy split — DD_ACTION_* and DD_NOTIFICATION_* prefixes. Action triggers (Docker, Docker Compose, Command) now use DD_ACTION_* / dd.action.*; messaging triggers (Slack, SMTP, Discord, Telegram, ntfy, Pushover, and all others) use DD_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 legacy DD_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 with DD_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/outbox REST 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 at GET /api/v1/debug/dump.

  • SSE Last-Event-ID replay. Every broadcast event carries a monotonic <bootId>:<counter> id; clients reconnecting with Last-Event-ID receive missed events from a 5-minute ring buffer. Clients that fall behind the buffer receive a dd:resync-required event.

  • 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_COLOR is set.

Changed

  • Action trigger default mode changed to AUTO=oninclude. Action triggers no longer auto-update every container by default; an explicit dd.action.include label 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, .snyk policy 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_SECRET auto-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 specific precision now track digest changes only. Opt in to semver climbing with dd.tag.include or dd.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 after dd: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}/status endpoint 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-Proto when DD_SERVER_TRUSTPROXY is 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. callRegistry implements 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 via DD_NOTIFICATION_HTTP_{name}_ALLOWMETADATA=true.

  • CSRF same-origin enforcement hardened. Forwarded-host headers are now trusted only when Express trust proxy is enabled; /auth mutations (logout, remember) are now covered by the same-origin check; and DD_SERVER_TRUSTPROXY=true (all hops) now emits a startup warning recommending a hop count.

  • Security digest templates no longer evaluated as JavaScript. SECURITYDIGESTTITLE/SECURITYDIGESTBODY previously passed through new 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: 0 to prevent credential exfiltration if a token endpoint returns a redirect. Applied to BaseRegistry and all providers that build their own credentialed token-fetch requests.

  • Container image CVE surface cleared. Bumped node:24-alpine base (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 sco...

Read more

v1.5.0-rc.38

v1.5.0-rc.38 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 20 Jun 02:05
daf75cf

v1.5.0-rc.38

Full Changelog: v1.5.0-rc.37...v1.5.0-rc.38

[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 by scripts/gen-banner.mjs, so startup decodes no image. The banner is written to stderr and suppressed automatically when stdout/stderr is not a TTY or NO_COLOR is 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.json in 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 new security-grype.yml runs 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 .snyk policy file, the security-snyk-weekly.yml workflow, the setup-snyk composite action, and the scripts/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) via scripts/regenerate-brand-assets.sh. Filenames are unchanged, so the Home Assistant entity_picture URL contract is preserved.

Security

  • Documentation site (apps/web) js-yaml pinned to 4.2.0 (GHSA-h67p-54hq-rp68). fumadocs-mdx pulled 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/core pinned to 2.8.0 (CVE-2026-54285). artillery pulled @opentelemetry/core 2.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-image scan on main flagged a pre-existing CVE backlog that nothing had been scanning (Snyk Container never ran — no token was configured). Bumped the node:24-alpine base (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) and cosign 2.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 bundled cosign and trivy CLI binaries (drydock shells out to them for signature verification and container scanning) — those clear only when Alpine rebuilds the packages, so a documented .grype.yaml scopes 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 the verify --output json/--certificate-identity/--certificate-oidc-issuer/--key flags drydock's signature path uses.

  • Patched a batch of newly-disclosed undici CVEs 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 on 8.3.0, vulnerable to all eight — bumped to 8.5.0, the only release clearing the full set (CVE-2026-9675 is fixed solely in 8.5.0), and pinned in overrides as well. The dashboard build (ui) and the e2e load-test harness pulled undici 7.25.0/7.26.0 transitively; an overrides entry forces 7.28.0 (the patched 7.x line) in each — build- and test-only, not part of the shipped image.

  • Patched nodemailer to 9.0.1 (GHSA-p6gq-j5cr-w38f, CVSS 7.1). A message-level raw option bypassed nodemailer's disableFileAccess/disableUrlAccess guards, allowing arbitrary file read and full-response SSRF in the delivered message. drydock's SMTP trigger only calls createTransport/sendMail with plain from/to/subject/text fields and never passes raw, 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 in app is bumped from 8.0.10. The 8→9 major jump doesn't touch the stable createTransport/sendMail core drydock relies on.

Warning

Upgrade notes — behavioral changes, please read before updating. Releases 1.4.6 and the entire 1.5 line ship security-hardening fixes that change runtime behavior. These are not deprecations: there is no compatibility shim or grace period, so a previously-working deployment can change behavior on upgrade.

  1. OIDC login now requires authorization_endpoint in 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-configuration does not advertise authorization_endpoint, OIDC sign-in will now fail closed — make sure the discovery document exposes it.
  2. 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 of DD_SERVER_TRUSTPROXY. Internet-facing or multi-user instances may begin to see unexpected 429 Too Many Requests on unauthenticated endpoints. Authenticated requests are keyed per session and are unaffected.
  3. HTTP-trigger proxy URLs must now use the http:// or https:// 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 an http(s):// proxy URL.

v1.5.0-rc.37

v1.5.0-rc.37 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 15 Jun 20:43
25b3df8

v1.5.0-rc.37

Full Changelog: v1.5.0-rc.36...v1.5.0-rc.37

[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), and ws (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 the safeLoad() 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.

Warning

Upgrade notes — behavioral changes, please read before updating. Releases 1.4.6 and the entire 1.5 line ship security-hardening fixes that change runtime behavior. These are not deprecations: there is no compatibility shim or grace period, so a previously-working deployment can change behavior on upgrade.

  1. OIDC login now requires authorization_endpoint in 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-configuration does not advertise authorization_endpoint, OIDC sign-in will now fail closed — make sure the discovery document exposes it.
  2. 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 of DD_SERVER_TRUSTPROXY. Internet-facing or multi-user instances may begin to see unexpected 429 Too Many Requests on unauthenticated endpoints. Authenticated requests are keyed per session and are unaffected.
  3. HTTP-trigger proxy URLs must now use the http:// or https:// 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 an http(s):// proxy URL.

v1.5.0-rc.36

v1.5.0-rc.36 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 15 Jun 15:51
423b6b0

v1.5.0-rc.36

Full Changelog: v1.4.6...v1.5.0-rc.36

[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: set DD_EXPERIMENTAL_PORTWING=true to enable it. When disabled, the endpoint is not mounted and the feature has zero runtime footprint. Once enabled, agents connect to WS /api/v1/portwing/ws using the portwing/1.0 subprotocol. 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 logLevel and pollInterval in the acknowledgement payload (PR #430, M4). Drydock threads these fields through buildRuntimeInfoFromAck and 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 as specific precision are now treated as digest-only by default — getTagCandidates returns an empty tag list so updates track digest changes only, not semver version bumps. Opt in to semver climbing by setting dd.tag.include (restricts climbing to matching tags) or dd.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 watchFromCron respected the window, but maybeFastResyncAfterUpdate (the post-update fast resync) called watchContainer unconditionally — 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 by watchFromCron. Additionally, computeUpdateEligibility gains a maintenanceWindowOpen context field: when false, a soft maintenance-window-closed blocker is recorded in the eligibility result. Manual UI/API-triggered updates pass undefined and 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/user response 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 transient drydock-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 unauthenticated GET /api/v1/self-update/{operationId}/status endpoint 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, or expired); and the helper container is labeled dd.watch=false so 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.

Warning

Upgrade notes — behavioral changes, please read before updating. Releases 1.4.6 and the entire 1.5 line ship security-hardening fixes that change runtime behavior. These are not deprecations: there is no compatibility shim or grace period, so a previously-working deployment can change behavior on upgrade.

  1. OIDC login now requires authorization_endpoint in 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-configuration does not advertise authorization_endpoint, OIDC sign-in will now fail closed — make sure the discovery document exposes it.
  2. 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 of DD_SERVER_TRUSTPROXY. Internet-facing or multi-user instances may begin to see unexpected 429 Too Many Requests on unauthenticated endpoints. Authenticated requests are keyed per session and are unaffected.
  3. HTTP-trigger proxy URLs must now use the http:// or https:// 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 an http(s):// proxy URL.

v1.4.6

Choose a tag to compare

@github-actions github-actions released this 12 Jun 17:08

Warning

Upgrade notes — behavioral changes, please read before updating. These are security hardening fixes, not deprecations: there is no compatibility shim or grace period, so a previously-working deployment can change behavior on upgrade.

  1. OIDC login now requires authorization_endpoint in 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-configuration does not advertise authorization_endpoint, OIDC sign-in will now fail closed — make sure the discovery document exposes it.
  2. 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 of DD_SERVER_TRUSTPROXY. Internet-facing or multi-user instances may begin to see unexpected 429 Too Many Requests on unauthenticated endpoints. Authenticated requests are keyed per session and are unaffected.
  3. HTTP-trigger proxy URLs must now use the http:// or https:// 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 an http(s):// proxy URL.

[1.4.6] — 2026-06-12

Security maintenance release for the 1.4.x line.

Security

  • Dependency security updates — Cleared all known advisories in the shipped backend (app) and dashboard (ui) dependencies, including a critical arbitrary-code-execution issue in protobufjs and high-severity SSRF / prototype-pollution / credential-leak issues in axios, plus @grpc/grpc-js, fast-uri, fast-xml-parser, path-to-regexp, qs, and others. Both workspaces now report zero npm audit vulnerabilities. No major dependency upgrades were needed — the Docker watcher stays on dockerode 4.x (the flagged transitive uuid advisory affects v3/v5/v6 with a buf argument and is not reachable through dockerode's v4() usage; it is pinned to a fixed release via an override).
  • OIDC authorization-redirect hardening — The OIDC provider now requires a strict authorization-endpoint match and no longer falls back to a broad same-origin allowlist when discovery metadata lacks an authorization_endpoint. This prevents an attacker who controls a different path under a shared-origin identity provider from steering the authorization redirect to an attacker-controlled endpoint.
  • HTTP trigger SSRF guard — Proxy URLs configured for the HTTP trigger are now restricted to http/https schemes, validated both at configuration time (schema) and at runtime, failing closed on any other scheme.
  • Rate-limit key spoofing fix — Unauthenticated rate-limit keys are now derived from the TCP peer address (socket.remoteAddress) instead of request.ip, so a client behind a trusted proxy cannot spoof X-Forwarded-For to evade per-IP rate limits.

v1.5.0-rc.35

v1.5.0-rc.35 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 10 Jun 20:20
e657dda

v1.5.0-rc.35

Full Changelog: v1.5.0-rc.34...v1.5.0-rc.35

[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_TRUSTPROXY set. WebSocket upgrades bypass Express, so the log-stream origin check never honored trust-proxy. It now compares the browser origin against X-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 agent no 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 (replacementExpected removals are skipped, and the new container's ID rides the dd:update-applied payload as newContainerId), 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 returns undefined without 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) via process.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 new DD_ACTION_COMMAND_{name}_ENV option (comma-separated).

  • Hook commands can be restricted to an allowlist of binaries (DD_HOOKS_ALLOWED_COMMANDS). With hooks enabled, dd.hook.pre/dd.hook.post labels 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/16 including 169.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 via DD_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 structuredClone of 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 BaseRegistry is compiled once at module load. An unused updatedAt collection 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.