Releases: CodesWhat/drydock
v1.5.0-rc.32
v1.5.0-rc.32
Full Changelog: v1.5.0-rc.31...v1.5.0-rc.32
[1.5.0-rc.32] — 2026-06-06
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 and watcher-level sensor topics — 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 each locally-monitored container 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 (commit [`cf77728...
v1.5.0-rc.31
v1.5.0-rc.31
Full Changelog: v1.5.0-rc.30...v1.5.0-rc.31
[1.5.0-rc.31] — 2026-06-05
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 and watcher-level sensor topics — 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 each locally-monitored container 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 (commit [`cf77728...
v1.5.0-rc.30
v1.5.0-rc.30
Full Changelog: v1.5.0-rc.29...v1.5.0-rc.30
[1.5.0-rc.30] — 2026-06-05
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 and watcher-level sensor topics — 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 each locally-monitored container 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 (commit [`cf77728...
v1.5.0-rc.29
v1.5.0-rc.29
Full Changelog: v1.5.0-rc.28...v1.5.0-rc.29
[1.5.0-rc.29] — 2026-05-31
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. -
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.
-
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 each locally-monitored container 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. -
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 a `...
v1.5.0-rc.28
v1.5.0-rc.28
Full Changelog: v1.5.0-rc.27...v1.5.0-rc.28
[1.5.0-rc.28] — 2026-05-28
Fixed
- #386 follow-through — 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. A user running drydock in a controller + agent topology reported on rc.27 that their
mlagent still rendered 0 running containers in the controller UI even after the rc.25 watcher-snapshot suppression (d02080ae) and the rc.26 stats-changed broadcast (512c3751). Cause: whenAgentClient._doHandshake(app/agent/AgentClient.ts) reconnects to an agent's SSE stream it handshakes viaGET /api/containers, which serves from the agent's in-memorystoreContainer. If the agent process has just restarted and itswatchatstartcron has not yet finished its first run (the agent's store is non-persistent across restarts), that endpoint legitimately returns[]even though the docker daemon has N running containers._doHandshakethen calledpruneOldContainers([])unconditionally, deleting every controller-side container the agent had previously contributed — even though the agent's firstdd:watcher-snapshotwas about to repopulate the store seconds later. The rc.25 fix inDocker.watch()only suppresses outgoing snapshots from the agent when enumeration fails on the agent itself; it does not cover the controller-side handshake path. The fix makes the handshake's prune step ambiguity-aware:_doHandshakenow skipspruneOldContainerswhenevercontainers.length === 0and emits aHandshake returned 0 containers; preserving last-known state until the first watch cycle completeswarning (only afterhasConnectedOnce, so the first-ever connection of a genuinely empty agent stays silent). Pruning is deferred to the next authoritativedd:watcher-snapshot, which is already gated on!containerEnumerationFailed && enrichmentErrors === 0(app/watchers/providers/docker/Docker.ts:1136) and is therefore unambiguous: a 0-container snapshot means the agent really has 0 running containers. Non-zero handshakes continue to prune normally — the behaviour change is scoped strictly to the 0-container case that exposed the cold-start race.
v1.5.0-rc.27
v1.5.0-rc.27
Full Changelog: v1.5.0-rc.26...v1.5.0-rc.27
[1.5.0-rc.27] — 2026-05-24
Fixed
-
#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" Pushover/Telegram notification long after the update actually succeeded. A user running drydock in a controller + agent topology reported on rc.25 that an "Update All" of Tautulli on two hosts produced the success notification only for the controller-host container; the agent-host container's success notification was missing and, ~30 minutes later, a second Pushover arrived saying
[mediavault] Container Tautulli update failed — Marked failed after exceeding active update TTL (1800000ms) while queued.even though the update had in fact succeeded on the agent. Cause: when the controller queues a container update viacreateAcceptedContainerUpdateRequest(app/updates/request-update.ts) it mints a controller-sideoperationIdand inserts aqueuedrow; the dispatcher then callsentry.trigger.trigger(entry.container, { operationId }). For containers hosted on an agent the trigger isAgentTrigger, whosetrigger(container)previously accepted only the container and discarded theruntimeContext.AgentClient.runRemoteTriggerposted{id, name}to the agent without the operationId, so the agent's/api/triggers/:type/:nameendpoint calledrequestContainerUpdatewith no operationId and minted its own row; the agent'sdd:update-applied/dd:update-operation-changedevents then arrived back at the controller carrying the agent-side id, which the controller routed throughtoAgentScopedIdinto a third, agent-scoped row (agent-<name>-<remote-id>). The original controller-side queued row was therefore never touched, sat queued past theUPDATE_OPERATION_ACTIVE_TTL_MSdeadline inapp/store/update-operation.ts:295-300, and was force-failed by the TTL sweep — which fired the misleading "failed" notification with the row's still-valid container snapshot (hence the correct[mediavault]agent prefix). The fix threads the controller'soperationIdend-to-end so a single row is the source of truth for the whole lifecycle:AgentTrigger.trigger/triggerBatchnow accept and forwardruntimeContext;AgentClient.runRemoteTrigger/runRemoteTriggerBatchextract per-container operationIds via the existinggetRequestedOperationIdhelper and include them in the agent payload ({id, name, operationId}for single triggers;{...container, operationId}per entry for batches); the agent-side controllerrunTriggeraccepts anoperationIdin the request body (validated bytriggerRequestBodySchema) and threads it intorequestContainerUpdate; the agent-side batch endpoint extracts per-container operationIds into an{operationIds}runtimeContext before forwarding to the local trigger;EnqueueContainerUpdateOptionsgains anoperationIdfield honored bycreateAcceptedContainerUpdateRequest(single-container batches only; multi-container batches still mint per-container UUIDs); and a newAgentClient.resolveAgentOperationIdhelper checks the controller's operation store for an existing row at the raw (unscoped) id and reuses it when found — falling back to thetoAgentScopedIdform only when the agent does not echo a known controller id, preserving backwards compatibility with older agents. The controller-side queued row therefore transitions directly toin-progressandsucceeded/failedfrom the agent's lifecycle events, no parallel agent-scoped row is created, the TTL sweep has nothing stale to fail, and the spurious "update failed" notification disappears. -
#289 — Update-applied and update-failed notification triggers (Pushover, Telegram, etc.) and UI success toasts no longer silently drop for containers running on a connected agent. A user running drydock in a controller + agent topology reported on rc.25 that an "Update All" across two hosts produced the success toast and Pushover notification only for the container on the controller host, never for the same-name container on the agent host. Cause: when the agent finishes an update it sends a
dd:update-appliedSSE payload to the controller carrying a fullcontainersnapshot. The controller'sAgentClient.handleEventroutes this throughmaybeMarkAgentOperationSucceededFromAppliedPayload→markAgentOperationTerminal→ensureAgentOperationForTerminal→updateOperationStore.insertOperation+markOperationTerminal, butbuildAgentOperationBaseinapp/agent/AgentClient.tsconstructed the inserted row from{id, kind, containerName, containerId, newContainerId}only — the container snapshot was dropped on the floor. WhenmarkOperationTerminalthen firedemitTerminalLifecycleEvent(app/store/update-operation.ts), the resultingemitContainerUpdateApplied/emitContainerUpdateFailedpayload built bybuildTerminalLifecycleEventBaselackedcontainer. The notification handlerhandleContainerUpdateAppliedEvent(app/triggers/providers/Trigger.ts) then fell back tofindContainerByBusinessId(containerName), which compares the agent's barecontainerName(e.g.tautulli) against the controller-sidefullName(e.g.mediavault_docker_tautulli) and silently dropped — the same class offindContainerByBusinessIdmiss as #385 but on the agent-scoped operation path that #385 did not cover. The fix threads the agent's container snapshot through every level of the agent-scoped operation pipeline —buildAgentOperationBase,ensureAgentOperationForTerminal,markAgentOperationTerminal,maybeMarkAgentOperationSucceededFromAppliedPayload, andmaybeMarkAgentOperationFailedFromFailedPayload— stampingagent: this.nameso the controller's view of the container is consistent. Thedd:update-operation-changed-before-dd:update-appliedrace is handled by patching the container snapshot onto the existing active row viaupdateOperationbefore the terminal emit runs (only when the existing row lacks a container, never overwriting an existing snapshot).containeris added toMutableUpdateOperationFieldsinapp/store/update-operation.tsso terminal and active patches accept it. The store's terminal-lifecycle emit therefore naturally carries the agent's container intoemitContainerUpdateApplied/emitContainerUpdateFailed, thepayloadContainershortcut in the trigger handler succeeds, and both the notification trigger and the SSE toast fire end-to-end on the controller for agent-originated updates.
v1.5.0-rc.26
v1.5.0-rc.26
Full Changelog: v1.5.0-rc.25...v1.5.0-rc.26
[1.5.0-rc.26] — 2026-05-22
Fixed
-
Image reference construction — unanchored
/v2strip could silently corrupt references when the image name contained a/v2path segment.Registry.getImageFullNameand the controller-mode fallback inresolveContainerImageFullNameboth applied.replace(/\/v2/, '')to the fully concatenatedregistryUrl/imageName:tagstring. Because the regex was unanchored and non-global, if the image name contained a/v2segment (e.g.library/v2/tool) the strip would remove it from the image name rather than the registry URL — producing a silently wrong reference handed to Trivy. The fix extracts a shared pure helperbuildImageReference(app/registries/image-reference.ts) that cleans the registry URL before concatenation using anchored regexes (^https?:\/\/and/v2\/?$) so the URL scheme and trailing/v2API path are removed without touching anything in the image name. BothRegistry.getImageFullNameand the fallback branch ofresolveContainerImageFullNamenow delegate to this helper, eliminating the duplicate logic. -
#386 — Agents intermittently showing 0 running containers in the controller UI — a second recurrence the rc.25 fix did not close. The rc.25 fix suppressed the authoritative watcher snapshot whenever container enumeration failed or per-container enrichment errors dropped containers, but the recurrence reported on rc.25 is a different failure mode: a cold-start race between the controller's handshake and the agent's first watch cycle. When the controller's
AgentClient(re)connects to an agent's SSE stream it handshakes immediately viaGET /api/containers; if the agent'swatchatstartcron has not yet finished its first run, the agent's in-memory store is still empty and the handshake legitimately receives 0 containers (the agent log showsHandshake successful. Received 0 containers.~5 s beforeCron finished (4 containers watched, 0 errors)). The handshake then firesemitAgentConnected, the UI re-fetches/api/v1/agents, and the agent's running-container count renders 0. When the agent's cron completes moments later it pushes add:watcher-snapshot, andAgentClient.handleWatcherSnapshotEventingests the four containers into the controller store correctly — but nothing told the UI to refresh, becauseAgentsViewonly re-fetches the agent summary onagent-status-changed/connected/resync-requiredevents, not oncontainer-added/container-updated. The stale 0 therefore persisted until an unrelated reconnect event (such as an agent restart) fired. The fix adds a dedicatedAgentStatsChangedevent:app/event/index.tsgainsemitAgentStatsChanged/registerAgentStatsChanged(mirroring the existingAgentConnectedpair);AgentClient.handleWatcherSnapshotEventnow emitsemitAgentStatsChanged({ agentName })after every completed watcher snapshot;app/api/sse.tsbroadcasts it to UI SSE clients asdd:agent-stats-changed; andui/src/stores/eventStream.tsmaps that to the existingagent-status-changedbus event. A completed agent watch cycle therefore always refreshes the controller's agent-summary count, even when the handshake raced ahead of the agent's first cron. -
#342 — A container is no longer shown as "update available" with a blank target version after a transient registry error.
hasRawUpdateinapp/model/container.tscomparedtransformTag(image.tag.value)againsttransformTag(result.tag)without guarding an undefinedresult.tag. When a registry scan failed mid-flight (for example a Docker Hub or GHCR429) and left a containerresultpresent but itstagunset,transformTag(undefined)returnedundefined, thelocalTag !== remoteTagcomparison evaluated true, and the container was flaggedupdateAvailablewith anunknownupdate kind — which the UI renders as an update with no target version (the reporter saw this onimmich_redis).hasRawUpdatenow performs the tag comparison only when bothimage.tag.valueandresult.tagare defined, matching the existing guard ingetRawTagUpdate. Digest-only updates are unaffected: a container with an undefinedresult.tagbut a genuine digest change still reports the digest update. -
#386 follow-through — the controller's agent-summary container count now also refreshes on docker-event-driven container changes, not only completed cron cycles. The initial #386 fix emitted
emitAgentStatsChangedfromAgentClient.handleWatcherSnapshotEvent, the cron-watch path. An agent also ingests individual container add/remove/update events from the Docker event stream between cron cycles (handleContainerChangeEvent,handleContainerRemovedEvent), via the controller-initiatedwatch()path, and via the per-container controller-initiatedwatchContainer()path — none of which emitted the stats-changed signal, so a container started or stopped on an agent host could leave theAgentsViewrunning-container count stale until the next 6-hourly cron. All four paths now emitemitAgentStatsChangedafter mutating the controller store, keeping the count current in real time. -
#342 — GitHub release-notes lookups now survive GitHub's secondary rate limit instead of giving up on the first burst. Drydock authenticates its api.github.com release-notes requests by reusing the configured GHCR token, but a watch cycle still fans out a lookup for every watched container at once and trips GitHub's secondary rate limit — a
403GitHub returns to authenticated callers who burst too many requests. The shared retry helper (app/registries/http-retry.ts) only retried429/503, so the secondary-limit403was never retried: the provider loggedGitHub release notes lookup is rate-limitedand returned nothing.withRetrygains two optional, opt-in hooks —retryPredicate(retry a status outsideretryableStatuses) andretryDelayMs(per-attempt delay override) — leaving every existing caller unchanged.GithubProviderclassifies a403as a secondary rate limit only when it carries aretry-afterheader orx-ratelimit-remaining: 0, retries those (honouringretry-after/x-ratelimit-resetfor the delay), and leaves a genuine403authorization failure failing fast as before. Once retries are exhausted the provider arms a short module-level cooldown — driven by GitHub's own retry hint, floored at the 60 s default so aretry-after: 0hint cannot produce an already-expired cooldown — during which further release-notes lookups are skipped, so a single cron cycle no longer hammers an already-tripped limit container after container. The rate-limit warning now also records whether the request was authenticated. -
#342 — the registry-error tooltip on the Containers view now names the registry that failed. When a registry tag lookup errors (for example a
429rate limit) the container shows a registry-error badge whose tooltip previously rendered only the raw transport message —Registry error: Request failed with status code 429— with no indication of which registry was queried.registryErrorTooltipinui/src/views/ContainersView.vuenow derives the registry hostname from the container'sregistryUrland renders it through a newregistryError.detailWithRegistryi18n string ({registryHost} — {error}), e.g.ghcr.io — Request failed with status code 429. Containers whoseregistryUrlis absent or unparseable fall back to the original message unchanged.
v1.5.0-rc.25
v1.5.0-rc.25
[1.5.0-rc.25] — 2026-05-21
Fixed
-
#371 — Containers "Group By Stack" view no longer dissolves a multi-container stack into "Ungrouped" while its last container is mid-update. The flatten rule in
groupedContainers(ui/src/views/ContainersView.vue) previously keyed off the transient live container count (buckets[key].length === 1). During a docker recreate a 2-container stack momentarily shows only 1 live container (old removed, new not yet added), so the rule fired and dropped the stack header. The fix adds agroupAssignedSizeMapref (populated byloadGroups()from the groups API response and reset to{}on error) that records each group's API-assigned member count. The flatten condition is nowbuckets[key].length === 1 && groupAssignedSizeMap.value[key] === 1— a strict equality check so stacks whose assigned size is > 1 or transiently absent from the API response are never flattened mid-update. Genuine single-container stacks (assigned size exactly 1) are still flattened as before (GitHub Discussion #179). -
#386 — Agents intermittently showing 0 running containers in the controller UI — a recurrence of #362 that the rc.20 fix did not fully close. The rc.20 guard introduced a
containerEnumerationFailedflag inDocker.watch()(app/watchers/providers/docker/Docker.ts) that suppresses the authoritativeemitWatcherSnapshotwhengetContainers()itself throws. However,getContainers()does not throw on per-container enrichment failures:addImageDetailsToContainer()is called for each watched container, and any container whose enrichment throws is caught (.catch(error => return error)) and then silently filtered out by.filter(result => !(result instanceof Error) && result != null). A transient docker / socket-proxy hiccup during image inspect can therefore causegetContainers()to return a short or empty array without throwing — thecontainerEnumerationFailedguard does not fire,watch()emits an authoritativeemitWatcherSnapshotwith the degraded container list, and the controller'sAgentClient.handleWatcherSnapshotEventprunes every container not in that list, wiping the agent's view. The agent's own store is preserved because its local prune re-confirms each container viainspect(), which is why the agent kept reporting its containers and a restart's handshake re-synced the controller. The fix extends the snapshot-suppression in two steps:getContainers()now accepts an optionaldiagnosticsout-parameter and writes the number of containers dropped due to enrichment errors intodiagnostics.enrichmentErrors;watch()creates and passes this object on every call, logs aContainer enumeration degradedwarning when the count is non-zero, and suppressesemitWatcherSnapshotwhenever eithercontainerEnumerationFailedis true orenumerationDiagnostics.enrichmentErrors > 0. Per-container reports still emit as before; only the authoritative controller-side prune is deferred until a fully clean watch cycle. -
#385 — Telegram, Pushover, and other notification triggers no longer silently swallow
update-appliedandupdate-failedevents after a compose recreate or on multi-agent deployments. When an update routed through the operation queue completed, the terminal lifecycle event (update-appliedon success,update-failedon failure/rolled-back) was emitted fromapp/store/update-operation.ts:buildTerminalLifecycleEventBasewith onlycontainerName/containerId/operationIdon the payload — nocontainerobject. Notification handlers inapp/triggers/providers/Trigger.tsfell back tofindContainerByBusinessId(containerName), which missed during the ~8 s window between the old container being removed and the new one being re-watched after a compose recreate; the handler then dropped the event with aNo container found for update-applied event => ignoredebug log. This was the same class of race as #355 but for the operation-queue-driven path that bypassesUpdateLifecycleExecutor's direct emit. The fix persists a snapshot of theContaineron the operation entry at enqueue time (app/updates/request-update.ts:createAcceptedContainerUpdateRequest) andbuildTerminalLifecycleEventBasenow forwards that snapshot on the terminal-lifecycle payload — bothupdate-appliedandupdate-failed, closing the race for compose successes and failures alike. The agent SSE wire was also extended to forward the container snapshot end-to-end so multi-agent deployments get the same fix:sanitizeUpdateAppliedPayloadForAgentSseandsanitizeUpdateFailedPayloadForAgentSseinapp/agent/api/event.tsincludecontainerwhen present (previously stripped to scalars only), and the controller'sAgentClient.parseUpdateFailedEventPayloadaccepts and decorates an inbound container with the sourceagentname to mirror the existing applied-path behaviour. The snapshot is internal-only: a newtoApiUpdateOperationhelper inapp/store/update-operation.tsstrips it before serialising operations throughGET /api/v1/update-operations/:id,GET /api/containers/:id/update-operations, andPOST /api/operations/:id/cancel, so container labels anddetails.envare not exposed to API consumers.
v1.5.0-rc.24
v1.5.0-rc.24
[1.5.0-rc.24] — 2026-05-17
Changed
- Translations refreshed from Crowdin (commit
202f3d83). Human translations were synced from Crowdin for the ~110-string rc.23 i18n extraction sweep, updating the 16 non-English locales across theappShell,containerComponents,listViews,sharedComponents,configView,agentsView, andnotificationOutboxViewnamespaces. Strings that were previously falling back to English now render in each locale.
Fixed
-
#370 — Containers list "Version" column again shows the human-readable image tag for floating-tag + digest-watch containers, restoring the #356 fix that rc.20 inadvertently reverted. The rc.20
#342follow-up (commitb40d3db8) added a visiblesha256:… → sha256:…digest pair to the Containers table "Version" cell and card body for allupdateKind === 'digest'containers that are not digest-pinned. The intent was to surface the digest transition for hybrid containers where both the tag and the underlying image layer changed simultaneously; however, the change cast too wide a net: it also applied to floating-tag + digest-watch containers (e.g.prom/prometheus:latest,linuxserver/plexwith a transform tag) — exactly the rows that #356 fixed to show the human-readable tag instead of raw digest strings. TheupdateKind === 'digest' && !isDigestPinnedbranch of the table version cell and card body inui/src/components/containers/ContainersGroupedViews.vuehas been restored to the rc.19 behaviour: the version cell rendersc.currentTagas aCopyableTag(with the full digest delta in the cell tooltip), and the card body shows only the update-state badge (with the digest delta in the badge tooltip). The digest transition remains visible through the adjacent "kind" column update-state indicator and the container detail panels. Digest-pinned containers (whereisDigestPinnedis true) are unaffected and continue to show thesha256:… → sha256:…pair directly in the cell. -
#374 — Security scans no longer hand Trivy a raw registry v2 API URL, which had caused every scan in controller mode to fail.
resolveContainerImageFullName(app/api/container/shared.ts), used by both the security scan scheduler and the container API, falls back to composing the image reference directly fromcontainer.image.registry.urlwhenever the container's registry component is not present in the controller's registry state — the normal situation in controller mode (DD_LOCAL_WATCHER=false), where registries are configured on the agents rather than on the controller.registry.urlis stored in the registry v2 API base form (e.g.https://registry-1.docker.io/v2), so the fallback produced references such ashttps://registry-1.docker.io/v2/dgtlmoon/sockpuppetbrowser:0.0.3; Trivy then interpreted the scheme as a hostname and every scan failed withdial tcp: lookup https. The fallback now mirrorsRegistry.getImageFullName: it strips the URL scheme and the/v2path segment and uses an@separator for digest references, yielding a plainregistry-1.docker.io/dgtlmoon/sockpuppetbrowser:0.0.3reference. Containers whose registry component is available are unaffected — they already resolved through the correctgetImageFullNamepath.
v1.5.0-rc.23
v1.5.0-rc.23
[1.5.0-rc.23] — 2026-05-16
Added
-
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). The self-update helper container — the short-lived container that outlives Drydock to stop the old instance, health-check the replacement, and commit or roll back — was hardcoded to a bind-mounted Unix socket and aborted withSelf-update requires the Docker socket to be bind-mountedwhenever Drydock's watcher was configured with a TCPhost. That is the normal setup when a Docker socket proxy (such as sockguard ordocker-socket-proxy) mediates daemon access, so self-update was unavailable for those deployments even though every other container updated correctly.resolveHelperDockerConnectionnow inspects the watcher's Dockerode connection: a TCP host produces a TCP helper that is attached to Drydock's own Docker network (the container'sNetworkModeis cloned so the helper can resolve the proxy by DNS) and receivesDD_SELF_UPDATE_DOCKER_HOST/DD_SELF_UPDATE_DOCKER_PORT/DD_SELF_UPDATE_DOCKER_PROTOCOLinstead of a socket bind mount;runSelfUpdateControllerbuilds a TCP Dockerode client from those variables and skips the socket-only API-version probe and redirect guard. The bind-mounted-socket path is unchanged. When self-update runs through a filtering socket proxy the Drydock container must carry the proxy's ownership label so the helper is permitted to stop and replace it — seecontent/docs/current/configuration/self-update/index.mdx. -
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 for the Drydock self-container when an update is available but self-update can run neither over a bind-mounted socket nor a TCP host — i.e. the watcher uses a Unix socket and/var/run/docker.sockis not present in the container. The blocker locks the per-row Update button with an explanatory tooltip and makesPOST /containers/:id/updatereturn409, instead of the previous behaviour where the button appeared actionable and the update failed mid-flight with a socket error. Deployments that reach Docker over TCP report self-update as available, so the button is unaffected there. The check fails open: when the watcher cannot be resolved the blocker is not raised. -
i18n coverage extended to the remaining hardcoded UI strings across 28 components (discussion #329, commit
1b65e591). A full audit of all 82 UI components found approximately 110 English strings that bypassedvue-i18nand rendered raw regardless of the active locale. The extraction sweep covers:AppLayoutsearch scopes, group labels, section subtitles, and the five deprecation banner bodies (converted to<i18n-t>so embedded<code>elements stay translatable);ThemeTogglevariant names;DataFilterBarview-mode names;DetailPanelsize labels (S / M / L); update-kind labels (Major/Minor/Patch/Digest) inContainerFullPageDetailandContainerFullPageTabContent, which are now reactivecomputedmaps so locale switches take effect without a page reload; tail/status labels and the stdout/stderr stream-type labels inContainerLogs; action tooltips and button labels inContainersGroupedViews; error messages and empty-state fallback labels across the Agents, Config, Registries, Triggers, Watchers, Notifications, NotificationOutbox, and Security views; and theWATCHINGwatcher-status badge, which was rendering the raw backend enum string. New keys land inen/appShell.json,en/containerComponents.json,en/listViews.json,en/sharedComponents.json,en/configView.json,en/agentsView.json, anden/notificationOutboxView.json. Non-translatable identifiers — the product name "Drydock" and format strings such asspdx-json/cyclonedx-json— are intentionally left as literals. Other locales pick up the new keys via theenfallback immediately and will receive human translations on the next Crowdin sync.
Changed
- Self-update helper now prefers the bind-mounted Docker socket over a TCP watcher connection (commit
aa828d88). The previousresolveHelperDockerConnectionlogic checked the watcher's TCP modem first, meaning that any deployment where Drydock was configured with a TCP host (e.g. routing through a socket proxy) would always route the helper through that proxy — even when the target container itself had/var/run/docker.sockbind-mounted. For infrastructure updates (dd.update.mode=infrastructure), where the container being replaced is the socket proxy, this is fatal: the helper relies on the proxy being up, but the update stops it. 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. The TCP path is preserved as the fallback for pure socket-less deployments where Drydock reaches Docker exclusively over a remote host.
Fixed
-
Dashboard Host Status widget no longer auto-scrolls to the last host when the host list changes (commit
cbe815a6). The full-mode host list usedscroll-snap-type: y mandatorywith a measured tail spacer. Whenever the host-row set changed — a watcher or agent added, removed, or renamed, or a full-to-compact mode transition — Chromium re-snapped to the last row's snap point, leaving only the final host visible above a large empty gap. The scroll-snap classes (snap-y,snap-mandatory,snap-start), the dynamic tail-spacer element, and the measurement machinery behind it (theonUpdatedhook,requestAnimationFramescheduler, andResizeObserver-triggered recompute) have all been removed. The content-aware full/compact sizing that keeps whole rows visible was already sufficient; the snapping added no functional value and actively fought the layout on every data change. -
Dashboard Resource Usage widget minimum height raised so per-container CPU and Memory lists stay visible (commit
59719757). Theresource-usagewidget'sminHwas set to 3 grid units (approximately 122 px), which falls below the 180 px threshold at which the per-container lists collapse out of view. The minimum is now 7 grid units (approximately 306 px).applyConstraintsclamps any saved layout item that is below the new minimum on load, so existing dashboard configurations with a shrunken resource-usage widget are silently corrected on the next render rather than persisting an unusable layout. -
AgentClienttimers are now cleared when an agent is removed, preventing orphaned timeouts (commit03bf7211).AgentClientmaintains twosetTimeouthandles —stableConnectionTimer(arms 30 s after the SSE response arrives to reset the backoff counter) andreconnectTimer(fires the next reconnect attempt after the exponential-backoff delay). Neither was cancelled whenremoveAgentspliced the client out of the manager's list. An agent removed mid-reconnect-cycle or mid-stability-window would keep an armed timer alive indefinitely, potentially triggering astartSsecall against a client that was no longer tracked and leaking the associated resources. A new idempotentstop()method onAgentClientcancels both timers and nulls the handles;removeAgentnow callsstop()on each matching client before splicing it.
Security
-
TCP Docker host is validated before the self-update controller passes it to Dockerode (commit
441b4358).DD_SELF_UPDATE_DOCKER_HOSTwas forwarded to Dockerode without sanitization. A newvalidateTcpDockerHostfunction rejects values that contain a URL scheme prefix (tcp://,http://,https://, or any<scheme>://form), a userinfo segment (@), whitespace, or path separators (/or\), throwing a descriptive error before any network connection is attempted. This prevents an environment variable or compose-file value from inadvertently injecting a path or URL component that Dockerode would interpret in an unexpected way. The validated host and resolved port are also logged atINFOlevel so the connection target is auditable in the container logs.runSelfUpdateControllerwas additionally refactored to remove a control-flow asymmetry: socket and TCP paths previously diverged into separate Dockerode-construct-and-run blocks; they now share a single tail (disableSocketRedirectsremains socket-only). -
OIDC error logs now redact RFC-1918 IP addresses and absolute filesystem paths (commit
9b79de77). The rc.22getErrorChainMessageimprovement walkserror.causechains up to depth 5 and appends the results to OIDC warn logs, which is the right diagnostic behaviour — but TLS and connection errors in Node/undici frequently include private network addresses (e.g.connect ECONNREFUSED 10.0.0.5:2376) and absolute filesystem paths (e.g.error loading /etc/ssl/certs/ca-bundle.pem) that should not appear in logs shipped to centralised observability systems.sanitizeOidcErrorMessagenow applies two additional redaction passes after the existing URL and bearer-token passes: RFC-1918 IPv4 ranges (10.x, 172.16–31.x, 192.168.x) with an optional port are replaced with[internal-addr]; absolut...