github CodesWhat/drydock v1.7.0-rc.11

pre-release6 hours ago

v1.7.0-rc.11

Full Changelog: v1.6.1-rc.9...v1.7.0-rc.11

[1.7.0-rc.11] — 2026-09-05

Fixed

  • OIDC login on v1.7.0-rc.1 through rc.10 bounced straight back to the login page. The service worker's navigation fallback denied /api/ and nothing else, so every other top-level navigation was answered from the precached app shell. The identity provider redirects the browser to /auth/oidc/<name>/cb?code=..., which is a document navigation, so the callback was served index.html and never reached Express: no code exchange, no session, and the SPA booted and bounced to /login. Basic auth was unaffected because it authenticates over fetch, which the navigation fallback never touches. skipWaiting and clientsClaim re-register the worker on the next load, so clearing site data only helped until the page reloaded. The navigation fallback now skips every server-owned route (/api, /auth/, /health, /metrics) and serves the shell only for the SPA's own paths; the /auth settings view still loads from the shell, because Express matches that mount path too but has no handler for it, so the shell answers instead. v1.6 was never affected, it shipped no service worker. (#939)
  • A controller running its own default local watcher refused every container reported by an agent whose watcher was also named local. The ownership gate added in v1.7.0-rc.6 (#922) decided ownership of a container that had no store row yet from the watcher's name: the report was refused whenever the controller had a watcher registered under that same name. The race it closes is real, an agent can otherwise claim a container id that lives on the controller's own host before the controller's watch cycle writes the record, but a watcher name is not evidence of who owns an id, and it collides by default. A controller with no DD_WATCHER_* registers its default watcher as local, and the agents quickstart sets DD_WATCHER_LOCAL_SOCKET on the agent, so both sides are called local while watching entirely different hosts. On a fresh store that refused every container the agent reported, on every ingest path, logging watcher 'local' belongs to the controller's local watcher namespace and leaving zero agent rows in the UI and the API, permanently. An install that already had the rows stored kept working until a container was recreated, because the gate also feeds the prune keep-set: the stale row was pruned as a removal rather than a replacement and the new id was refused, losing that container for good. Ownership of a no-record id is now decided by what the controller's own watchers have actually enumerated. Each controller-local Docker watcher records the container ids its listContainers() call returns, replacing the set every cycle so a container that leaves the controller's host stops being claimed, and clearing it when the watcher is deregistered. An agent report with no store row is refused only when its id is in one of those sets, and the log line names the watcher holding it. A controller-side agent watcher running the Docker transport records nothing, since it enumerates the agent's daemon rather than the controller's. Disabling the controller's local watcher or renaming the watcher on either side were the only workarounds.
  • A rollback of a compose-managed container redeployed the update it was undoing. Dockercompose.recreateContainer wrote the image it was handed into the compose file and then handed the runtime refresh no image at all, so the refresh re-derived one from the container's own update candidate. The compose file said the backup and the container that came up ran the update, while the API answered Container rolled back successfully and the audit row recorded a success. Clearing the update result did not help either, because the fast resync that follows an update had already moved the container's tag to the new version by the time anyone rolled back. The recreate now passes the caller's image through to the refresh, so the container, the compose file and the report agree. Automatic rollback on a failed healthcheck never got that far on a compose stack at all: the health monitor called the trigger with only the container's id and name, and the compose action read the registry, the watcher and the compose labels off the container to find the service to rewrite, so it threw a TypeError before it reached the compose file. That failure was logged as an auto-rollback failure and recorded in an auto-rollback audit row carrying the error, neither of which stopped the unhealthy container from staying up on the version that had just failed its healthcheck. The monitor now hands over the whole container, carrying the id of the replacement it is rolling back, and the container type it arrives as requires the registry and the watcher, so the stand-in that caused this cannot compile again. Getting that far exposed the next one: the monitor logged that it was pulling the backup image and never pulled it. Neither recreate fetches the image for itself, because the manual rollback in app/api/backup.ts pulls before it calls in and a second pull there would be wasted work, so a backup image no longer on the host failed at create, on the Docker path after the running container had already been stopped and removed and on the compose path after the runtime refresh removed it and then "restored" it onto the failing update the rollback existed to undo. The automatic rollback now pulls the backup reference, digest-pinned when the record carries one, before it touches anything, and the compose refresh refuses an image it cannot find locally before the stop and remove rather than at create, so a rollback that cannot happen leaves the running container where it is. That pull is skipped when the backup image is already on the host, so a rollback on a host with no route to the registry, or one hitting a Docker Hub anonymous rate limit, still restores from the image the prune retained.
  • The arm64 image published for v1.7.0-rc.4 through rc.10 was an x86-64 image wearing an arm64 label. #881 rolled the node:24-alpine and alpine:3.24 base pins to digests that name a single amd64 manifest instead of the multi-arch image index, and buildx resolves a digest pin the same way for every --platform, so the arm64 stage built on an amd64 rootfs and nothing in the build, the manifest list, or the image scans noticed. On a Raspberry Pi the container died at exec /sbin/tini: exec format error (#1021). Both pins are back on image index digests, CI now rejects any FROM pinned to a per-platform manifest, and the release reads the ELF machine type of /sbin/tini, /usr/local/bin/node and /bin/healthcheck in each platform of the image it built and refuses to sign, tag, or promote one whose binaries do not match the platform they are published under.
  • A container that moved to an agent stayed stranded when the controller's local watcher was turned off. DD_LOCAL_WATCHER=false leaves the controller with no local watcher registered, and the startup prune that clears records naming a watcher that no longer exists skipped that case entirely, so the old controller-owned record survived every restart. The agent's report for the same container was then refused as owned by the controller, the record was never rewritten to name the agent, and the container sat on whatever it last said. The prune now runs when no local watcher was configured in the first place. It still leaves the store alone when watchers were configured and all of them failed to register, which looks identical in the registry but means something transient rather than an operator decision. A snooze, a maturity mode and any skipped tags or digests set from the UI live on that record, and they travel with the container rather than dying with it: the prune stashes them under the container's Docker id and the agent's copy inherits them when it arrives.
  • A container handed from the controller to an agent came up with its update policy cleared. The startup prune deletes the controller's record so the agent can claim the container, and it deleted it the way it deletes a container that is gone for good. Nothing would have helped if it had asked for a replacement either: the stash that carries an update policy across a delete is keyed on agent plus watcher plus name, and a move to an agent changes all three, so the incoming record looked up a key nothing had ever written. A snooze, a maturity mode and minimum age, and skipped tags or digests were all silently back to defaults, which for a maturity gate means the next update goes out with no soak at all. The prune now stashes the policy under the container's Docker id, which is the one thing the move leaves alone because it is the same physical container on both sides, and a record arriving under a new identity with that id inherits it. The identity-keyed stash is untouched and still covers the opposite move, a recreate, where the id changes and the identity does not. The same fix covers a watcher rename, which loses the policy for the same reason. A container recreated as part of the move mints a new Docker id and matches neither key, so that one still starts from the declarative policy.
  • One local watcher failing to register deleted its containers' records when a second watcher registered fine. The startup prune keeps records whose watcher is in the registry, and #1025 only protected the case where every configured watcher failed. With two configured and one of them broken by an unreadable CA file, a socket that is not there, or a value the schema rejects, the surviving watcher keeps the registered set non-empty, so the broken watcher's records were pruned as though the operator had renamed it away, and it came back to an empty slate once its configuration was fixed. The prune now keeps any record whose watcher is still present in DD_WATCHER_*, whether or not it registered this run, and only prunes a watcher that is neither registered nor configured. registerWatchers also waits for every registration to settle instead of returning the moment one rejects, so the prune reads the registry the run actually produced rather than whichever watchers happened to have landed by the time another one failed.

Documentation

  • The agents page didn't say registries have to be configured on every agent, not just the controller. A traditional agent runs its own watcher and does its own registry matching and update checks, so DD_REGISTRY_* configured only on the controller left every agent-reported container from that registry stamped unknown, credentials are never pushed from controller to agent. The controller needs the same registry configured too, or the container's registry link in the UI resolves to a registry the controller was never told about. A new "Registries on agents" section spells this out with a worked Gitea example on both sides. Reported in #945.
  • The agents page's registries example pointed DD_AGENT_REMOTE1_CAFILE at a CA file the controller's compose service never mounted. The environment variable named /certs/agent-ca.pem, but the example's services.drydock had no matching volumes: entry, so a reader following it as written couldn't find the file on the container's filesystem. The example now mounts ./certs/agent-ca.pem read-only at that path, and the inline comment points at the controller environment variables reference instead of citing an unrelated plain-HTTP quickstart.
  • The watchers page said manual updates bypass the maintenance window but didn't say the window gates the entire scheduled scan, not just installing an update. A closed window means new containers stay invisible in the UI until the next window opens, container state shown in the UI goes stale (a container stopped during the last window still shows as stopped, and a start action on it fails because Docker refuses to start a container that's already running), and update notifications are deferred right along with the update itself. A manual scan (POST /api/v1/containers/watch, or the UI) bypasses the window the same way a manual update does, because it calls the watcher's watch() directly rather than the cron path that checks it. Discussed in #946.

Don't miss a new drydock release

NewReleases is sending notifications on new releases.